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
|
|
@ -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