"""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" _INDEX_TYPE = "index" _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 _parse_index_entry(index_path: Path) -> ConceptFile: """Parse the bundle entry point, tolerating a frontmatter-less index (§3 Step 1). method-spec §3 Step 1 renders the index as "the index body (the summary)" and treats every OTHER file as a "non-index concept file" (`## {type}: {title}` section) — so the index is NOT a concept file, and it MAY omit frontmatter: a generated index carrying only ``bundle_summary`` + cross-links (ingest spec §6, frozen in the ingest golden) is valid. A frontmatter-ful index (the curated convention, ``type: index``) still parses normally, its extra fields preserved. When frontmatter is absent, the whole file is the body and ``type`` defaults to ``index`` so downstream ``.type`` reads never raise. """ text = index_path.read_text(encoding="utf-8") lines = text.splitlines() if lines and lines[0].strip() == "---": return parse_concept_file(index_path) return ConceptFile(path=index_path, frontmatter={"type": _INDEX_TYPE}, body=text.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. The index entry point may omit frontmatter (``_parse_index_entry``); non-index concept files still require ``type``. """ 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_index_entry(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) try: resolved = (bundle_dir / target).resolve() if not resolved.is_relative_to(bundle_root) or not resolved.is_file(): continue except ValueError: # An unrepresentable target (e.g. an embedded NUL byte, which carries no # path separator and so slips past the out-of-bundle filter) makes # ``resolve``/``is_file`` raise ``ValueError``. That is a broken link, not # a fatal error — skip it, never raise (method-spec §72, OKF robustness). 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)