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:
Kjell Tore Guttormsen 2026-06-29 10:56:48 +02:00
commit d6d83d42b5
9 changed files with 529 additions and 7 deletions

View file

@ -7,6 +7,8 @@ genuine two-arg ``extend_instructions(source_id, instructions)`` GA signature
Critical Fase 1 risk. Pattern: tests/spikes/test_d_verdictstore.py + real SessionContext.
"""
from pathlib import Path
import pytest
from agent_framework import SessionContext
@ -15,9 +17,13 @@ from portfolio_optimiser.verdicts import (
ProposalFeatures,
Verdict,
VerdictStore,
bundle_candidate_features,
capture_verdict,
seed_store_from_bundle,
)
_BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
_QUERY = ProposalFeatures(
affected_codes=frozenset({"05.2", "03.1"}),
measure_type="scope_reduction",
@ -106,3 +112,36 @@ async def test_before_run_populates_real_sessioncontext_two_arg() -> None:
ctx = SessionContext(input_messages=[], instructions=[])
await provider.before_run(agent=None, session=None, context=ctx, state={})
assert any(true_id in instr for instr in ctx.instructions)
# --- OKF-bundle seeding (Fase 2a): the pre-hypothesis ExpeL query key + the seed store ---
def test_bundle_candidate_features_keys_on_the_ir_projection() -> None:
"""The pre-hypothesis ExpeL query is the candidate measure's cost-IR features (from the
bundle's ``validator-input.json``) — available BEFORE any proposal is generated."""
features = bundle_candidate_features(str(_BUNDLE_DIR))
assert features.affected_codes == frozenset({"ENERGI-TOTAL-EL"})
assert "LED-retrofit" in features.measure_type
assert features.claimed_saving_nok == 30000
def test_seed_store_from_bundle_carries_the_realization_signal() -> None:
"""Each ``type: verdict`` file becomes a structurally-keyed ``Verdict`` whose rationale carries
the learning signal the validator cannot compute (the realization rate 0.82)."""
store = seed_store_from_bundle(str(_BUNDLE_DIR))
assert len(store.verdicts) == 1
seed = store.verdicts[0]
assert seed.proposal_features.affected_codes == frozenset({"ENERGI-TOTAL-EL"})
assert "approved" in seed.decision
assert "0.82" in seed.rationale
def test_seed_store_retrieval_matches_the_candidate() -> None:
"""A3: seed and query derive from the SAME IR -> similarity 1.0 -> the lone seed is retrieved
for the candidate (the structural match the Step-1 wiring relies on)."""
store = seed_store_from_bundle(str(_BUNDLE_DIR))
query = bundle_candidate_features(str(_BUNDLE_DIR))
hits = store.retrieve(query, k=3)
assert len(hits) == 1
assert "0.82" in hits[0].rationale