feat(fase2b): OKF-navigated bundle context replaces chunk-stuffing

Closes the honest Fase 2a limitation: docs_dir==bundle_dir let keyword
chunk-stuffing leak the verdict's realization rate ("0.82") into the debate /
generation prompt regardless of the ExpeL fold (it surfaced from both
verdict-led-fro.md AND golden.json). The realization signal now reaches the
hypothesis prompt ONLY via the gated ExpeL fold.

- okf.py: bundle_context() + Bundle.context_files render the navigated bundle
  (index + frontmatter + cross-links) as the agent read-context, EXCLUDING
  type: verdict (maalbilde §2/§4). Pure stdlib, still MAF-free.
- datasource.py: bundle_citations() derives first-class citations from the
  navigated non-verdict files.
- run_project: on the bundle path context + citations + debate tools come from
  navigation (tools=[]; navigation replaces query-time RAG); the road path keeps
  chunk-stuffing unchanged.

Load-bearing (maalbilde §7): the marker is upgraded from the minted verdict id
to the realization signal itself. The empty-store control now asserts "0.82"
reaches NO prompt — RED against the pre-2b chunk-stuffing path, green after
navigation (TDD red->green). New okf-level test_bundle_context_excludes_verdict_layer
guards the seam directly.

Suite 133->134 passed, 4 skipped; mypy + ruff check clean. Reverted unrelated
ruff-format drift (backends/budget/verdicts/test_contracts) to keep the diff
surgical.

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-30 06:42:19 +02:00
commit 8814a698c2
7 changed files with 117 additions and 29 deletions

View file

@ -84,6 +84,13 @@ class Bundle:
"""Every ``type: verdict`` file (the ExpeL seeds the Step-1 wiring retrieves)."""
return [f for f in self.files if f.type == "verdict"]
@property
def context_files(self) -> list[BundleFile]:
"""The non-index, non-``verdict`` concept files — the bodies that form the agent context.
The verdict layer is deliberately EXCLUDED: prior verdicts reach the hypothesis prompt only
through the gated ExpeL fold, never by stuffing them into the read-context (målbilde §4)."""
return [f for f in self.files if f.name != _INDEX_NAME and f.type != "verdict"]
@property
def hypothesis(self) -> BundleFile | None:
"""The candidate ``type: hypothesis`` file, if present."""
@ -123,6 +130,19 @@ def navigate_bundle(bundle_dir: str) -> Bundle:
return Bundle(dir=bundle_dir, files=tuple(files))
def bundle_context(bundle: Bundle) -> str:
"""Render a navigated bundle as agent read-context via progressive disclosure: the ``index.md``
summary, then each concept file as ``## {type}: {title}\\n{body}``. ``type: verdict`` files are
EXCLUDED (målbilde §2 step 1 / §4: navigation, not chunk-stuffing the verdict layer folds in
only via the gated ExpeL retrieval). Deterministic: index first, then context files in
navigation order; empty sections are dropped."""
sections = [bundle.index_summary]
for f in bundle.context_files:
title = f.frontmatter.get("title", f.name).strip('"')
sections.append(f"## {f.type or 'document'}: {title}\n{f.body}")
return "\n\n".join(s for s in sections if s.strip())
def load_ir_projection(bundle_dir: str, name: str = _IR_PROJECTION) -> dict[str, Any]:
"""Load the bundle's IR projection (``validator-input.json`` by default): the candidate
measure's cost-IR (``measure``, ``affected_items``, ``claimed_saving_nok``) — the