Placement settled by catalog's own reading of upstream at the pinned commit
3fcbb9f: SS8:509-510 and SS12:773-775 both put `okf_version` in a bundle-root
`index.md` frontmatter block, and SS12 calls it the only place frontmatter is
permitted in an index. Catalog's spec says the opposite about the same file;
that divergence is theirs against upstream, and we conform to upstream.
The value never touches a profile. `OKF_V0_2.index.root_frontmatter` names the
key; the caller supplies the value through a new keyword-only
`root_frontmatter_values` mapping. That keeps V4/V-A5 intact - `okf_version`'s
value tracks the upstream Google version and belongs to catalog (E1), so a
constant here would claim a decision we do not own and would have to be chased
on every upstream release. In the fixture the value is fixture DATA
(`okf-version.txt`), not a literal in our source.
Ordering comes from the policy, not the caller's mapping: a dict preserves
insertion order, so two callers passing the same keys would otherwise emit
different bytes. A key the policy does not name is refused fail-fast, before
any disk mutation. Omitting the argument emits no block at all - SS12 is a MAY
and none of upstream's four reference bundles declares the key.
The block is written only when the index is CREATED, so a re-run into an
existing bundle stays byte-identical (A-E5).
Raw-byte assertions rather than parsed ones, on the committed fixture as well
as on fresh runs: catalog measured that a quoted value fails their shape regex
with exit 1 and that a BOM hides the marker while still exiting 0.
`yaml.safe_load` returns "0.2" either way and strips a BOM first, so a parsed
assertion masks exactly those two defects. Asserting the frozen fixture catches
what a self-comparison cannot - regenerating from a broken emitter moves both
sides together.
A-E6 is now placement-explicit (promised catalog in 99cf987), and separates the
two byte properties: BOM-free is a property of the file, unquoted is a property
of CATALOG'S GATE and not of OKF v0.2 - upstream's own SS12 example is quoted,
so their gate rejects the spec's canonical form.
README gains the upstream-version section it was missing; CLAUDE.md gains the
mechanism behind "no profile hard-codes an upstream version": a profile names a
key, a caller owns its value.
550 -> 559 tests. test_profile_threading's `OKF_V0_2.index is DEFAULT.index`
assertion is replaced rather than deleted: object identity was a proxy for "the
shipped profiles differ in no NAME-bearing field", which is what makes the
synthetic test profile necessary, so the guard now asserts that directly.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013dgkSPjkLpACjMayd9R5jx
227 lines
10 KiB
Python
227 lines
10 KiB
Python
"""Requirement 6 — `profile` threaded through `materialize_bundle`'s disk phase.
|
|
|
|
`OKF_V0_2` landed in D2, but nothing outside the library could ask for it: the
|
|
content phase already accepted `profile`, while `materialize_bundle` neither
|
|
took one nor passed one down. The parameter is keyword-only behind the `*` the
|
|
signature already carried, so every three-positional call site stays
|
|
source-compatible — which is what the consumer who asked for it needs.
|
|
|
|
Two of these tests would be untestable by construction if written only against
|
|
the shipped profiles: `OKF_V0_2.paths is DEFAULT.paths` and
|
|
`.index is DEFAULT.index`, so no assertion can tell `profile.index.name` apart
|
|
from `DEFAULT.index.name` when they are the same object. That identity is what
|
|
makes the change byte-neutral, and it is also what would let six of the nine
|
|
threading sites stay hard-coded with every shipped-profile test still green.
|
|
`_SYNTHETIC` exists to close exactly that gap: it renames the index and the
|
|
concept files, so a site still reading `DEFAULT` writes to the wrong path and
|
|
fails. It is a test instrument, never a supported profile.
|
|
|
|
What this does NOT claim: the materializer honours `profile.types` (validation
|
|
still runs against `DEFAULT` in `manifest.py`; both policies compare equal
|
|
today, so nothing is hidden — but the parameter does not reach it), nor
|
|
`IndexPolicy`'s judging fields (`per_directory`, `entries_match_directory`,
|
|
`root_frontmatter`). `STRICT_V1` sets all three, so passing it here produces a
|
|
bundle that does not meet its own profile. Supported here: `DEFAULT` and
|
|
`OKF_V0_2`.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from dataclasses import replace
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from llm_ingestion_okf.materialize import materialize_bundle
|
|
from llm_ingestion_okf.profiles import DEFAULT, OKF_V0_2, BundleProfile
|
|
|
|
INGESTED_AT = "2026-07-16T12:00:00Z"
|
|
|
|
# Renames both halves the disk phase reads: a threading site left on `DEFAULT`
|
|
# writes `ingest-orders.md` / `index.md` instead, and the assertions below say
|
|
# so by name rather than by a byte diff that cannot point at the cause.
|
|
_SYNTHETIC = replace(
|
|
OKF_V0_2,
|
|
paths=replace(DEFAULT.paths, ingest_prefix="x-", concept_suffix=".mdx"),
|
|
index=replace(DEFAULT.index, name="contents.md"),
|
|
)
|
|
|
|
|
|
def _manifest_data() -> dict[str, Any]:
|
|
return {
|
|
"manifest_version": 1,
|
|
"source": {"type": "file", "id": "catalogue-1", "root": "data"},
|
|
"bundle_summary": "A test bundle.",
|
|
"extractions": [
|
|
{
|
|
"id": "orders",
|
|
"title": "Orders",
|
|
"query": "orders.csv",
|
|
"okf_type": "dataset",
|
|
"max_rows": 100,
|
|
}
|
|
],
|
|
}
|
|
|
|
|
|
@pytest.fixture
|
|
def file_setup(tmp_path: Path) -> tuple[Path, Path]:
|
|
"""A manifest + CSV catalogue in tmp/src, and an empty bundle target path."""
|
|
src = tmp_path / "src"
|
|
src.mkdir(parents=True)
|
|
manifest_path = src / "manifest.json"
|
|
manifest_path.write_text(json.dumps(_manifest_data()), encoding="utf-8")
|
|
(src / "data").mkdir()
|
|
(src / "data" / "orders.csv").write_text("a,b\n1,x\n2,y\n", encoding="utf-8", newline="")
|
|
return manifest_path, tmp_path / "bundle"
|
|
|
|
|
|
# --- the profile reaches the content phase ---
|
|
|
|
|
|
def test_v0_2_profile_reaches_the_emitted_frontmatter(file_setup: tuple[Path, Path]) -> None:
|
|
"""`_render_concept_file` took `profile` since D2; the call site did not pass
|
|
one, so `OKF_V0_2` was unreachable through the only public door.
|
|
"""
|
|
manifest_path, bundle = file_setup
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT, profile=OKF_V0_2)
|
|
text = (bundle / "ingest-orders.md").read_text(encoding="utf-8")
|
|
assert f"generated: {{ by: process:llm-ingestion-okf, at: {INGESTED_AT} }}" in text
|
|
assert "sources: [{ id: catalogue-1, resource: data }]" in text
|
|
assert "generated: true" not in text
|
|
|
|
|
|
def test_default_run_is_unchanged_by_the_new_parameter(file_setup: tuple[Path, Path]) -> None:
|
|
"""Byte-neutrality at the signature: passing `DEFAULT` explicitly and passing
|
|
nothing must produce the same bundle, or the default is not the default.
|
|
"""
|
|
manifest_path, bundle = file_setup
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT)
|
|
implicit = (bundle / "ingest-orders.md").read_bytes()
|
|
index_implicit = (bundle / "index.md").read_bytes()
|
|
|
|
explicit_dir = bundle.parent / "bundle-explicit"
|
|
materialize_bundle(manifest_path, explicit_dir, INGESTED_AT, profile=DEFAULT)
|
|
assert (explicit_dir / "ingest-orders.md").read_bytes() == implicit
|
|
assert (explicit_dir / "index.md").read_bytes() == index_implicit
|
|
assert b"generated: true" in implicit
|
|
|
|
|
|
def test_profile_is_keyword_only(file_setup: tuple[Path, Path]) -> None:
|
|
"""The parameter sits behind the `*` the signature already had. A positional
|
|
fourth argument must not be accepted, or a later reordering would silently
|
|
rebind existing call sites.
|
|
"""
|
|
manifest_path, bundle = file_setup
|
|
with pytest.raises(TypeError):
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT, DEFAULT) # type: ignore[misc]
|
|
|
|
|
|
# --- A-E5: two runs into the SAME directory ---
|
|
|
|
|
|
def test_second_v0_2_run_into_the_same_directory_succeeds(
|
|
file_setup: tuple[Path, Path],
|
|
) -> None:
|
|
"""A-E5, end to end. The emitter and the ownership predicate are coupled
|
|
through the stamp value; `OKF_V0_2` changes that value. If the predicate is
|
|
not given the same profile the emitter used, the library stops recognising
|
|
its own output and the §3 collision gate fires `collision_unstamped` on the
|
|
files the previous run wrote. One run compared byte for byte cannot see it.
|
|
"""
|
|
manifest_path, bundle = file_setup
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT, profile=OKF_V0_2)
|
|
first = (bundle / "ingest-orders.md").read_bytes()
|
|
first_index = (bundle / "index.md").read_bytes()
|
|
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT, profile=OKF_V0_2)
|
|
|
|
assert (bundle / "ingest-orders.md").read_bytes() == first
|
|
assert (bundle / "index.md").read_bytes() == first_index
|
|
|
|
|
|
# --- the paths/index half of the profile reaches disk ---
|
|
|
|
|
|
def test_profile_paths_and_index_name_reach_disk(file_setup: tuple[Path, Path]) -> None:
|
|
"""Six of the nine threading sites read `paths`/`index`, which are the same
|
|
objects on every shipped profile. `_SYNTHETIC` renames both so the sites are
|
|
provable rather than merely regression-covered.
|
|
"""
|
|
manifest_path, bundle = file_setup
|
|
result = materialize_bundle(manifest_path, bundle, INGESTED_AT, profile=_SYNTHETIC)
|
|
|
|
assert (bundle / "x-orders.mdx").is_file()
|
|
assert not (bundle / "ingest-orders.md").exists()
|
|
assert (bundle / "contents.md").is_file()
|
|
assert not (bundle / "index.md").exists()
|
|
assert [path.name for path in result.written] == ["x-orders.mdx"]
|
|
assert "- [Orders](x-orders.mdx)" in (bundle / "contents.md").read_text(encoding="utf-8")
|
|
|
|
|
|
def test_second_synthetic_run_replaces_rather_than_colliding(
|
|
file_setup: tuple[Path, Path],
|
|
) -> None:
|
|
"""The §3 ownership scan globs `*{concept_suffix}` and excludes `index.name`.
|
|
Left on `DEFAULT`, the glob finds nothing under a renamed suffix, `owned` is
|
|
empty, and the gate refuses to overwrite the file this same code just wrote.
|
|
The index must also not gain a second link for the same target.
|
|
"""
|
|
manifest_path, bundle = file_setup
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT, profile=_SYNTHETIC)
|
|
first = (bundle / "x-orders.mdx").read_bytes()
|
|
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT, profile=_SYNTHETIC)
|
|
|
|
assert (bundle / "x-orders.mdx").read_bytes() == first
|
|
index_text = (bundle / "contents.md").read_text(encoding="utf-8")
|
|
assert index_text.count("- [Orders](x-orders.mdx)") == 1
|
|
|
|
|
|
def test_removed_extraction_is_unlinked_under_a_renamed_index(
|
|
file_setup: tuple[Path, Path], tmp_path: Path
|
|
) -> None:
|
|
"""`_update_index_lines` runs only on an EXISTING index, so it is reached
|
|
solely by a second run — and only its `link_pattern`/`render_link` are
|
|
profile-owned. Dropping an extraction is what forces the removal branch.
|
|
"""
|
|
manifest_path, bundle = file_setup
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT, profile=_SYNTHETIC)
|
|
|
|
data = _manifest_data()
|
|
data["extractions"][0]["id"] = "invoices"
|
|
data["extractions"][0]["title"] = "Invoices"
|
|
manifest_path.write_text(json.dumps(data), encoding="utf-8")
|
|
(tmp_path / "src" / "data" / "invoices.csv").write_text(
|
|
"a,b\n1,x\n", encoding="utf-8", newline=""
|
|
)
|
|
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT, profile=_SYNTHETIC)
|
|
|
|
index_text = (bundle / "contents.md").read_text(encoding="utf-8")
|
|
assert "- [Invoices](x-invoices.mdx)" in index_text
|
|
assert "x-orders.mdx" not in index_text
|
|
assert not (bundle / "x-orders.mdx").exists()
|
|
|
|
|
|
def test_synthetic_profile_is_not_a_shipped_profile() -> None:
|
|
"""Guards the instrument: if a future profile ever shares these overrides,
|
|
the two tests above stop proving anything and would go quietly green.
|
|
"""
|
|
assert isinstance(_SYNTHETIC, BundleProfile)
|
|
assert _SYNTHETIC.paths is not DEFAULT.paths
|
|
assert _SYNTHETIC.index is not DEFAULT.index
|
|
assert OKF_V0_2.paths is DEFAULT.paths
|
|
# `OKF_V0_2.index` stopped being DEFAULT's object at D5, which named
|
|
# `okf_version` in its `root_frontmatter`. That is NOT the distinctness this
|
|
# guard is about: every field that carries a NAME — the index filename and
|
|
# the concept filenames — is still shared, so a threading bug that passed
|
|
# the wrong profile would still emit identical bytes under either. Object
|
|
# identity was only ever a proxy for that; assert the thing itself, or this
|
|
# guard would go green on a change that leaves the instrument blind.
|
|
assert OKF_V0_2.index.name == DEFAULT.index.name
|
|
assert OKF_V0_2.index.link_template == DEFAULT.index.link_template
|
|
assert _SYNTHETIC.index.name != DEFAULT.index.name
|
|
assert _SYNTHETIC.paths.concept_suffix != DEFAULT.paths.concept_suffix
|