"""OKF navigation + read-context rendering (method-spec §3 Step 1, §11). Load-bearing seams proved here: - **Verdict-layer exclusion:** the realization signal must NEVER appear in the rendered read-context — prior verdicts reach the prompt ONLY via the gated experience fold. The test is RED if ``type: verdict`` files leak into rendering. - **Context-seam purity (import guard):** the navigation/context module imports no agent toolkit — and stays pure stdlib (D7-portable by design). """ from __future__ import annotations import ast import sys from pathlib import Path import pytest from portfolio_optimiser_claude.okf import ConceptFile, bundle_context, navigate_bundle BUNDLE = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro" SRC_PKG = Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser_claude" def _write(path: Path, text: str) -> None: path.write_text(text, encoding="utf-8") def _make_bundle(tmp_path: Path, index_body: str, files: dict[str, str]) -> Path: _write(tmp_path / "index.md", f"---\ntype: index\ntitle: Test\n---\n{index_body}\n") for name, text in files.items(): _write(tmp_path / name, text) return tmp_path class TestNavigation: """§3 Step 1: navigate from index.md — deterministic, tolerant, boundary-checked.""" def test_missing_index_is_an_error(self, tmp_path: Path) -> None: # A bundle has no entry point without index.md. with pytest.raises(FileNotFoundError): navigate_bundle(tmp_path) def test_order_is_index_first_then_first_seen_deduplicated(self, tmp_path: Path) -> None: bundle = _make_bundle( tmp_path, "See [b](b.md) then [a](a.md) then [b again](b.md).", { "a.md": "---\ntype: project\ntitle: A\n---\nBody A.", "b.md": "---\ntype: reference\ntitle: B\n---\nBody B.", }, ) names = [c.path.name for c in navigate_bundle(bundle)] assert names == ["index.md", "b.md", "a.md"] def test_frontmatterless_index_navigates(self, tmp_path: Path) -> None: # method-spec §3 Step 1: the index contributes "the index body (the summary)" # and is NOT a "non-index concept file" rendered as a `## {type}: {title}` # section — so a generated index carrying only the summary + links (ingest # spec §6 shape, frozen in the ingest golden) is valid. The index MAY omit # frontmatter; okf must navigate it via its body, never raise. _write(tmp_path / "index.md", "Summary line.\n- [A](a.md)\n") _write(tmp_path / "a.md", "---\ntype: project\ntitle: A\n---\nBody A.") concepts = navigate_bundle(tmp_path) assert [c.path.name for c in concepts] == ["index.md", "a.md"] index = concepts[0] assert index.type == "index" # default type, no KeyError for downstream .type reads assert index.body == "Summary line.\n- [A](a.md)" # body = the whole file # The summary flows into the read-context as the leading section. assert bundle_context(tmp_path).startswith("Summary line.") def test_frontmatterless_tolerance_is_scoped_to_the_index(self, tmp_path: Path) -> None: # LOAD-BEARING (honesty): the relaxation is for the index ENTRY POINT only. # A NON-index concept file without frontmatter is still malformed and MUST # raise — else the relaxation has silently weakened the `type`-required rule # for concept files (method-spec §3 Step 1). RED if the tolerance leaks. _write(tmp_path / "index.md", "Summary.\n- [A](a.md)\n") _write(tmp_path / "a.md", "no frontmatter here\n") with pytest.raises(ValueError, match="frontmatter"): navigate_bundle(tmp_path) def test_out_of_bundle_and_escaping_targets_are_skipped(self, tmp_path: Path) -> None: # A target containing a path separator is out-of-bundle — skipped, never # raised; ../-escapes never resolve outside the bundle (fail-closed). outside = tmp_path / "outside.md" _write(outside, "---\ntype: project\ntitle: Outside\n---\nSecret.") bundle_dir = tmp_path / "bundle" bundle_dir.mkdir() bundle = _make_bundle( bundle_dir, "Links: [up](../outside.md), [sub](sub/inner.md), [ok](a.md).", {"a.md": "---\ntype: project\ntitle: A\n---\nBody A."}, ) names = [c.path.name for c in navigate_bundle(bundle)] assert names == ["index.md", "a.md"] def test_broken_link_is_tolerated_never_raised(self, tmp_path: Path) -> None: bundle = _make_bundle( tmp_path, "Links: [gone](missing.md), [ok](a.md).", {"a.md": "---\ntype: project\ntitle: A\n---\nBody A."}, ) names = [c.path.name for c in navigate_bundle(bundle)] assert names == ["index.md", "a.md"] def test_shared_bundle_navigates_all_linked_concepts(self) -> None: # Integration on the shared example bundle: every index-linked file is # reached exactly once; the out-of-bundle ../../README.md link is skipped. names = [c.path.name for c in navigate_bundle(BUNDLE)] assert names[0] == "index.md" assert len(names) == len(set(names)) assert set(names) == { "index.md", "bygg-kontor-nord.md", "tiltak-led-retrofit.md", "metode-ipmvp-a.md", "kilder-realiseringsgap.md", "verdict-led-fro.md", } class TestFrontmatter: """§3 Step 1: leading ``---`` block, line-oriented ``key: value``; ``type`` required.""" def test_missing_type_is_an_error(self, tmp_path: Path) -> None: _write(tmp_path / "index.md", "---\ntitle: No type\n---\nBody.") with pytest.raises(ValueError, match="type"): navigate_bundle(tmp_path) def test_unknown_fields_preserved_and_quotes_stripped(self, tmp_path: Path) -> None: _write( tmp_path / "index.md", '---\ntype: index\ntitle: "Quoted title"\ncustom_field: kept\n---\nBody.', ) index = navigate_bundle(tmp_path)[0] assert isinstance(index, ConceptFile) assert index.type == "index" assert index.title == "Quoted title" assert index.frontmatter["custom_field"] == "kept" class TestRendering: """§3 Step 1: index body first, then ``## {type}: {title}`` sections.""" def test_sections_are_typed_and_titled_after_index_body(self, tmp_path: Path) -> None: bundle = _make_bundle( tmp_path, "The summary.\n\nSee [a](a.md).", {"a.md": "---\ntype: project\ntitle: Building A\n---\nProject body."}, ) context = bundle_context(bundle) assert context.startswith("The summary.") assert "## project: Building A" in context assert "Project body." in context def test_empty_sections_are_dropped(self, tmp_path: Path) -> None: bundle = _make_bundle( tmp_path, "Summary. See [empty](empty.md).", {"empty.md": "---\ntype: reference\ntitle: Empty\n---\n"}, ) context = bundle_context(bundle) assert "## reference: Empty" not in context class TestVerdictLayerExclusion: """LOAD-BEARING (§3 Step 1, §11): verdicts never leak via the read-context.""" def test_navigable_verdict_is_excluded_from_rendering(self, tmp_path: Path) -> None: # Control + seam in one: the verdict file IS navigable (so exclusion is # doing real work), yet its unique marker never reaches the read-context. # RED if rendering stops gating on ``type: verdict``. marker = "UNIQUE-REALIZATION-MARKER-0.82" bundle = _make_bundle( tmp_path, "Summary. See [v](v.md) and [a](a.md).", { "v.md": f"---\ntype: verdict\ntitle: Seed\n---\nSignal: {marker}.", "a.md": "---\ntype: project\ntitle: A\n---\nBody A.", }, ) navigated = [c.path.name for c in navigate_bundle(bundle)] assert "v.md" in navigated context = bundle_context(bundle) assert marker not in context assert "## verdict" not in context assert "## project: A" in context def test_shared_bundle_context_carries_no_realization_signal(self) -> None: # The seed verdict's learning signal (realization rate 0.82, expected # actual 24 600 NOK) must be absent from the rendered context — it may # reach the prompt ONLY via the gated experience fold. context = bundle_context(BUNDLE) assert "## verdict" not in context for leak in ("0.82", "0,82", "24600", "24 600", "realization_rate"): assert leak not in context # ...while the non-verdict concept layers ARE rendered: assert "## project:" in context assert "## hypothesis:" in context assert "## methodology:" in context assert "## reference:" in context def _imported_module_names(module_path: Path) -> set[str]: tree = ast.parse(module_path.read_text(encoding="utf-8")) names: set[str] = set() for node in ast.walk(tree): if isinstance(node, ast.Import): names.update(alias.name.split(".")[0] for alias in node.names) elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: names.add(node.module.split(".")[0]) return names class TestContextSeamPurity: """LOAD-BEARING (§11): the context seam imports no agent toolkit.""" @pytest.mark.parametrize("module", ["okf.py", "experience.py"]) def test_context_seam_never_imports_an_agent_toolkit(self, module: str) -> None: names = _imported_module_names(SRC_PKG / module) assert not names & {"claude_agent_sdk", "anthropic"} def test_okf_is_pure_stdlib(self) -> None: # D7-portable by design: navigation/rendering depends on nothing beyond # the standard library (relative imports would show as level > 0). names = _imported_module_names(SRC_PKG / "okf.py") assert names <= set(sys.stdlib_module_names)