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:
Kjell Tore Guttormsen 2026-07-03 06:48:13 +02:00
commit 46f2f521f7
4 changed files with 730 additions and 0 deletions

View file

@ -0,0 +1,112 @@
"""OKF bundle navigation and read-context rendering (method-spec §3 Step 1).
The read-context is built by NAVIGATING the bundle with progressive disclosure
never by stuffing the whole bundle (or keyword-retrieved chunks) into the prompt.
Navigation starts at ``index.md`` and follows its intra-bundle cross-links; broken
or bundle-escaping links are tolerated (skipped, never raised the OKF robustness
rule), while a missing ``index.md`` is an error (no entry point). ``type: verdict``
files are EXCLUDED from rendering: prior verdicts reach the hypothesis prompt ONLY
via the gated experience fold (see ``experience``), never via context rendering.
Pure stdlib by design the context seam imports no agent toolkit (§11).
"""
from __future__ import annotations
import re
from dataclasses import dataclass
from pathlib import Path
_INDEX_FILENAME = "index.md"
_VERDICT_TYPE = "verdict"
# (reference) the intra-bundle cross-link pattern, per §3 Step 1.
_CROSSLINK_PATTERN = re.compile(r"\]\(([^)]+\.md)\)")
@dataclass(frozen=True)
class ConceptFile:
"""One parsed OKF concept file: frontmatter (``type`` required) plus body."""
path: Path
frontmatter: dict[str, str]
body: str
@property
def type(self) -> str:
return self.frontmatter["type"]
@property
def title(self) -> str:
return self.frontmatter.get("title", self.path.stem)
def _strip_matching_quotes(value: str) -> str:
if len(value) >= 2 and value[0] == value[-1] and value[0] in {'"', "'"}:
return value[1:-1]
return value
def parse_concept_file(path: Path) -> ConceptFile:
"""Parse frontmatter (leading ``---`` block, line-oriented ``key: value``) + body.
The single required field is ``type``; unknown fields are preserved as strings.
"""
lines = path.read_text(encoding="utf-8").splitlines()
if not lines or lines[0].strip() != "---":
raise ValueError(f"{path.name}: missing frontmatter block")
frontmatter: dict[str, str] = {}
body_start = len(lines)
for i, line in enumerate(lines[1:], start=1):
if line.strip() == "---":
body_start = i + 1
break
key, sep, value = line.partition(":")
if sep:
frontmatter[key.strip()] = _strip_matching_quotes(value.strip())
if "type" not in frontmatter:
raise ValueError(f"{path.name}: frontmatter lacks the required field 'type'")
return ConceptFile(
path=path, frontmatter=frontmatter, body="\n".join(lines[body_start:]).strip()
)
def navigate_bundle(bundle_dir: Path) -> list[ConceptFile]:
"""Navigate from ``index.md`` — deterministic order: index first, links first-seen.
Targets containing a path separator are out-of-bundle and skipped; resolution is
boundary-checked against the bundle directory (fail-closed); broken links are
skipped, never raised. Repeated links are de-duplicated.
"""
index_path = bundle_dir / _INDEX_FILENAME
if not index_path.is_file():
raise FileNotFoundError(
f"bundle has no entry point: missing {_INDEX_FILENAME} in {bundle_dir}"
)
index = parse_concept_file(index_path)
bundle_root = bundle_dir.resolve()
concepts = [index]
seen = {_INDEX_FILENAME}
for target in _CROSSLINK_PATTERN.findall(index_path.read_text(encoding="utf-8")):
if "/" in target or "\\" in target or target in seen:
continue
seen.add(target)
resolved = (bundle_dir / target).resolve()
if not resolved.is_relative_to(bundle_root) or not resolved.is_file():
continue
concepts.append(parse_concept_file(resolved))
return concepts
def bundle_context(bundle_dir: Path) -> str:
"""Render the read-context: index body, then ``## {type}: {title}`` sections.
Empty sections are dropped. ``type: verdict`` files are excluded the verdict
layer must never leak into the read-context (§3 Step 1, load-bearing §11).
"""
index, *concepts = navigate_bundle(bundle_dir)
sections = [index.body] if index.body else []
for concept in concepts:
if concept.type == _VERDICT_TYPE or not concept.body:
continue
sections.append(f"## {concept.type}: {concept.title}\n\n{concept.body}")
return "\n\n".join(sections)