feat(fase2): wire Step-1 ExpeL retrieval into the hypothesis prompt
Closes maalbilde §5 gap #1 (the one missing "feedback-into-prompt" dataflow) for the OKF-bundle path. Before, ExpeL was computed AFTER generation into a discarded SessionContext, so a prior verdict could not influence any hypothesis (context_providers=0). - New okf.py: framework-neutral OKF bundle navigation (index + frontmatter + cross-links), pure stdlib, no agent_framework/mcp (D7-portable), enforced by test_okf_is_maf_free. - verdicts.py: seed_store_from_bundle + bundle_candidate_features build the ExpeL substrate + the pre-hypothesis query key from a bundle. - run_project(bundle_dir=...): folds the candidate's prior verdicts into the generation context BEFORE generate_via_llm; the road path is unchanged. Load-bearing (maalbilde §7): test_step1_expel_loadbearing proves a prior verdict reaches the hypothesis prompt and goes RED when the fold is detached (shown via TDD red->green). The marker is the minted verdict id (content hash) because docs_dir==bundle_dir lets keyword chunk-stuffing leak the realization rate; clean layer separation is Fase 2b. Suite 121->133 passed; mypy + ruff check clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MHR8iKxJRxDiDfNw8HZmWE
This commit is contained in:
parent
2b1a1832ef
commit
d6d83d42b5
9 changed files with 529 additions and 7 deletions
96
tests/test_okf.py
Normal file
96
tests/test_okf.py
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
"""OKF bundle navigation (okf.py) — the framework-neutral, D7-portable context seam.
|
||||
|
||||
These tests pin the minimal navigation the Step-1 ExpeL wiring depends on: read ``index.md``,
|
||||
follow intra-bundle cross-links, parse each file's frontmatter, classify by ``type``, and locate
|
||||
the candidate IR projection. Robustness (OKF SPEC §4): broken links are tolerated, never raised.
|
||||
|
||||
No ``agent_framework``/``mcp`` import is allowed in ``okf`` — guarded by ``test_okf_is_maf_free``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from portfolio_optimiser import okf
|
||||
|
||||
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
|
||||
|
||||
|
||||
def test_navigate_classifies_every_typed_file() -> None:
|
||||
"""The energi bundle resolves to its six typed OKF files (index + project + hypothesis +
|
||||
methodology + reference + verdict), each carrying its declared ``type``."""
|
||||
bundle = okf.navigate_bundle(str(BUNDLE_DIR))
|
||||
types = {f.name: f.type for f in bundle.files}
|
||||
assert types == {
|
||||
"index.md": "index",
|
||||
"bygg-kontor-nord.md": "project",
|
||||
"tiltak-led-retrofit.md": "hypothesis",
|
||||
"metode-ipmvp-a.md": "methodology",
|
||||
"kilder-realiseringsgap.md": "reference",
|
||||
"verdict-led-fro.md": "verdict",
|
||||
}
|
||||
|
||||
|
||||
def test_navigate_exposes_verdicts_and_hypothesis() -> None:
|
||||
"""The navigation surfaces exactly one ``type: verdict`` file (the ExpeL seed) and the
|
||||
candidate hypothesis — the two the Step-1 wiring keys on."""
|
||||
bundle = okf.navigate_bundle(str(BUNDLE_DIR))
|
||||
assert [f.name for f in bundle.verdicts] == ["verdict-led-fro.md"]
|
||||
assert bundle.verdicts[0].frontmatter["realization_rate"] == "0.82"
|
||||
assert bundle.hypothesis is not None
|
||||
assert bundle.hypothesis.name == "tiltak-led-retrofit.md"
|
||||
|
||||
|
||||
def test_navigate_index_summary_is_progressive_disclosure() -> None:
|
||||
"""``index_summary`` is the index body (the progressive-disclosure entry point), not stuffing
|
||||
the whole bundle."""
|
||||
bundle = okf.navigate_bundle(str(BUNDLE_DIR))
|
||||
assert "progressiv disclosure" in bundle.index_summary.lower()
|
||||
|
||||
|
||||
def test_navigate_tolerates_broken_links(tmp_path) -> None:
|
||||
"""OKF SPEC §4: a consumer MUST tolerate broken links. An index linking a missing file
|
||||
navigates without raising, simply omitting the absent target."""
|
||||
(tmp_path / "index.md").write_text(
|
||||
"---\ntype: index\n---\n\nSee [gone](missing.md) and [here](real.md).\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "real.md").write_text("---\ntype: project\n---\n\nbody\n", encoding="utf-8")
|
||||
bundle = okf.navigate_bundle(str(tmp_path))
|
||||
names = {f.name for f in bundle.files}
|
||||
assert names == {"index.md", "real.md"} # missing.md silently skipped, no raise
|
||||
|
||||
|
||||
def test_load_ir_projection_returns_candidate_ir() -> None:
|
||||
"""The bundle's IR projection (``validator-input.json``) is the candidate measure's cost-IR —
|
||||
the pre-hypothesis ExpeL query key source."""
|
||||
ir = okf.load_ir_projection(str(BUNDLE_DIR))
|
||||
assert ir["project_id"] == "BYGG-KONTOR-NORD"
|
||||
assert [a["code"] for a in ir["affected_items"]] == ["ENERGI-TOTAL-EL"]
|
||||
assert ir["claimed_saving_nok"] == 30000
|
||||
|
||||
|
||||
def test_parse_frontmatter_reads_scalar_fields() -> None:
|
||||
"""The minimal frontmatter reader returns the leading ``---`` block as key:value strings."""
|
||||
fm = okf.parse_frontmatter(BUNDLE_DIR / "verdict-led-fro.md")
|
||||
assert fm["type"] == "verdict"
|
||||
assert fm["decision"] == "approved_with_adjustment"
|
||||
|
||||
|
||||
def test_okf_is_maf_free() -> None:
|
||||
"""D7 portability: ``okf.py`` IMPORTS no ``agent_framework`` / ``mcp`` (the docstring may name
|
||||
them to document the constraint, exactly as ``retrieval.py`` does) — checked via the AST, not a
|
||||
raw substring, so the prose claim doesn't trip the guard."""
|
||||
import ast
|
||||
|
||||
src = (
|
||||
Path(__file__).resolve().parents[1] / "src" / "portfolio_optimiser" / "okf.py"
|
||||
).read_text(encoding="utf-8")
|
||||
imported: list[str] = []
|
||||
for node in ast.walk(ast.parse(src)):
|
||||
if isinstance(node, ast.Import):
|
||||
imported += [a.name for a in node.names]
|
||||
elif isinstance(node, ast.ImportFrom):
|
||||
imported.append(node.module or "")
|
||||
forbidden = [m for m in imported if m.split(".")[0] in {"agent_framework", "mcp"}]
|
||||
assert forbidden == [], f"okf.py must not import MAF/mcp, found: {forbidden}"
|
||||
Loading…
Add table
Add a link
Reference in a new issue