feat(context): S7 — D7 context seam: OKF navigation + gated ExpeL fold
Step 1 of the loop, built from method-spec §3 alone: - okf.py (pure stdlib): frontmatter parse, deterministic/tolerant/boundary-checked index navigation, bundle_context rendering with type:verdict exclusion - experience.py: CandidateFeatures from the IR projection, §4.2 id minting, structural ranking (0.60·Jaccard + 0.25·type + 0.15·magnitude bucket), first-write-wins store, bundle seeding with the realization marker, fold-before-generation (empty retrieval → base unchanged) Load-bearing (§11), each proved RED on detach: verdict-layer exclusion, fold, seed learning fields, agent-toolkit import guard. 76/76 without an API key; ruff + mypy --strict clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QdSfQdND84oeq2mbjueLTS
This commit is contained in:
parent
1e1b7e4506
commit
46f2f521f7
4 changed files with 730 additions and 0 deletions
202
tests/test_okf.py
Normal file
202
tests/test_okf.py
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
"""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_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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue