diff --git a/ouroboros/loop_tool_execution.py b/ouroboros/loop_tool_execution.py index 2c50e86a6..168775611 100644 --- a/ouroboros/loop_tool_execution.py +++ b/ouroboros/loop_tool_execution.py @@ -31,6 +31,7 @@ from ouroboros.tool_capabilities import ( ) from ouroboros.tool_capabilities import ( UNTRUNCATED_REPO_READ_PATHS as _UNTRUNCATED_REPO_READ_PATHS, + UNTRUNCATED_REPO_READ_PREFIXES as _UNTRUNCATED_REPO_READ_PREFIXES, ) from ouroboros.tool_capabilities import ( UNTRUNCATED_TOOL_RESULTS as _UNTRUNCATED_TOOL_RESULTS, @@ -325,7 +326,8 @@ def _path_is_cognitive_artifact(tool_name: str, tool_args: Optional[Dict[str, An return normalized.startswith("memory/") and "/_backup/" not in normalized if tool_name == "read_file": - return normalized.startswith("prompts/") or normalized in _UNTRUNCATED_REPO_READ_PATHS + return (normalized.startswith(_UNTRUNCATED_REPO_READ_PREFIXES) + or normalized in _UNTRUNCATED_REPO_READ_PATHS) return False diff --git a/ouroboros/reference_books.py b/ouroboros/reference_books.py index 79ab1f179..a993da769 100644 --- a/ouroboros/reference_books.py +++ b/ouroboros/reference_books.py @@ -22,6 +22,35 @@ BOOK_ENTRYPOINTS = { } +def book_path_role(path: str) -> str: + """``"entrypoint"``, ``"chapter"`` or ``""`` for a repository path. + + Pure path shape and no I/O, so every consumer that must treat a relocated + chapter exactly as it treated the monolith it came out of — canonical + requiredness, untruncated reads, pack duplicate suppression, the + new-module documentation gate — asks ONE question instead of carrying its + own copy of the chapter population. + """ + normalized = str(path or "").replace("\\", "/").lstrip("./") + if normalized in set(BOOK_ENTRYPOINTS.values()): + return "entrypoint" + for book_id in BOOK_ENTRYPOINTS: + if normalized.startswith(f"docs/{book_id}/") and normalized.endswith(".md"): + return "chapter" + return "" + + +def book_entrypoint_for(path: str) -> str: + """The entrypoint of the book this path belongs to, else ``""``.""" + normalized = str(path or "").replace("\\", "/").lstrip("./") + role = book_path_role(normalized) + if role == "entrypoint": + return normalized + if role == "chapter": + return BOOK_ENTRYPOINTS[normalized.split("/")[1]] + return "" + + @dataclass(frozen=True) class ReferenceBook: book_id: str diff --git a/ouroboros/tool_capabilities.py b/ouroboros/tool_capabilities.py index 90bb807c8..41758dd21 100644 --- a/ouroboros/tool_capabilities.py +++ b/ouroboros/tool_capabilities.py @@ -206,6 +206,18 @@ UNTRUNCATED_REPO_READ_PATHS: frozenset[str] = frozenset({ "docs/DEVELOPMENT.md", }) +# Whole DIRECTORIES whose repository reads keep the same guarantee: the runtime +# prompts, and the two reference books' chapters. A prefix rather than a list +# of the current chapter filenames, because a hand-maintained population is +# exactly what goes stale when a book gains, splits or renames a chapter -- and +# a silently capped chapter read is a partial governance source that reads like +# a complete one. Four chapters exceed the 80,000-char `read_file` result cap. +UNTRUNCATED_REPO_READ_PREFIXES: tuple[str, ...] = ( + "prompts/", + "docs/architecture/", + "docs/development/", +) + # Per-tool char caps; omitted tools use DEFAULT_TOOL_RESULT_LIMIT. TOOL_RESULT_LIMITS: dict[str, int] = { "read_file": 80_000, diff --git a/ouroboros/tools/preflight_review_prompt.py b/ouroboros/tools/preflight_review_prompt.py index b67af96ba..43836394b 100644 --- a/ouroboros/tools/preflight_review_prompt.py +++ b/ouroboros/tools/preflight_review_prompt.py @@ -29,6 +29,7 @@ from ouroboros.triad_review import ( REVIEW_JSON_MATRIX_CONTRACT, ) from ouroboros.tools.review_helpers import ( + CANONICAL_GOVERNANCE_DOCS, build_rebuttal_section, REVIEW_SEVERITY_THRESHOLDS, REVIEW_THOROUGHNESS_BLOCK, @@ -131,9 +132,10 @@ def _build_blocking_history_section(drive_root: pathlib.Path, repo_key: str = "" # (the mandatory-read pointers of `_build_advisory_prompt`); the CHECKLISTS # entry is the surface's one checklist section. `_mandatory_read_corpus_chars` # measures exactly these, never a remembered size. -_MANDATORY_READ_DOCS = ( - "BIBLE.md", "docs/CHECKLISTS.md", "docs/DEVELOPMENT.md", "docs/DESIGN.md", "docs/ARCHITECTURE.md", -) +# The canonical five, from their one owner. A book entrypoint measures as its +# COMPOSED text here, because that is what the non-retrieving branch inlines +# and what a retrieving reviewer is told to read in full. +_MANDATORY_READ_DOCS = CANONICAL_GOVERNANCE_DOCS def _checklist_name(review_surface: str) -> str: diff --git a/ouroboros/tools/review.py b/ouroboros/tools/review.py index 8883912e0..4d096721c 100644 --- a/ouroboros/tools/review.py +++ b/ouroboros/tools/review.py @@ -554,11 +554,22 @@ def _preflight_check(commit_message: str, staged_files: str, f for f in new_files if f.startswith(("ouroboros/", "supervisor/")) and f.endswith(".py") ] - if new_logic_files and "docs/ARCHITECTURE.md" not in active_staged: + # The Architecture book is the obligation, not one file: a new module is + # documented in the CHAPTER that owns its subsystem, and demanding an + # entrypoint edit would only buy a membership-list touch that documents + # nothing. Any staged source of the book satisfies it. + from ouroboros.reference_books import BOOK_ENTRYPOINTS, book_entrypoint_for + + architecture_entrypoint = BOOK_ENTRYPOINTS["architecture"] + documented = any( + book_entrypoint_for(staged) == architecture_entrypoint for staged in active_staged + ) + if new_logic_files and not documented: return ( "⚠️ PREFLIGHT_BLOCKED: New files added in ouroboros/ or supervisor/ " - "but docs/ARCHITECTURE.md is not staged.\n" - " New structural additions must be documented in ARCHITECTURE.md " + "but no source of the Architecture book is staged.\n" + " New structural additions must be documented in the Architecture book " + f"(`{architecture_entrypoint}` or a `docs/architecture/` chapter) " "(Bible P6: authenticity / architectural mirror).\n" f" New files: {new_logic_files[:5]}\n" f" Currently staged: {', '.join(sorted(staged_set)) or '(none)'}" diff --git a/ouroboros/tools/review_context_atlas.py b/ouroboros/tools/review_context_atlas.py index 1e9e66044..41ba4261f 100644 --- a/ouroboros/tools/review_context_atlas.py +++ b/ouroboros/tools/review_context_atlas.py @@ -16,6 +16,7 @@ from ouroboros.runtime_mode_policy import ( PROTECTED_RUNTIME_PATHS, ) from ouroboros.tools.review_helpers import ( + is_canonical_governance_path, _FULL_REPO_BINARY_EXTENSIONS, _FULL_REPO_SKIP_DIR_PREFIXES, _MAX_FULL_REPO_FILE_BYTES, @@ -45,13 +46,6 @@ _COLLAPSED_INDEX_DISPOSITIONS = frozenset({ "vendored_minified", }) -_CANONICAL_CONTEXT_DOCS = frozenset({ - "BIBLE.md", - "docs/DEVELOPMENT.md", - "docs/DESIGN.md", - "docs/ARCHITECTURE.md", - "docs/CHECKLISTS.md", -}) _REVIEW_STACK_PATHS = frozenset({ "ouroboros/size_ratchet_manifest.py", @@ -557,7 +551,7 @@ def _build_file_facts( # Every early return sees the same answer; no branch re-derives it. force_include = _is_force_include(rel) is_anchor = rel in anchors - is_canonical = rel in _CANONICAL_CONTEXT_DOCS + is_canonical = is_canonical_governance_path(rel) facts.required = force_include or is_anchor or is_canonical # The classes owed IN FULL regardless of the change: for these the staged # diff is never a substitute for the artifact. An anchor is required @@ -854,7 +848,7 @@ def atlas_required_beyond_diff(rel: str) -> bool: ladder that chooses what to degrade and the assembler that refuses a degraded required artifact cannot drift apart. """ - return _is_force_include(rel) or rel in _CANONICAL_CONTEXT_DOCS + return _is_force_include(rel) or is_canonical_governance_path(rel) def _skip_by_dir(rel: str) -> bool: diff --git a/ouroboros/tools/review_file_pack.py b/ouroboros/tools/review_file_pack.py index e742266bf..c57e749b1 100644 --- a/ouroboros/tools/review_file_pack.py +++ b/ouroboros/tools/review_file_pack.py @@ -430,20 +430,32 @@ def triad_pack_exclusions( copy already inlined in this same prompt's governance prefix (``prefix_texts``: path -> the prefix's text) — pure duplication. + A reference-book entrypoint's prefix copy is the COMPOSED book, so a + touched CHAPTER is duplicated exactly when its current bytes already sit + in that composition — the same byte-identity fact, asked of a member + instead of a whole file. Without this the split would have withheld + nothing and inlined every touched chapter twice. + A carrier edited outside its spans, a prefix doc whose bytes differ from the prefix copy and a managed subject (the caller skips this helper: its reviewed delta is M0→staged, not HEAD→staged) all keep the full text.""" + from ouroboros.reference_books import book_entrypoint_for, book_path_role + carriers = span_only_release_carriers(repo_dir, paths) duplicated: list[str] = [] for rel in paths: - prefix_text = prefix_texts.get(rel) or "" - if not prefix_text or rel in carriers: + if rel in carriers: + continue + member_of = book_entrypoint_for(rel) if book_path_role(rel) == "chapter" else "" + prefix_text = prefix_texts.get(rel) or (prefix_texts.get(member_of) or "" if member_of else "") + if not prefix_text: continue try: - if (repo_dir / rel).read_text(encoding="utf-8") == prefix_text: - duplicated.append(rel) + current = (repo_dir / rel).read_text(encoding="utf-8") except Exception: continue + if current == prefix_text or (member_of and current and current in prefix_text): + duplicated.append(rel) return set(carriers) | set(duplicated), pack_exclusion_note(carriers, duplicated) diff --git a/ouroboros/tools/review_helpers.py b/ouroboros/tools/review_helpers.py index b934183c2..c87f88a5d 100644 --- a/ouroboros/tools/review_helpers.py +++ b/ouroboros/tools/review_helpers.py @@ -384,6 +384,52 @@ def build_skill_host_context(repo_dir: Path | None = None) -> str: return "\n\n".join(parts) +# The canonical governance corpus a packed review surface owes IN FULL. Two of +# the five are reference-book entrypoints, so their chapters are canonical too: +# `is_canonical_governance_path` is the one predicate that answers for both +# forms, and `canonical_governance_sources` resolves the actual population of a +# given tree. Four surfaces used to keep their own copy of this list. +CANONICAL_GOVERNANCE_DOCS = ( + "BIBLE.md", + "docs/DEVELOPMENT.md", + "docs/DESIGN.md", + "docs/ARCHITECTURE.md", + "docs/CHECKLISTS.md", +) + + +def is_canonical_governance_path(path: str) -> bool: + """One of the canonical five, or a chapter of one of the two books.""" + from ouroboros.reference_books import book_path_role + + normalized = str(path or "").replace("\\", "/").lstrip("./") + return normalized in CANONICAL_GOVERNANCE_DOCS or book_path_role(normalized) == "chapter" + + +def canonical_governance_sources(repo_dir: Path) -> tuple[str, ...]: + """Every canonical path a packed review inlines in full for THIS tree. + + The five documents plus the chapters each book's entrypoint declares, so a + pack that inlined a composed book does not ALSO owe its chapters as + separate snapshots. A book that cannot be read contributes its entrypoint + alone — the reader that assembles it reports the failure; this resolver + must not turn an unreadable book into a claim of wider coverage. + """ + from ouroboros.reference_books import BOOK_ENTRYPOINTS, load_reference_book + + root = Path(repo_dir) + resolved = [doc for doc in CANONICAL_GOVERNANCE_DOCS if (root / doc).is_file()] + for book_id, entrypoint in BOOK_ENTRYPOINTS.items(): + if entrypoint not in resolved: + continue + try: + book = load_reference_book(root, book_id) + except (OSError, ValueError): + continue + resolved.extend(chapter.source_path for chapter in book.chapters) + return tuple(dict.fromkeys(resolved)) + + def load_governance_doc( repo_dir: Path, rel_path: str, @@ -391,8 +437,26 @@ def load_governance_doc( on_missing: str = "explicit", fallback: str = "", ) -> str: - """Load a governance/review document relative to ``repo_dir`` with explicit miss policy.""" + """Load a governance/review document relative to ``repo_dir`` with explicit miss policy. + + A reference-book entrypoint resolves to the COMPOSED book. The entrypoint + alone is an orientation page and a membership list: handing it to a review + surface that believes it received the architecture map would deliver zero + chapters while every caller's contract says "in full". + """ + from ouroboros.reference_books import BOOK_ENTRYPOINTS, compose_book, load_reference_book + path = Path(repo_dir) / rel_path + book_id = next((key for key, entry in BOOK_ENTRYPOINTS.items() if entry == rel_path), None) + if book_id is not None: + try: + return compose_book(load_reference_book(Path(repo_dir), book_id)) + except (OSError, ValueError) as exc: + if on_missing == "silent": + return fallback + if on_missing == "placeholder": + return fallback if fallback else f"({rel_path} could not be assembled: {exc})" + return f"[⚠️ OMISSION: {rel_path} could not be assembled from its chapters ({path}): {exc}]" try: if path.is_file(): return path.read_text(encoding="utf-8") diff --git a/ouroboros/tools/scope_review_pack.py b/ouroboros/tools/scope_review_pack.py index ac1f319bd..874d469ec 100644 --- a/ouroboros/tools/scope_review_pack.py +++ b/ouroboros/tools/scope_review_pack.py @@ -23,6 +23,11 @@ import pathlib from dataclasses import dataclass from typing import Any, Optional +from ouroboros.tools.review_helpers import ( + CANONICAL_GOVERNANCE_DOCS, + canonical_governance_sources, + is_canonical_governance_path, +) from ouroboros.tools.review_prompt_text import ( _ANTI_THRASHING_RULE_VERDICT, _CONVERGENCE_RULE_TEXT, @@ -89,13 +94,10 @@ def _current_scope_context_manifest() -> dict: return dict(_SCOPE_CONTEXT_MANIFEST.get({}) or {}) -_CANONICAL_CONTEXT_DOCS = ( - "BIBLE.md", - "docs/DEVELOPMENT.md", - "docs/DESIGN.md", - "docs/ARCHITECTURE.md", - "docs/CHECKLISTS.md", -) +# The canonical corpus and its chapter membership have ONE owner +# (`review_helpers`); this alias keeps the historical local spelling for the +# reading order of `_load_canonical_context_docs` and `scope_review`'s import. +_CANONICAL_CONTEXT_DOCS = CANONICAL_GOVERNANCE_DOCS _CURRENT_TOUCHED_CONTEXT_SKIP_PREFIXES = ( @@ -109,7 +111,7 @@ def _should_skip_current_touched_context(path: str) -> bool: full atlas anchors, ladder-degradable — but never canonical docs).""" norm = str(path or "").replace("\\", "/").lstrip("./") return ( - norm in _CANONICAL_CONTEXT_DOCS + is_canonical_governance_path(norm) or any(norm.startswith(prefix) for prefix in _CURRENT_TOUCHED_CONTEXT_SKIP_PREFIXES) ) @@ -306,7 +308,9 @@ def _gather_scope_packs( # from requiredness classification. A canonical doc is claimed only if it exists. already_included = frozenset( set(snapshot_included_paths or frozenset()) - | {doc for doc in _CANONICAL_CONTEXT_DOCS if (repo_dir / doc).is_file()} + # A canonical book is inlined as its COMPOSED text, so its declared + # chapters are already in the prompt and must not be owed again. + | set(canonical_governance_sources(repo_dir)) ) _input_limit = _sr()._effective_scope_input_limit(scope_model=scope_model, **({"window_binding": window_binding} if window_binding else {})) try: diff --git a/tests/_governance_docs_shared.py b/tests/_governance_docs_shared.py new file mode 100644 index 000000000..adc95f627 --- /dev/null +++ b/tests/_governance_docs_shared.py @@ -0,0 +1,35 @@ +"""The ONE reader tests use for a governance document's complete text. + +`docs/ARCHITECTURE.md` and `docs/DEVELOPMENT.md` are reference-book +entrypoints: an orientation paragraph and an ordered `## Chapters` membership +list. Reading either with `Path.read_text()` returns that page and none of the +book, so a substring pin over it stops testing anything while still passing -- +which is worse than failing. Every test that asserts something about a +governance document's CONTENT resolves it here instead, and a book resolves to +its composed text exactly as the review surfaces receive it. +""" + +from __future__ import annotations + +import pathlib + +from ouroboros.reference_books import BOOK_ENTRYPOINTS, compose_book, load_reference_book + +REPO_ROOT = pathlib.Path(__file__).resolve().parents[1] + + +def governance_doc_text(rel_path: str, repo_root: pathlib.Path | None = None) -> str: + """The complete text of one governance document, composed when it is a book.""" + root = pathlib.Path(repo_root) if repo_root is not None else REPO_ROOT + book_id = next((key for key, entry in BOOK_ENTRYPOINTS.items() if entry == rel_path), None) + if book_id is not None: + return compose_book(load_reference_book(root, book_id)) + return (root / rel_path).read_text(encoding="utf-8") + + +def architecture_text(repo_root: pathlib.Path | None = None) -> str: + return governance_doc_text(BOOK_ENTRYPOINTS["architecture"], repo_root) + + +def development_text(repo_root: pathlib.Path | None = None) -> str: + return governance_doc_text(BOOK_ENTRYPOINTS["development"], repo_root) diff --git a/tests/test_acceptance_fixround.py b/tests/test_acceptance_fixround.py index 2564ce0b8..cf4ce425c 100644 --- a/tests/test_acceptance_fixround.py +++ b/tests/test_acceptance_fixround.py @@ -5,6 +5,7 @@ from pathlib import Path from types import SimpleNamespace import pytest +from tests._governance_docs_shared import architecture_text, development_text def test_prompt_projection_keeps_panels_ahead_of_an_oversized_lens(): @@ -721,8 +722,8 @@ def test_budget_ladder_stops_shedding_after_predecessor_fits(): def test_acceptance_docs_have_complete_sentence_boundaries(): - development = Path("docs/DEVELOPMENT.md").read_text(encoding="utf-8") - architecture = Path("docs/ARCHITECTURE.md").read_text(encoding="utf-8") + development = development_text() + architecture = architecture_text() assert "silent false green. The\n Every forced rail" not in development assert "never certify success; An OPEN plan wave" not in architecture diff --git a/tests/test_available_subagents_runtime_review_fixes.py b/tests/test_available_subagents_runtime_review_fixes.py index 869c50e88..1a4e50be7 100644 --- a/tests/test_available_subagents_runtime_review_fixes.py +++ b/tests/test_available_subagents_runtime_review_fixes.py @@ -7,6 +7,7 @@ import re from types import SimpleNamespace import pytest +from tests._governance_docs_shared import governance_doc_text @pytest.fixture(autouse=True) @@ -515,7 +516,7 @@ def test_delegate_start_recipes_match_the_fresh_start_schema(): # audit removed. Any recipe the prompt GAINS must still be schema-valid. tolerant = {"docs/CHECKLISTS.md", "prompts/SYSTEM.md"} for relative in (*recipe_paths, "docs/CHECKLISTS.md"): - text = (repo / relative).read_text(encoding="utf-8") + text = governance_doc_text(relative, repo) recipes = re.findall(r"\bdelegate_start\(([^)]*)\)", text, flags=re.DOTALL) assert recipes or relative in tolerant, ( f"expected at least one delegate_start recipe in {relative}" diff --git a/tests/test_claudexor_owned_daemon.py b/tests/test_claudexor_owned_daemon.py index a3d68049b..0c9f880de 100644 --- a/tests/test_claudexor_owned_daemon.py +++ b/tests/test_claudexor_owned_daemon.py @@ -11,6 +11,7 @@ import shlex import pytest from ouroboros import claudexor_daemon as owned +from tests._governance_docs_shared import architecture_text def _write_descriptor(config_dir: pathlib.Path, *, port: int = 45678) -> None: @@ -3610,8 +3611,7 @@ def test_the_proxy_count_in_the_docs_matches_the_handlers_that_exist(tmp_path): f"does not say \"{expected} THIN proxies\"" ) - arch = (pathlib.Path(__file__).resolve().parents[1] / "docs" / "ARCHITECTURE.md") \ - .read_text(encoding="utf-8") + arch = architecture_text() account_line = next(ln for ln in arch.splitlines() if "claudexor_accounts.py" in ln) assert f"{expected} thin proxies" in account_line.lower(), ( "the gateway map still counts a different number of account proxies: " diff --git a/tests/test_cybergym_benchmark.py b/tests/test_cybergym_benchmark.py index 3722c265f..42f171861 100644 --- a/tests/test_cybergym_benchmark.py +++ b/tests/test_cybergym_benchmark.py @@ -13,6 +13,7 @@ from pathlib import Path from devtools.benchmarks.common import launcher_audit from ouroboros.configured_subagents import parse_configured_subagents from ouroboros.reviewer_slot_config import parse_reviewer_slots +from tests._governance_docs_shared import architecture_text, development_text REPO = Path(__file__).resolve().parents[1] PROFILE = REPO / "devtools" / "benchmarks" / "cybergym" / "settings_base.json" @@ -142,7 +143,7 @@ def test_benchmark_inventory_points_to_cybergym_docs(): common_readme = (REPO / "devtools" / "benchmarks" / "README.md").read_text( encoding="utf-8" ) - architecture = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(REPO) assert "cybergym/" in common_readme assert "devtools/benchmarks/cybergym/" in architecture diff --git a/tests/test_delegated_skill_payload.py b/tests/test_delegated_skill_payload.py index da4aea393..9576389d3 100644 --- a/tests/test_delegated_skill_payload.py +++ b/tests/test_delegated_skill_payload.py @@ -18,6 +18,7 @@ import subprocess import pytest from ouroboros import delegate_custody as custody +from tests._governance_docs_shared import architecture_text, development_text from ouroboros.subagent_worktrees import ( find_execution_snapshot, provision_payload_snapshot, @@ -1166,8 +1167,7 @@ def test_schema_and_docs_split_git_staging_from_payload_live_apply(): decision = entry.schema["parameters"]["properties"]["decision"]["description"] assert "STAGED into your active root" in decision assert "applied LIVE into the non-Git payload" in decision - arch = (pathlib.Path(__file__).resolve().parents[1] / "docs" / - "ARCHITECTURE.md").read_text(encoding="utf-8") + arch = architecture_text() assert "staging substrate differs" in arch assert "A SKILL-PAYLOAD target captures through the payload adapter" in arch assert "QUEUES the extension reconcile request" in arch diff --git a/tests/test_doc_context.py b/tests/test_doc_context.py index 9ea74397c..eba453d2e 100644 --- a/tests/test_doc_context.py +++ b/tests/test_doc_context.py @@ -21,6 +21,7 @@ SYSTEM + BIBLE are tier-0 and always full. import os import pathlib import tempfile +from tests._governance_docs_shared import governance_doc_text # Unique sentinel placed inside the ARCHITECTURE body so we can prove the full # body is inlined (max) vs replaced by a structure-only nav map (low). @@ -91,7 +92,7 @@ def test_plan_review_docs_pin_fail_closed_exact_artifact_custody(): repo = pathlib.Path(__file__).resolve().parents[1] for relative in ("docs/ARCHITECTURE.md", "docs/DEVELOPMENT.md"): - text = (repo / relative).read_text(encoding="utf-8") + text = governance_doc_text(relative, repo) assert "plan_review_exact_artifact_unavailable" in text, relative assert "only when no exact artifact reference exists" in text, relative diff --git a/tests/test_external_workspace_documentation.py b/tests/test_external_workspace_documentation.py index 0f6600914..cd5252be6 100644 --- a/tests/test_external_workspace_documentation.py +++ b/tests/test_external_workspace_documentation.py @@ -1,6 +1,7 @@ """The public workspace contract must keep ordinary folders visible.""" from pathlib import Path +from tests._governance_docs_shared import architecture_text, development_text ROOT = Path(__file__).resolve().parents[1] @@ -8,7 +9,7 @@ ROOT = Path(__file__).resolve().parents[1] def test_external_workspace_docs_distinguish_plain_folders_from_git_operations(): readme = (ROOT / "README.md").read_text(encoding="utf-8") - architecture = (ROOT / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(ROOT) control = (ROOT / "ouroboros" / "tools" / "control.py").read_text(encoding="utf-8") assert "ordinary folders or separate Git worktree roots" in readme assert "ordinary file and process work runs directly" in readme diff --git a/tests/test_health_invariants_ownership.py b/tests/test_health_invariants_ownership.py index af12b5917..245d8d7cd 100644 --- a/tests/test_health_invariants_ownership.py +++ b/tests/test_health_invariants_ownership.py @@ -14,6 +14,7 @@ import types import ouroboros.context_health as context_health from ouroboros.delegate_custody import RunCustody +from tests._governance_docs_shared import architecture_text, development_text def _env(tmp_path): @@ -61,10 +62,7 @@ def test_non_owner_gets_no_call_shaped_instruction(tmp_path, monkeypatch): def test_architecture_states_the_terminal_owner_apply_reject_authority_split(): - import pathlib - - architecture = (pathlib.Path(__file__).resolve().parents[1] / "docs" / - "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text() assert "Apply requires the caller's active Git root or fresh payload binding" in architecture assert "Reject requires only the owner's proven terminality" in architecture assert "a live top-level task with a different active root may reject and release" in architecture diff --git a/tests/test_legacy_timeout_retirement.py b/tests/test_legacy_timeout_retirement.py index 18b12f909..d2630aa3f 100644 --- a/tests/test_legacy_timeout_retirement.py +++ b/tests/test_legacy_timeout_retirement.py @@ -117,16 +117,28 @@ def test_no_runtime_or_settings_surface_still_names_either_key(): "tests/test_rc_audit_fixture_suite.py", "tests/test_settings_honesty.py", "tests/test_heartbeat_presentation.py", - "docs/ARCHITECTURE.md", # the retirement is documented + # The retirement is documented in the Architecture book: the entrypoint + # is now a membership list, so the prose lives in the chapter that owns + # the frozen contracts. A book source is allowed as a whole class, so a + # later chapter split cannot silently turn a record of a removal into a + # live surface finding. + "docs/ARCHITECTURE.md", "ADOPTION_v7next.md", # ...and adopted: the D04 row names # what it retired, same as the already # skipped docs/v7next/ ledger. A record # of a removal is not a live surface } + from ouroboros.reference_books import book_entrypoint_for + offenders = [] for pattern in ("*.py", "*.js", "*.json", "*.md", "*.html"): for path in REPO.rglob(pattern): rel = path.relative_to(REPO).as_posix() + # A reference book is allowed as a BOOK: whichever chapter of an + # allowlisted entrypoint carries the retirement record, the record + # is still documentation and not a live surface. + if book_entrypoint_for(rel) in allowed: + continue if rel in allowed or rel.startswith(("venv", "node_modules", "docs/v7next/", "docs/archive/")): continue try: diff --git a/tests/test_packaging_sync.py b/tests/test_packaging_sync.py index 7b4c2fffa..f9da9378e 100644 --- a/tests/test_packaging_sync.py +++ b/tests/test_packaging_sync.py @@ -2,6 +2,7 @@ import pathlib import re import pytest +from tests._governance_docs_shared import architecture_text, development_text from ouroboros.tools.release_sync import ( RELEASE_ASSET_TEMPLATES, @@ -89,7 +90,7 @@ def test_release_guidance_accepts_author_facing_and_pep440_forms(): def test_architecture_docs_describe_bundle_bootstrap_not_per_launch_core_sync(): - architecture = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(REPO) assert "scripts/build_repo_bundle.py" in architecture assert "repo.bundle" in architecture @@ -214,7 +215,7 @@ def test_install_page_matches_macos_quick_start_and_model_prerequisite(): def test_architecture_doc_describes_build_script_release_tag_check(): - architecture = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(REPO) assert "Release tag prerequisite" in architecture assert "scripts/build_repo_bundle.py" in architecture @@ -275,7 +276,7 @@ def test_architecture_doc_does_not_claim_ensure_managed_repo_fetches(): """ensure_managed_repo only validates + ensures the managed remote is configured; the actual fetch lives in supervisor.git_ops.checkout_and_reset. The ARCHITECTURE.md startup flow must not conflate the two.""" - architecture = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(REPO) assert "ensure_managed_repo()" in architecture assert "supervisor/git_ops.checkout_and_reset" in architecture @@ -311,7 +312,7 @@ def test_architecture_module_tree_lists_all_live_extension_http_endpoints(): same document. Specifically the Phase 5 review surface ``POST /api/skills//review`` is exported via ``server.py`` and must appear in both places.""" - architecture = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(REPO) # Module map entry lives on the ``gateway/extensions.py`` tree line. tree_idx = architecture.find("├── extensions.py") @@ -328,7 +329,7 @@ def test_architecture_doc_lists_valid_extension_route_methods_in_frozen_contract ``__all__`` + ``tests/test_contracts.py``). The ARCHITECTURE §11.1 frozen-contract table must list it alongside the other Phase 4 plugin_api exports so the doc/code mirror is accurate.""" - architecture = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(REPO) assert "VALID_EXTENSION_ROUTE_METHODS" in architecture assert "test_extension_route_methods_contract_matches_server_dispatch" in architecture @@ -339,7 +340,7 @@ def test_architecture_doc_describes_extension_staging_surface(): runtime subdirectory under ``data/state/skills//``. The architecture doc's skills data-layout section must describe it so the doc/code mirror is accurate.""" - architecture = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(REPO) assert "__extension_imports/" in architecture assert "_stage_extension_import_tree" in architecture diff --git a/tests/test_reference_book_readers.py b/tests/test_reference_book_readers.py new file mode 100644 index 000000000..a839487e5 --- /dev/null +++ b/tests/test_reference_book_readers.py @@ -0,0 +1,225 @@ +"""A relocated chapter must be read exactly as the monolith it came out of. + +The split turned one physical governance file into an entrypoint plus chapters. +Every reader that used to get ~1 MB of prose from one `read_text()` now gets a +membership list unless it asks for the BOOK, and every predicate keyed on the +path used to answer for the whole corpus with one exact string. This module +pins the four places that difference is load-bearing: the full-book loader, the +canonical-corpus predicate, the pack's duplicate suppression, and the +untruncated-read guarantee -- plus the documentation gate, which must accept the +chapter that actually documents a new module. +""" + +import pathlib + +import pytest + +from ouroboros.reference_books import BOOK_ENTRYPOINTS, book_entrypoint_for, book_path_role + +REPO = pathlib.Path(__file__).resolve().parents[1] + + +def _chaptered_corpus(root: pathlib.Path, *, body: str = "The exact chapter body.\n") -> dict: + files = {} + for book_id, entrypoint in BOOK_ENTRYPOINTS.items(): + files[entrypoint] = ( + f"# {book_id.title()}\n\nThe authored orientation.\n\n## Chapters\n\n" + f"- [Only chapter]({book_id}/only.md)\n" + ) + files[f"docs/{book_id}/only.md"] = f"# Only chapter\n\nWhy it exists.\n\n## Detail\n\n{body}" + for rel, text in files.items(): + target = root / rel + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text, encoding="utf-8") + return files + + +# --- path role ------------------------------------------------------------- + +@pytest.mark.parametrize("path,role", [ + ("docs/ARCHITECTURE.md", "entrypoint"), + ("docs/DEVELOPMENT.md", "entrypoint"), + ("docs/architecture/06-agent-core.md", "chapter"), + ("docs/development/14-build-and-ci.md", "chapter"), + ("docs/reference-books-migration.md", ""), + ("docs/CHECKLISTS.md", ""), + ("docs/architecture/notes.txt", ""), + ("", ""), +]) +def test_book_path_role_answers_by_shape_without_reading_the_tree(path, role): + assert book_path_role(path) == role + + +def test_book_entrypoint_for_maps_a_chapter_back_to_its_own_book(): + assert book_entrypoint_for("docs/development/09-process-custody-rule.md") == "docs/DEVELOPMENT.md" + assert book_entrypoint_for("docs/ARCHITECTURE.md") == "docs/ARCHITECTURE.md" + assert book_entrypoint_for("BIBLE.md") == "" + + +# --- the full-book loader -------------------------------------------------- + +def test_load_governance_doc_delivers_the_composed_book_not_the_membership_page(tmp_path): + from ouroboros.tools.review_helpers import load_governance_doc + + _chaptered_corpus(tmp_path, body="THE-ONLY-CHAPTER-BODY\n") + for entrypoint in BOOK_ENTRYPOINTS.values(): + text = load_governance_doc(tmp_path, entrypoint) + assert "THE-ONLY-CHAPTER-BODY" in text, entrypoint + assert "## Chapters" in text, entrypoint + # A non-book governance document is untouched by the book branch. + (tmp_path / "BIBLE.md").write_text("# Constitution\n", encoding="utf-8") + assert load_governance_doc(tmp_path, "BIBLE.md") == "# Constitution\n" + + +def test_an_unassemblable_book_is_a_named_omission_not_a_short_entrypoint(tmp_path): + from ouroboros.tools.review_helpers import load_governance_doc + + _chaptered_corpus(tmp_path) + (tmp_path / "docs/architecture/only.md").unlink() + explicit = load_governance_doc(tmp_path, "docs/ARCHITECTURE.md") + assert "OMISSION" in explicit and "could not be assembled" in explicit + assert "## Chapters" not in explicit, "a failure must not look like a delivered book" + assert load_governance_doc(tmp_path, "docs/ARCHITECTURE.md", on_missing="silent") == "" + + +def test_the_live_books_reach_the_packed_review_surfaces_whole(): + from ouroboros.reference_books import compose_book, load_reference_book + from ouroboros.tools.review_helpers import load_governance_doc + + for book_id, entrypoint in BOOK_ENTRYPOINTS.items(): + book = load_reference_book(REPO, book_id) + delivered = load_governance_doc(REPO, entrypoint) + assert delivered == compose_book(book) + assert len(delivered) > 10 * len(book.entrypoint.text) + + +# --- the canonical corpus -------------------------------------------------- + +def test_a_chapter_is_canonical_exactly_as_its_entrypoint_was(tmp_path): + from ouroboros.tools.review_context_atlas import atlas_required_beyond_diff + from ouroboros.tools.review_helpers import ( + canonical_governance_sources, + is_canonical_governance_path, + ) + + assert is_canonical_governance_path("docs/architecture/06-agent-core.md") + assert is_canonical_governance_path("docs/DEVELOPMENT.md") + assert not is_canonical_governance_path("docs/reference-books-migration.md") + # Owed in full regardless of the change: the staged diff is never a + # substitute for a canonical artifact, chapter or entrypoint. + assert atlas_required_beyond_diff("docs/development/06-rules-by-change-class.md") + + _chaptered_corpus(tmp_path) + (tmp_path / "BIBLE.md").write_text("# Constitution\n", encoding="utf-8") + sources = canonical_governance_sources(tmp_path) + assert "docs/architecture/only.md" in sources and "docs/development/only.md" in sources + assert "BIBLE.md" in sources + assert "docs/CHECKLISTS.md" not in sources, "an absent document is not claimed" + assert len(sources) == len(set(sources)) + + +def test_an_unreadable_book_contributes_its_entrypoint_alone(tmp_path): + from ouroboros.tools.review_helpers import canonical_governance_sources + + _chaptered_corpus(tmp_path) + (tmp_path / "docs/development/only.md").unlink() + sources = canonical_governance_sources(tmp_path) + assert "docs/DEVELOPMENT.md" in sources + assert not any(s.startswith("docs/development/") for s in sources), ( + "a book that cannot be read must not widen the claimed coverage" + ) + + +# --- pack duplicate suppression ------------------------------------------- + +def test_a_touched_chapter_is_withheld_when_the_prefix_carries_its_book(tmp_path): + from ouroboros.tools.review_file_pack import triad_pack_exclusions + from ouroboros.tools.review_helpers import load_governance_doc + + _chaptered_corpus(tmp_path) + composed = load_governance_doc(tmp_path, "docs/ARCHITECTURE.md") + touched = ["docs/architecture/only.md", "docs/development/only.md"] + excluded, note = triad_pack_exclusions( + tmp_path, touched, prefix_texts={"docs/ARCHITECTURE.md": composed}, + ) + assert "docs/architecture/only.md" in excluded + assert "docs/development/only.md" not in excluded, ( + "only the book whose composed copy IS in the prefix may be withheld" + ) + assert "docs/architecture/only.md" in note + + # A chapter edited after the prefix was rendered keeps its full text. + (tmp_path / "docs/architecture/only.md").write_text( + "# Only chapter\n\nWhy it exists.\n\n## Detail\n\nEdited after the prefix.\n", + encoding="utf-8", + ) + excluded_after, _ = triad_pack_exclusions( + tmp_path, touched, prefix_texts={"docs/ARCHITECTURE.md": composed}, + ) + assert "docs/architecture/only.md" not in excluded_after + + +# --- the documentation gate ------------------------------------------------ + +def test_the_new_module_gate_takes_the_chapter_that_documents_the_module(): + from ouroboros.tools import review + + staged = "A ouroboros/brand_new_owner.py\n" + blocked = review._preflight_check("add an owner", staged, REPO) + assert blocked and "Architecture book" in blocked + + for documented in ("docs/ARCHITECTURE.md", "docs/architecture/01-high-level-architecture.md"): + assert review._preflight_check( + "add an owner", staged + f"M {documented}\n", REPO, + ) is None, documented + + assert review._preflight_check( + "add an owner", staged + "M docs/reference-books-migration.md\n", REPO, + ) is not None, "a docs file outside the book documents no module" + + +# --- the untruncated-read guarantee --------------------------------------- + +def test_a_chapter_read_keeps_the_untruncated_guarantee_the_monolith_had(): + from ouroboros.loop_tool_execution import _path_is_cognitive_artifact, _truncate_tool_result + from ouroboros.tool_capabilities import TOOL_RESULT_LIMITS + + limit = TOOL_RESULT_LIMITS["read_file"] + oversized = "x" * (limit + 5_000) + for rel in ("docs/architecture/06-agent-core.md", "docs/development/06-rules-by-change-class.md"): + assert _path_is_cognitive_artifact("read_file", {"path": rel}), rel + assert _truncate_tool_result(oversized, "read_file", {"path": rel}) == oversized, rel + # The guarantee is a book-source class, not every markdown file under docs/. + assert not _path_is_cognitive_artifact("read_file", {"path": "docs/reference-books-migration.md"}) + assert len(_truncate_tool_result(oversized, "read_file", + {"path": "docs/reference-books-migration.md"})) < len(oversized) + + +def test_every_chapter_over_the_read_cap_is_covered_by_the_prefix_guarantee(): + """The chapters that actually need it, measured rather than assumed.""" + from ouroboros.loop_tool_execution import _path_is_cognitive_artifact + from ouroboros.reference_books import load_reference_book + from ouroboros.tool_capabilities import TOOL_RESULT_LIMITS + + oversized = [] + for book_id in BOOK_ENTRYPOINTS: + for chapter in load_reference_book(REPO, book_id).chapters: + assert _path_is_cognitive_artifact("read_file", {"path": chapter.source_path}) + if len(chapter.raw) > TOOL_RESULT_LIMITS["read_file"]: + oversized.append(chapter.source_path) + assert oversized, "if no chapter exceeds the cap, this guarantee needs a different proof" + + +# --- the mandatory-read corpus -------------------------------------------- + +def test_the_mandatory_read_pointer_measures_the_book_not_its_membership_page(): + from ouroboros.tools import preflight_review_prompt as prompt + + measured = prompt._mandatory_read_corpus_chars(REPO) + entrypoints = sum(len((REPO / rel).read_text(encoding="utf-8")) + for rel in BOOK_ENTRYPOINTS.values()) + assert measured > 20 * entrypoints, ( + "a retrieving reviewer told to read the books in full must be budgeted " + f"for their chapters; measured {measured} against {entrypoints} of membership" + ) + assert set(prompt._MANDATORY_READ_DOCS) >= set(BOOK_ENTRYPOINTS.values()) diff --git a/tests/test_repo_read_limits.py b/tests/test_repo_read_limits.py index 5613d6d35..33218c234 100644 --- a/tests/test_repo_read_limits.py +++ b/tests/test_repo_read_limits.py @@ -10,6 +10,7 @@ Also covers the core governance artifact invariants introduced in the """ from unittest.mock import MagicMock +from tests._governance_docs_shared import architecture_text, development_text def _make_ctx(tmp_path): @@ -462,7 +463,7 @@ def test_development_md_contains_core_governance_invariant(): import pathlib dev_md = pathlib.Path(__file__).resolve().parent.parent / "docs" / "DEVELOPMENT.md" assert dev_md.exists(), "docs/DEVELOPMENT.md must exist" - content = dev_md.read_text(encoding="utf-8") + content = development_text() required_phrases = [ "Core Governance Artifacts", diff --git a/tests/test_review_context_atlas.py b/tests/test_review_context_atlas.py index f40156bd6..a3df1517d 100644 --- a/tests/test_review_context_atlas.py +++ b/tests/test_review_context_atlas.py @@ -794,13 +794,27 @@ def test_scope_ladder_never_hands_required_beyond_diff_paths_to_diff_only(tmp_pa assert "docs/ARCHITECTURE.md" not in degradable -def test_canonical_context_docs_membership_includes_design(): - """Drift guard for the three synchronized canonical-doc lists: the atlas - copy must carry docs/DESIGN.md like scope_review's tuple and the external - substrate set (fable p0-review note, 2026-08-31).""" - from ouroboros.tools.review_context_atlas import _CANONICAL_CONTEXT_DOCS +def test_canonical_context_docs_have_one_owner_and_cover_book_chapters(): + """This was a drift guard over three synchronized canonical-doc lists (fable + p0-review note, 2026-08-31). The lists are now ONE owner, so the guard is + the identity instead: the atlas reads `review_helpers`, the scope pack's + tuple IS that value, and a book chapter answers exactly as its entrypoint + does -- a copy that omitted the chapters would quietly stop treating + relocated governance prose as canonical.""" + from ouroboros.tools import review_context_atlas as atlas + from ouroboros.tools.review_helpers import ( + CANONICAL_GOVERNANCE_DOCS, + is_canonical_governance_path, + ) + from ouroboros.tools.scope_review import _CANONICAL_CONTEXT_DOCS - assert "docs/DESIGN.md" in _CANONICAL_CONTEXT_DOCS + assert "docs/DESIGN.md" in CANONICAL_GOVERNANCE_DOCS + assert tuple(_CANONICAL_CONTEXT_DOCS) == CANONICAL_GOVERNANCE_DOCS + assert atlas.is_canonical_governance_path is is_canonical_governance_path + for doc in CANONICAL_GOVERNANCE_DOCS: + assert atlas.atlas_required_beyond_diff(doc), doc + assert atlas.atlas_required_beyond_diff("docs/architecture/06-agent-core.md") + assert not atlas.atlas_required_beyond_diff("docs/reference-books-migration.md") def test_atlas_diff_only_reason_override_is_the_callers_typed_omission(tmp_path): diff --git a/tests/test_review_cycles.py b/tests/test_review_cycles.py index 8ac696d59..c2a5fa62e 100644 --- a/tests/test_review_cycles.py +++ b/tests/test_review_cycles.py @@ -23,6 +23,7 @@ from ouroboros import config as cfg from ouroboros import review_cycles as rc from ouroboros import task_pacing from ouroboros.contracts.task_contract import normalize_budget_profile +from tests._governance_docs_shared import architecture_text, development_text REPO = pathlib.Path(__file__).resolve().parents[1] KEY = "OUROBOROS_REVIEW_MAX_CYCLES" @@ -549,8 +550,8 @@ def test_exhausted_event_is_durable_even_with_a_live_queue(tmp_path): def test_docs_describe_shared_key_and_new_module_size(): - dev = (REPO / "docs" / "DEVELOPMENT.md").read_text(encoding="utf-8") - arch = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + dev = development_text(REPO) + arch = architecture_text(REPO) assert dev.count(KEY) >= 2 and "review_cycles.py" in dev assert f"| {KEY} |" in arch # the LEGACY row documents the load-time migration, not a runtime binding diff --git a/tests/test_settings_read_seam.py b/tests/test_settings_read_seam.py index 220c84241..a273c0085 100644 --- a/tests/test_settings_read_seam.py +++ b/tests/test_settings_read_seam.py @@ -43,6 +43,7 @@ from starlette.routing import Route from starlette.testclient import TestClient from tests._shared import SETTINGS_WRITERS, calls_function +from tests._governance_docs_shared import architecture_text, development_text # One owner-authored document, written entirely under keys a release renamed or # retired. Every value differs from both its legacy default and its current one, @@ -665,8 +666,7 @@ def test_a_retired_key_is_absent_from_every_surface_that_would_react_to_it(): from ouroboros import config as cfg from ouroboros.gateway import settings as settings_mod - documented = (pathlib.Path(__file__).resolve().parents[1] - / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8").splitlines() + documented = architecture_text().splitlines() for key in cfg.RETIRED_SETTING_KEYS: assert key not in settings_mod._IMMEDIATE_KEYS, key assert key not in settings_mod._RESTART_REQUIRED_KEYS, key diff --git a/tests/test_skill_widget_surface.py b/tests/test_skill_widget_surface.py index 386ced330..303b2ec3b 100644 --- a/tests/test_skill_widget_surface.py +++ b/tests/test_skill_widget_surface.py @@ -12,6 +12,7 @@ import pathlib import pytest from ouroboros.tools.registry import ToolContext +from tests._governance_docs_shared import architecture_text def _make_ctx(tmp_path: pathlib.Path) -> ToolContext: @@ -437,9 +438,7 @@ def test_save_enabled_row_is_disclosure_never_a_gate(tmp_path): def test_save_enabled_best_effort_disclosure_contract_is_documented(): from ouroboros.skill_loader import save_enabled - architecture = (pathlib.Path(__file__).resolve().parents[1] / "docs" / "ARCHITECTURE.md").read_text( - encoding="utf-8" - ) + architecture = architecture_text() assert "best-effort" in str(save_enabled.__doc__) assert "append failure is logged and never blocks the enablement change" in architecture diff --git a/tests/test_trust_metadata.py b/tests/test_trust_metadata.py index 1593f99c4..961c87011 100644 --- a/tests/test_trust_metadata.py +++ b/tests/test_trust_metadata.py @@ -5,6 +5,7 @@ import re from pathlib import Path import yaml +from tests._governance_docs_shared import architecture_text, development_text REPO = Path(__file__).resolve().parents[1] RELEASE_VERSION = "6.92.1" @@ -195,7 +196,7 @@ def test_benchmark_evidence_keeps_exact_scores_and_hugging_face_revisions(): def test_architecture_registers_each_new_public_metadata_surface(): - architecture = (REPO / "docs" / "ARCHITECTURE.md").read_text(encoding="utf-8") + architecture = architecture_text(REPO) for path in ( ".github/workflows/scorecard.yml",