feat(okf): conform the context seam to the pulled navigation + stamp-integrity contracts
The commons pull (7aa53fc -> a2b57d2) rewrote method-spec §3 Step 1 and added two §11 seams. Measuring okf.py against the new normative text found six contradictions; this closes all six, gated by the commons-owned nav-goldens that came with the pull. method-spec §3 Step 1 (navigate_bundle / bundle_context): - follow cross-links RECURSIVELY, depth-first in first-seen order (there was no recursion at all — only the root index's links were read, so no hierarchy was navigable even with the other fixes in place); - resolve a leading `/` against the BUNDLE ROOT, anything else against the LINKING file's directory, and drop the retired "a path separator means out-of-bundle" heuristic, which conflated depth with escape and forbade valid nesting; - de-duplicate on the RESOLVED path (`./a.md` == `a.md` == `/a.md`), which is also what terminates cycles; - exclude index files by BASENAME at every level, so a nested index is navigation and never renders as content (flat rendering regardless of depth); - bind index_summary to the ROOT index alone. safe_resolve stays the sole in-/out-of-bundle test, fail-closed: a target that fails to resolve for ANY reason is skipped, never raised. ingest-spec §3 (write_concept_file): it is the repo's one authoring primitive that materialises a concept file from caller-supplied frontmatter, so it now refuses the COMPLETE ownership stamp (`generated: true` + `ingest_manifest`) with IngestStampError, while permitting either field alone. A validation, never a repair — nothing is written. Gates (tests/test_okf.py, 529 -> 537): - nav-golden-hierarchy and nav-golden-escape compared against the shipped expected-read-context.md fasit (trailing-whitespace normalisation only, which the fixture README explicitly permits; internal blank-line structure stays gated); - traversal order pinned separately from the rendered output, so a right-looking render from a wrong walk still fails; - unit seams for the recursion in isolation, resolved-path dedup, and the leading-`/` rule's breach case (a real out-of-bundle file addressed by its absolute path). Load-bearing MEASURED, not asserted: seven mutations each go red — detach the recursion, restore the separator prefilter, dedup on the raw target, read `/` as filesystem-absolute, render nested index bodies as content, drop the stamp guard, and the fully naive navigator with no boundary check (which is what makes the `/`-trap test bite). okf.py restored from a checksum-verified copy after each. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WetWTHpdRbqinN5XHFTaTb
This commit is contained in:
parent
bb877d241f
commit
0d50ab89d3
3 changed files with 288 additions and 29 deletions
17
CLAUDE.md
17
CLAUDE.md
|
|
@ -36,6 +36,23 @@ Python ≥3.10. MAF (`agent-framework-core` 1.9.0). Pakkehåndtering: `uv`. To b
|
|||
fra denne konteksten: tidligere dommer når hypotese-prompten KUN via den gatede ExpeL-folden.
|
||||
Load-bearing: `test_bundle_context_excludes_verdict_layer` + empty-store-kontrollen i
|
||||
`test_step1_expel_loadbearing.py` (realiseringssignalet lekker aldri inn via kontekst).
|
||||
- **Navigasjons-kontrakten er hierarkisk, og escape — ikke dybde — er forbudt** (`method-spec`
|
||||
§3 Steg 1): `navigate_bundle` følger cross-links REKURSIVT, dybde-først i først-sett-rekkefølge;
|
||||
ledende `/` betyr **bundle-rot** (aldri filsystem-absolutt), alt annet er relativt til den
|
||||
LENKENDE filas katalog; dedup skjer på **resolvert** sti (så `./a.md` == `a.md`, og sykler
|
||||
termineres). `safe_resolve` er den ENESTE inn-/ut-av-bundle-testen (fail-closed) — den erstattet
|
||||
den pensjonerte «separator = utenfor bundelen»-heuristikken, som forvekslet dybde med escape.
|
||||
Manglende `index.md` er feil KUN i bundle-rota (navigasjon følger lenker, aldri katalog-enumerering).
|
||||
Rendering er FLAT uansett dybde; nestede `index.md` er navigasjon, ikke innhold. Gaten er
|
||||
commons-eide nav-goldens (`shared/examples/nav-golden-*/expected-read-context.md`, byte-nivå
|
||||
fasit): `test_nav_golden_hierarchy_*` (positiv) + `test_nav_golden_escape_*` (negativ — en gate
|
||||
som bare kan bli grønn beviser ingenting).
|
||||
- **Kuraterte skrivere kan ikke forfalske ingest-stempelet** (`ingest-spec` §3): `write_concept_file`
|
||||
er repoets ene authoring-primitiv som materialiserer en konseptfil fra CALLER-oppgitt frontmatter,
|
||||
og avviser derfor det KOMPLETTE eierskaps-stempelet (`generated: true` + `ingest_manifest`) med
|
||||
`IngestStampError` — mens hver halvdel alene er lovlig (kuratert innhold kan bære ett
|
||||
provenance-felt). Validering, ALDRI reparasjon: ingenting skrives. Uten dette kunne en kuratert fil
|
||||
bli stille slettet av en senere re-materialisering, som fjerner nøyaktig det som bærer stempelet.
|
||||
- **Stoppkriterier + budsjett-tak påkrevd ved oppstart** (fail-fast, aldri ubegrenset loop).
|
||||
- **Group Chat maker-checker** som debatt-default (IKKE Magentic, som er eksperimentell).
|
||||
- **To falsifiserere, samme kandidat (Steg 3/4, målbilde §2/§6):** den deterministiske validatoren
|
||||
|
|
|
|||
|
|
@ -1,19 +1,29 @@
|
|||
"""OKF (Open Knowledge Format) bundle navigation — framework-neutral, D7-portable context seam.
|
||||
|
||||
Reads a bundle the way OKF intends (progressive disclosure): start at ``index.md``, follow
|
||||
intra-bundle cross-links, parse each file's YAML frontmatter, classify by the one required
|
||||
``type`` field. **NO** ``agent_framework``, **NO** ``mcp`` — pure stdlib, so the SAME navigation
|
||||
serves both the MAF and the Claude-SDK implementations unchanged (målbilde §4 vendor-neutrality).
|
||||
intra-bundle cross-links **recursively, depth-first in first-seen link order**, parse each file's
|
||||
YAML frontmatter, classify by the one required ``type`` field. **NO** ``agent_framework``, **NO**
|
||||
``mcp`` — pure stdlib, so the SAME navigation serves both the MAF and the Claude-SDK
|
||||
implementations unchanged (målbilde §4 vendor-neutrality).
|
||||
|
||||
Link resolution follows ``shared/method-spec.md`` §3 Step 1: a leading ``/`` denotes the **bundle
|
||||
root** (NEVER a filesystem-absolute path), any other form is relative to the LINKING FILE's own
|
||||
directory — so a bundle may be a hierarchy. It is **escape, not depth**, that is forbidden; this
|
||||
resolve-and-boundary-check replaced the old "a path separator means out-of-bundle" heuristic, which
|
||||
conflated the two and forbade valid nesting. Repeated links de-duplicate on the RESOLVED path, so
|
||||
``./a.md`` and ``a.md`` are one entry and cycles terminate.
|
||||
|
||||
Robustness is part of the spec (OKF SPEC §4): consumers MUST tolerate broken links and unknown
|
||||
fields. A link to a missing file — or one escaping the bundle — is silently skipped, never raised.
|
||||
Path-safety reuses ``retrieval.safe_resolve`` (also pure stdlib): each cross-link is canonicalised
|
||||
and boundary-checked against the bundle dir, fail-closed.
|
||||
fields. A target that fails to resolve for ANY reason (missing file, invalid path component,
|
||||
escape) is silently skipped, never raised. Path-safety reuses ``retrieval.safe_resolve`` (also pure
|
||||
stdlib): each cross-link is canonicalised and boundary-checked against the bundle dir, fail-closed
|
||||
— the SOLE in-/out-of-bundle test.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import posixpath
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
|
@ -23,8 +33,8 @@ from portfolio_optimiser.retrieval import PathSecurityError, safe_resolve
|
|||
|
||||
_INDEX_NAME = "index.md"
|
||||
_IR_PROJECTION = "validator-input.json"
|
||||
# Intra-bundle markdown cross-links: ``](target.md)``. Targets with a path separator (``/``) are
|
||||
# treated as out-of-bundle and skipped (only same-dir bundle files are navigated).
|
||||
# Intra-bundle markdown cross-links: ``](target.md)``. A path separator is NOT a rejection reason —
|
||||
# ``_resolve_target`` decides in-/out-of-bundle, and only escape is refused (method-spec §3 Step 1).
|
||||
_LINK_RE = re.compile(r"\]\(([^)]+\.md)\)")
|
||||
|
||||
|
||||
|
|
@ -76,7 +86,8 @@ class Bundle:
|
|||
|
||||
@property
|
||||
def index_summary(self) -> str:
|
||||
"""The index body — the progressive-disclosure entry point (not whole-bundle stuffing)."""
|
||||
"""The ROOT index body — the progressive-disclosure entry point (not whole-bundle stuffing).
|
||||
Bound to the root alone: a nested ``a/index.md`` is navigation, never summary prose."""
|
||||
return next((f.body for f in self.files if f.name == _INDEX_NAME), "")
|
||||
|
||||
@property
|
||||
|
|
@ -88,8 +99,16 @@ class Bundle:
|
|||
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"]
|
||||
through the gated ExpeL fold, never by stuffing them into the read-context (målbilde §4).
|
||||
The exclusion is a TYPE CHECK on each reached file, applied at EVERY level — never a
|
||||
property of the link graph, so a mislabelled or injected navigation edge cannot smuggle a
|
||||
nested verdict in. Index files are dropped by BASENAME at every level too: a nested
|
||||
``a/index.md`` is navigation, not content."""
|
||||
return [
|
||||
f
|
||||
for f in self.files
|
||||
if posixpath.basename(f.name) != _INDEX_NAME and f.type != "verdict"
|
||||
]
|
||||
|
||||
@property
|
||||
def hypothesis(self) -> BundleFile | None:
|
||||
|
|
@ -98,8 +117,8 @@ class Bundle:
|
|||
|
||||
|
||||
def _load_file(bundle_dir: str, name: str) -> BundleFile | None:
|
||||
"""Resolve ``name`` within ``bundle_dir`` and read it, or ``None`` if missing / escaping the
|
||||
bundle (OKF §4 broken-link tolerance + fail-closed path-safety)."""
|
||||
"""Resolve the bundle-relative ``name`` within ``bundle_dir`` and read it, or ``None`` if
|
||||
missing / escaping the bundle (OKF §4 broken-link tolerance + fail-closed path-safety)."""
|
||||
try:
|
||||
resolved = Path(safe_resolve(bundle_dir, name))
|
||||
except PathSecurityError:
|
||||
|
|
@ -110,23 +129,60 @@ def _load_file(bundle_dir: str, name: str) -> BundleFile | None:
|
|||
return BundleFile(name=name, type=fm.get("type", ""), frontmatter=fm, body=_read_body(resolved))
|
||||
|
||||
|
||||
def _resolve_target(bundle_dir: str, from_name: str, target: str) -> tuple[str, str] | None:
|
||||
"""Resolve one cross-link into ``(bundle-relative posix name, canonical path)``, or ``None``
|
||||
when it fails to resolve for ANY reason (escape, invalid path component) — the caller skips,
|
||||
never raises (method-spec §3 Step 1).
|
||||
|
||||
A leading ``/`` denotes the BUNDLE ROOT, not the filesystem root: an implementation that let
|
||||
``os.path.join`` see an absolute target would either escape to the real filesystem path or
|
||||
refuse a legitimate root-relative link. Any other form resolves against the LINKING FILE's own
|
||||
directory, so an index links its immediate children one segment at a time."""
|
||||
if target.startswith("/"):
|
||||
rel = posixpath.normpath(target.lstrip("/"))
|
||||
else:
|
||||
rel = posixpath.normpath(posixpath.join(posixpath.dirname(from_name), target))
|
||||
try:
|
||||
return rel, safe_resolve(bundle_dir, rel)
|
||||
except PathSecurityError:
|
||||
return None
|
||||
|
||||
|
||||
def _walk(bundle_dir: str, current: BundleFile, files: list[BundleFile], seen: set[str]) -> None:
|
||||
"""Follow ``current``'s cross-links depth-first in first-seen order, appending each newly
|
||||
reached file and recursing into it. De-duplication is on the CANONICAL RESOLVED path (so
|
||||
``./a.md``, ``a.md`` and ``/a.md`` are one entry), which is also what terminates cycles."""
|
||||
for target in _LINK_RE.findall(current.body):
|
||||
resolved = _resolve_target(bundle_dir, current.name, target)
|
||||
if resolved is None:
|
||||
continue
|
||||
rel, canonical = resolved
|
||||
if canonical in seen:
|
||||
continue
|
||||
seen.add(canonical)
|
||||
linked = _load_file(bundle_dir, rel)
|
||||
if linked is None:
|
||||
continue # broken link: tolerated, never raised (OKF §4)
|
||||
files.append(linked)
|
||||
_walk(bundle_dir, linked, files, seen)
|
||||
|
||||
|
||||
def navigate_bundle(bundle_dir: str) -> Bundle:
|
||||
"""Navigate the OKF bundle from ``index.md``: parse the index, follow its intra-bundle ``.md``
|
||||
cross-links, and read each linked file's frontmatter + body. Deterministic: index first, then
|
||||
links in first-seen order, de-duplicated. Broken / escaping links are skipped (§4). Raises
|
||||
``ValueError`` only when ``index.md`` itself is unreadable (a bundle has no entry point)."""
|
||||
"""Navigate the OKF bundle from ``index.md``: parse the root index, then follow intra-bundle
|
||||
``.md`` cross-links RECURSIVELY, depth-first in first-seen link order, reading each reached
|
||||
file's frontmatter + body. Fully deterministic. Broken / escaping links are skipped (§4).
|
||||
|
||||
Navigation follows LINKS ONLY — a directory is never enumerated. Hence the missing-``index.md``
|
||||
error binds the bundle ROOT alone (a bundle has no entry point without it); an intermediate
|
||||
directory reached by a link needs no ``index.md`` of its own, and content nothing links to is
|
||||
simply unreachable, not an error. Raises ``ValueError`` only for the unreadable root index."""
|
||||
index = _load_file(bundle_dir, _INDEX_NAME)
|
||||
if index is None:
|
||||
raise ValueError(f"OKF bundle has no readable {_INDEX_NAME}: {bundle_dir!r}")
|
||||
files: list[BundleFile] = [index]
|
||||
seen = {_INDEX_NAME}
|
||||
for target in _LINK_RE.findall(index.body):
|
||||
if "/" in target or target in seen:
|
||||
continue # only same-dir bundle files; de-dup repeated links
|
||||
seen.add(target)
|
||||
linked = _load_file(bundle_dir, target)
|
||||
if linked is not None:
|
||||
files.append(linked)
|
||||
root = _resolve_target(bundle_dir, _INDEX_NAME, _INDEX_NAME)
|
||||
seen = {root[1]} if root is not None else set()
|
||||
_walk(bundle_dir, index, files, seen)
|
||||
return Bundle(dir=bundle_dir, files=tuple(files))
|
||||
|
||||
|
||||
|
|
@ -140,15 +196,23 @@ def bundle_context(bundle: Bundle, *, dimension: str | None = None) -> str:
|
|||
When ``dimension`` is given, only concept files whose frontmatter ``dimension`` matches — or that
|
||||
carry no ``dimension`` at all (un-scoped knowledge is never dropped) — are rendered; the default
|
||||
``dimension=None`` renders every concept file, byte-identical to the prior behavior. ``dimension``
|
||||
is a plain ``str`` (not the ``Dimension`` type) so ``okf`` stays MAF-free and import-cycle-free."""
|
||||
sections = [bundle.index_summary]
|
||||
is a plain ``str`` (not the ``Dimension`` type) so ``okf`` stays MAF-free and import-cycle-free.
|
||||
|
||||
Rendering is FLAT regardless of nesting depth (method-spec §3 Step 1): directory structure is
|
||||
navigation, not presentation, so a nested concept file renders as the same
|
||||
``## {type}: {title}`` section a root file would — there is no level heading, and nested index
|
||||
bodies do not appear at all. The serialisation (heading, blank line, body; sections separated by
|
||||
one blank line) is what the commons nav-golden ``expected-read-context.md`` fasit compares
|
||||
against — see ``tests/test_okf.py`` nav-golden gates."""
|
||||
sections = [bundle.index_summary.strip("\n")]
|
||||
for f in bundle.context_files:
|
||||
if dimension is not None:
|
||||
file_dim = f.frontmatter.get("dimension")
|
||||
if file_dim is not None and file_dim != dimension:
|
||||
continue
|
||||
title = f.frontmatter.get("title", f.name).strip('"')
|
||||
sections.append(f"## {f.type or 'document'}: {title}\n{f.body}")
|
||||
body = f.body.strip("\n")
|
||||
sections.append(f"## {f.type or 'document'}: {title}\n\n{body}")
|
||||
return "\n\n".join(s for s in sections if s.strip())
|
||||
|
||||
|
||||
|
|
@ -163,11 +227,42 @@ def render_frontmatter(frontmatter: dict[str, str]) -> str:
|
|||
return "\n".join(f"{key}: {' '.join(str(value).split())}" for key, value in frontmatter.items())
|
||||
|
||||
|
||||
class IngestStampError(ValueError):
|
||||
"""A curated writer was handed frontmatter carrying the COMPLETE ingest ownership stamp
|
||||
(``shared/ingest-spec.md`` §3). The stamp is the sole mark separating ingest-owned files from
|
||||
curated ones, and re-materialization replaces exactly what carries it — so a curated file that
|
||||
forged it could be silently deleted by a later ingest run."""
|
||||
|
||||
|
||||
def _carries_complete_ingest_stamp(frontmatter: dict[str, str]) -> bool:
|
||||
"""Whether ``frontmatter`` carries BOTH halves of the ingest ownership stamp: ``generated:
|
||||
true`` together with a non-empty ``ingest_manifest`` reference (ingest-spec §7).
|
||||
|
||||
The test is on the COMPLETE stamp, never on the individual field names — curated content may
|
||||
legitimately carry a single provenance field, and a verbatim round-trip of one half must keep
|
||||
working. Values are compared the way ``parse_frontmatter`` yields them (line-oriented strings,
|
||||
quotes retained), so surrounding quotes and case are normalised away here."""
|
||||
generated = str(frontmatter.get("generated", "")).strip().strip('"').lower()
|
||||
manifest = str(frontmatter.get("ingest_manifest", "")).strip().strip('"')
|
||||
return generated == "true" and bool(manifest)
|
||||
|
||||
|
||||
def write_concept_file(bundle_dir: str, name: str, frontmatter: dict[str, str], body: str) -> Path:
|
||||
"""Write a typed OKF concept file (``---`` frontmatter + markdown body) into ``bundle_dir``,
|
||||
path-safe via ``safe_resolve`` (fail-closed: a ``name`` escaping the bundle raises
|
||||
``PathSecurityError``). Pure stdlib — the D7-portable counterpart of ``navigate_bundle``'s read.
|
||||
|
||||
This is the repo's one authoring primitive that materialises a concept file from CALLER-SUPPLIED
|
||||
frontmatter, so it is the surface ingest-spec §3's "no other writer may forge the stamp" binds:
|
||||
a frontmatter carrying the complete ingest stamp raises ``IngestStampError`` and NOTHING is
|
||||
written. A validation, never a repair — the caller is told, not silently corrected.
|
||||
Returns the written path."""
|
||||
if _carries_complete_ingest_stamp(frontmatter):
|
||||
raise IngestStampError(
|
||||
"refusing to write a curated concept file carrying the COMPLETE ingest ownership stamp "
|
||||
"(generated: true + ingest_manifest); only the ingest materializer may claim it "
|
||||
"(ingest-spec §3) — either field alone is permitted"
|
||||
)
|
||||
resolved = Path(safe_resolve(bundle_dir, name))
|
||||
resolved.parent.mkdir(parents=True, exist_ok=True)
|
||||
resolved.write_text(f"---\n{render_frontmatter(frontmatter)}\n---\n\n{body}", encoding="utf-8")
|
||||
|
|
|
|||
|
|
@ -29,7 +29,26 @@ _MAF_FREE_MODULES = [
|
|||
"semretrieval.py",
|
||||
]
|
||||
|
||||
BUNDLE_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples" / "bygg-energi-mikro"
|
||||
_EXAMPLES_DIR = Path(__file__).resolve().parents[1] / "shared" / "examples"
|
||||
BUNDLE_DIR = _EXAMPLES_DIR / "bygg-energi-mikro"
|
||||
|
||||
|
||||
def _normalise_trailing_ws(text: str) -> str:
|
||||
"""The ONLY normalisation the nav-golden gate applies, and the README explicitly permits it
|
||||
("byte-exact or after trailing-whitespace normalization"): per-line trailing whitespace and the
|
||||
file's terminating newline. Internal blank-line structure is NOT normalised — the serialisation
|
||||
shape (``## {type}: {title}`` + blank line + body, sections separated by one blank line) stays
|
||||
gated, so a renderer that drops or doubles a separator still goes RED."""
|
||||
return "\n".join(line.rstrip() for line in text.rstrip("\n").split("\n"))
|
||||
|
||||
|
||||
def _nav_golden(case: str) -> tuple[str, str]:
|
||||
"""A commons-owned nav-golden case: ``(bundle_dir, expected_read_context)``. The fixture class
|
||||
is bundle-in / read-context-out — the only shape that can express method-spec §3 Step 1's
|
||||
cross-implementation property "two conformant implementations MUST produce an identical
|
||||
read-context from the same bundle"."""
|
||||
root = _EXAMPLES_DIR / case
|
||||
return str(root / "bundle"), (root / "expected-read-context.md").read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_navigate_classifies_every_typed_file() -> None:
|
||||
|
|
@ -84,6 +103,106 @@ def test_bundle_context_excludes_verdict_layer() -> None:
|
|||
assert "progressiv disclosure" in context.lower() # the index summary is the entry point
|
||||
|
||||
|
||||
def test_nav_golden_hierarchy_read_context_matches_commons_fasit() -> None:
|
||||
"""CONFORMANCE GATE (method-spec §3 Step 1, commons-owned nav-golden). The positive case: the
|
||||
rendered read-context of a HIERARCHICAL bundle must equal the shipped ``expected-read-context.md``
|
||||
— the cross-implementation fasit. One assertion covers the whole Q3 navigation contract at once:
|
||||
recursive depth-first traversal, both link forms, resolved-path dedup, cycle termination, the
|
||||
root-only binding of the missing-index rule, recursive verdict exclusion, and FLAT rendering
|
||||
(nested index bodies are navigation, never content)."""
|
||||
bundle_dir, expected = _nav_golden("nav-golden-hierarchy")
|
||||
context = okf.bundle_context(okf.navigate_bundle(bundle_dir))
|
||||
assert _normalise_trailing_ws(context) == _normalise_trailing_ws(expected)
|
||||
|
||||
|
||||
def test_nav_golden_hierarchy_traversal_is_depth_first_first_seen() -> None:
|
||||
"""The fasit above pins the RENDERED output; this pins the TRAVERSAL that produced it, so a
|
||||
renderer that accidentally reproduced the right text from a wrong walk still goes RED. Order is
|
||||
the commons README's trace verbatim: root index -> overview -> /a/index -> a/doc-a ->
|
||||
a/verdict-nested (REACHED, then excluded by type) -> a/b/index -> a/b/doc-b, with ``/a/index.md``
|
||||
and ``/overview.md`` deduped on their RESOLVED paths (cycle termination). ``c/orphan.md`` is
|
||||
unreachable — a link-following navigator never sees it, a directory-walking one wrongly would,
|
||||
and ``c/`` having no ``index.md`` is NOT an error (that rule binds the bundle root alone)."""
|
||||
bundle_dir, _ = _nav_golden("nav-golden-hierarchy")
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
assert [f.name for f in bundle.files] == [
|
||||
"index.md",
|
||||
"overview.md",
|
||||
"a/index.md",
|
||||
"a/doc-a.md",
|
||||
"a/verdict-nested.md",
|
||||
"a/b/index.md",
|
||||
"a/b/doc-b.md",
|
||||
]
|
||||
# Verdict exclusion is a TYPE check on each reached file, applied recursively — never a property
|
||||
# of the link graph: the nested verdict IS navigated, and IS kept out of the context.
|
||||
assert [f.name for f in bundle.verdicts] == ["a/verdict-nested.md"]
|
||||
assert [f.name for f in bundle.context_files] == ["overview.md", "a/doc-a.md", "a/b/doc-b.md"]
|
||||
|
||||
|
||||
def test_nav_golden_escape_read_context_matches_commons_fasit() -> None:
|
||||
"""CONFORMANCE GATE, negative case (a gate that can only pass proves nothing). Every link but
|
||||
the first escapes the bundle; a conformant navigator skips them all, reads none of them, RAISES
|
||||
NOTHING, and still returns the one valid sibling. The decoy really exists one level up."""
|
||||
bundle_dir, expected = _nav_golden("nav-golden-escape")
|
||||
bundle = okf.navigate_bundle(bundle_dir)
|
||||
assert [f.name for f in bundle.files] == ["index.md", "valid.md"]
|
||||
context = okf.bundle_context(bundle)
|
||||
assert _normalise_trailing_ws(context) == _normalise_trailing_ws(expected)
|
||||
# The decoy's CONTENT, not its filename: the name legitimately appears in the index body as the
|
||||
# (skipped) link target, so asserting on the name would pass for a navigator that read the file.
|
||||
decoy = (_EXAMPLES_DIR / "nav-golden-escape" / "SHOULD-NOT-BE-READ.md").read_text(
|
||||
encoding="utf-8"
|
||||
)
|
||||
assert decoy.split("---")[-1].strip() not in context
|
||||
|
||||
|
||||
def test_navigate_follows_links_recursively(tmp_path) -> None:
|
||||
"""§11 "Navigation boundary", the RECURSION on its own: a pure same-directory chain
|
||||
``index -> a.md -> b.md``. Deliberately flat, so it is isolated from link-form and boundary
|
||||
concerns — a navigator that only reads the ROOT index's links returns ``[index, a]`` and goes
|
||||
RED here even though every target is a legal same-dir sibling."""
|
||||
(tmp_path / "index.md").write_text("---\ntype: index\n---\n\n- [A](a.md)\n", encoding="utf-8")
|
||||
(tmp_path / "a.md").write_text(
|
||||
"---\ntype: project\n---\n\nA body\n\n- [B](b.md)\n", encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "b.md").write_text("---\ntype: reference\n---\n\nB body\n", encoding="utf-8")
|
||||
bundle = okf.navigate_bundle(str(tmp_path))
|
||||
assert [f.name for f in bundle.files] == ["index.md", "a.md", "b.md"]
|
||||
|
||||
|
||||
def test_navigate_dedups_on_resolved_path(tmp_path) -> None:
|
||||
"""Dedup is on the RESOLVED path, not the raw target string: ``./a.md``, ``a.md`` and ``/a.md``
|
||||
are one entry. A navigator de-duplicating raw targets reads (and renders) the same file three
|
||||
times and goes RED."""
|
||||
(tmp_path / "index.md").write_text(
|
||||
"---\ntype: index\n---\n\n- [1](a.md)\n- [2](./a.md)\n- [3](/a.md)\n", encoding="utf-8"
|
||||
)
|
||||
(tmp_path / "a.md").write_text("---\ntype: project\n---\n\nA body\n", encoding="utf-8")
|
||||
bundle = okf.navigate_bundle(str(tmp_path))
|
||||
assert [f.name for f in bundle.files] == ["index.md", "a.md"]
|
||||
|
||||
|
||||
def test_navigate_root_relative_link_is_bundle_root_not_filesystem(tmp_path) -> None:
|
||||
"""§11 "Navigation boundary", the ratified leading-``/`` rule and its breach. A leading ``/``
|
||||
denotes the BUNDLE ROOT, never a filesystem-absolute path — so an index carrying the real
|
||||
absolute path of a file OUTSIDE the bundle resolves to ``{bundle}/{that path}`` (missing ->
|
||||
skipped), and the outside file is never opened. An implementation reading ``/`` as
|
||||
filesystem-absolute exfiltrates it: that is the path-traversal breach the escape golden's
|
||||
``/etc/passwd`` trap describes, made assertable here with a file this test owns."""
|
||||
outside = tmp_path / "outside.md"
|
||||
outside.write_text("---\ntype: secret\n---\n\nEXFIL-SENTINEL\n", encoding="utf-8")
|
||||
bundle_dir = tmp_path / "bundle"
|
||||
bundle_dir.mkdir()
|
||||
(bundle_dir / "index.md").write_text(
|
||||
f"---\ntype: index\n---\n\n- [trap]({outside.resolve().as_posix()})\n", encoding="utf-8"
|
||||
)
|
||||
bundle = okf.navigate_bundle(str(bundle_dir))
|
||||
assert [f.name for f in bundle.files] == ["index.md"] # resolved under the bundle -> missing
|
||||
assert all("EXFIL-SENTINEL" not in f.body for f in bundle.files)
|
||||
assert "EXFIL-SENTINEL" not in okf.bundle_context(bundle)
|
||||
|
||||
|
||||
def _dimension_bundle(tmp_path) -> str:
|
||||
(tmp_path / "index.md").write_text(
|
||||
"---\ntype: index\n---\n\n# Bundle\n\n"
|
||||
|
|
@ -227,6 +346,34 @@ def test_render_frontmatter_single_lines_scalars(tmp_path) -> None:
|
|||
assert parsed["decision"] == "approved" # the trailing key was NOT lost to a spurious ---
|
||||
|
||||
|
||||
def test_write_concept_file_refuses_complete_ingest_stamp(tmp_path) -> None:
|
||||
"""§11 "Stamp integrity (curated writers)" (ingest-spec §3, third bullet). The ingest stamp is
|
||||
the SOLE mark separating ingest-owned files from curated ones — and re-materialization deletes
|
||||
what carries it. ``write_concept_file`` materialises a concept file from CALLER-SUPPLIED
|
||||
frontmatter, so it is exactly the authoring primitive that could forge the stamp; it MUST refuse
|
||||
the COMPLETE stamp and write NOTHING. A validation, never a repair: the file must not appear
|
||||
stamp-stripped either."""
|
||||
fm = {"type": "reference", "generated": "true", "ingest_manifest": "bygg@0123456789abcdef"}
|
||||
with pytest.raises(okf.IngestStampError):
|
||||
okf.write_concept_file(str(tmp_path), "forged.md", fm, "body\n")
|
||||
assert not (tmp_path / "forged.md").exists() # refused, not silently repaired
|
||||
|
||||
|
||||
def test_write_concept_file_permits_either_stamp_field_alone(tmp_path) -> None:
|
||||
"""The check is on the COMPLETE stamp, never on the individual field names: curated content may
|
||||
legitimately carry a single provenance field, and a genuine verbatim round-trip is preserved.
|
||||
Both halves alone are written unchanged."""
|
||||
okf.write_concept_file(str(tmp_path), "a.md", {"type": "reference", "generated": "true"}, "b\n")
|
||||
okf.write_concept_file(
|
||||
str(tmp_path),
|
||||
"b.md",
|
||||
{"type": "reference", "ingest_manifest": "bygg@0123456789abcdef"},
|
||||
"b\n",
|
||||
)
|
||||
assert okf.parse_frontmatter(tmp_path / "a.md")["generated"] == "true"
|
||||
assert okf.parse_frontmatter(tmp_path / "b.md")["ingest_manifest"] == "bygg@0123456789abcdef"
|
||||
|
||||
|
||||
def _minimal_bundle(tmp_path) -> str:
|
||||
(tmp_path / "index.md").write_text(
|
||||
"---\ntype: index\n---\n\n# Bundle\n\n- [proj](bygg.md)\n", encoding="utf-8"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue