Plan step 3's remaining half, and the one place steps 1-3 add behavior rather than only characterizing it. Operator decision this session: build the gate NOW rather than at D2, so the multi-file D2 work is developed with the invariant already in place. OKF section 13.1 reads `timestamp` as a legacy stand-in for `generated.at` only while `generated` is ABSENT. A schema able to name both can therefore describe a document with neither a valid `generated.at` nor an eligible fallback. Refused at construction, the same shape as the reserved `verdict` layer -- and one level below the sketch that was approved, on `FrontmatterSchema` rather than `BundleProfile`: the schema is the policy that names frontmatter keys, it is the exact C3 analog, and a profile must build its schema first, so the stricter placement subsumes the other. Stated as key names rather than as a judgement on values, deliberately: every `generated` a schema can express today is a scalar (`_is_legal_value`) and so malformed as a v0.2 mapping, which makes naming both exactly the hazard. It narrows when a value model can express a well-formed `generated` -- recorded at the constant rather than left for a reader to reconstruct. Behavior-neutral for everything shipped, and not by assertion: DEFAULT emits `generated` and no `timestamp`, STRICT_V1 the reverse (which is what puts the wiki on the legacy path rather than in a defect), and the byte-exact golden suite is green. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
334 lines
14 KiB
Python
334 lines
14 KiB
Python
"""The safety net under OKF v0.2 support (plan steps 1-3).
|
|
|
|
Every test here characterizes behavior this library ALREADY has, before any
|
|
v0.2 code exists. Nothing is changed by them; that is the point. They are
|
|
written first because the one part of the v0.2 work we called risky —
|
|
`_is_ingest_owned`, the pre-mutation collision gate — is only safe to touch
|
|
with its current guarantees pinned.
|
|
|
|
Three groups, one per plan step:
|
|
|
|
1. **Characterization.** An inline flow mapping survives the scalar
|
|
`parse_frontmatter` verbatim (V-A2), a block list does not, and a v0.2
|
|
`generated` mapping can never make a file ingest-owned (V-A3). The last one
|
|
converts today's fail-safe from an accident into a documented guarantee:
|
|
a v0.2 file at a target name is refused, never overwritten.
|
|
2. **Door C consumer tolerance** (D3). OKF §14 forbids a consumer to reject on
|
|
five things; Door C is a consumer in that sense. One case per MUST NOT, plus
|
|
the §5.2 bare-`verified` mapping.
|
|
3. **No profile hard-codes an upstream version** (V4/V-A5). The `okf_version`
|
|
VALUE belongs to catalog (decision E1); a profile names the key and expresses
|
|
any value, which is what keeps "always the latest OKF version" from turning
|
|
into a constant this repo has to chase.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import json
|
|
import re
|
|
from collections.abc import Iterator
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from llm_ingestion_okf.errors import MaterializationError
|
|
from llm_ingestion_okf.materialize import (
|
|
_is_ingest_owned,
|
|
materialize_bundle,
|
|
parse_frontmatter,
|
|
)
|
|
from llm_ingestion_okf.profiles import DEFAULT, STRICT_V1, BundleProfile, FrontmatterSchema
|
|
from test_import_flow import StubImportGate, place, run
|
|
|
|
INGESTED_AT = "2026-07-25T12:00:00Z"
|
|
|
|
# The v0.2 `generated` shape, as an inline flow mapping — the form A-E3 pins
|
|
# for the coming profile, and the form the §5 single-line MUST admits.
|
|
V0_2_GENERATED = "{ by: llm-ingestion-okf/0.4.0, at: 2026-07-25T12:00:00Z }"
|
|
|
|
|
|
# --- step 1: what the scalar parser already does --------------------------
|
|
|
|
|
|
def test_an_inline_flow_mapping_survives_the_scalar_parser_verbatim(tmp_path: Path) -> None:
|
|
"""V-A2. `parse_frontmatter` splits on the FIRST colon, so a flow mapping —
|
|
colons and all — comes back as one opaque string. Byte-exact both ways:
|
|
what the emitter wrote is what the parser returns.
|
|
"""
|
|
values = {"type": "dataset", "title": "Orders", "generated": V0_2_GENERATED}
|
|
path = tmp_path / "concept.md"
|
|
path.write_bytes(f"---\n{DEFAULT.frontmatter.emit(values)}\n---\n\nBody.\n".encode())
|
|
|
|
assert path.read_bytes().decode("utf-8").splitlines()[3] == f"generated: {V0_2_GENERATED}"
|
|
assert parse_frontmatter(path) == values
|
|
|
|
|
|
def test_a_block_list_pollutes_the_scalar_parsers_key_space(tmp_path: Path) -> None:
|
|
"""The measured reason `sources` is emitted as an inline flow sequence.
|
|
|
|
Upstream's canonical `sources` is a block list of multi-key mappings. Read
|
|
through this line-oriented parser, each item line becomes a KEY: the list
|
|
disappears and `- id` / `resource` appear as frontmatter keys that no
|
|
document declared. `_is_ingest_owned` reads through this same parser, so
|
|
emitting the block form would have forced the gate and the parser to be
|
|
hardened in one step.
|
|
"""
|
|
path = tmp_path / "concept.md"
|
|
path.write_bytes(
|
|
b"---\ntype: dataset\nsources:\n - id: margin-standard\n"
|
|
b" resource: policies/margin-standard.md\n---\n\nBody.\n"
|
|
)
|
|
|
|
parsed = parse_frontmatter(path)
|
|
|
|
assert parsed["sources"] == ""
|
|
assert parsed["- id"] == "margin-standard"
|
|
assert parsed["resource"] == "policies/margin-standard.md"
|
|
|
|
|
|
def _stamped_concept(generated: str) -> bytes:
|
|
return (
|
|
"---\ntype: dataset\ntitle: Orders\n"
|
|
"ingest_manifest: manifest@0123456789abcdef\n"
|
|
f"generated: {generated}\n---\n\nBody.\n"
|
|
).encode()
|
|
|
|
|
|
def test_a_v0_1_generated_true_is_ingest_owned(tmp_path: Path) -> None:
|
|
"""The control for the test below: the stamp as v0.1 writes it DOES own."""
|
|
path = tmp_path / "ingest-orders.md"
|
|
path.write_bytes(_stamped_concept("true"))
|
|
|
|
assert _is_ingest_owned(path, "manifest") is True
|
|
|
|
|
|
def test_a_v0_2_generated_mapping_is_never_ingest_owned(tmp_path: Path) -> None:
|
|
"""V-A3. `generated` as a v0.2 mapping is not the string `true`, so the
|
|
predicate returns False and the file is not ours to replace.
|
|
|
|
The manifest reference is readable and matches — the refusal comes from
|
|
`generated` alone. This is also why `generated` can never become the
|
|
ownership predicate on its own (U3): upstream writes it for HAND-AUTHORED
|
|
files, so presence proves nothing about who generated what.
|
|
"""
|
|
path = tmp_path / "ingest-orders.md"
|
|
path.write_bytes(_stamped_concept(V0_2_GENERATED))
|
|
|
|
assert parse_frontmatter(path)["ingest_manifest"] == "manifest@0123456789abcdef"
|
|
assert _is_ingest_owned(path, "manifest") is False
|
|
|
|
|
|
def test_a_v0_2_file_at_a_target_name_is_refused_not_overwritten(tmp_path: Path) -> None:
|
|
"""The fail-safe, end to end: not-owned means the §3 collision gate refuses
|
|
the run before any mutation. A v0.2 shape arriving in a bundle Door A also
|
|
writes into can cost a run; it can never cost data.
|
|
"""
|
|
src = tmp_path / "src"
|
|
src.mkdir()
|
|
manifest_path = src / "manifest.json"
|
|
manifest_path.write_text(
|
|
json.dumps(
|
|
{
|
|
"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,
|
|
}
|
|
],
|
|
}
|
|
),
|
|
encoding="utf-8",
|
|
)
|
|
(src / "data").mkdir()
|
|
(src / "data" / "orders.csv").write_text("a,b\n1,x\n", encoding="utf-8", newline="")
|
|
bundle = tmp_path / "bundle"
|
|
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT)
|
|
concept = bundle / "ingest-orders.md"
|
|
v0_2_bytes = concept.read_bytes().replace(
|
|
b"generated: true", f"generated: {V0_2_GENERATED}".encode()
|
|
)
|
|
concept.write_bytes(v0_2_bytes)
|
|
|
|
with pytest.raises(MaterializationError) as excinfo:
|
|
materialize_bundle(manifest_path, bundle, INGESTED_AT)
|
|
|
|
assert excinfo.value.code == "collision_unstamped"
|
|
assert concept.read_bytes() == v0_2_bytes
|
|
|
|
|
|
# --- step 2: Door C against the §14 consumer tolerance rules --------------
|
|
|
|
TOLERANCE_CASES = [
|
|
pytest.param(
|
|
"---\ntype: Concept\n---\n\nOnly a type.\n",
|
|
id="missing-optional-frontmatter",
|
|
),
|
|
pytest.param(
|
|
"---\ntype: Attested Computation\ntitle: Gross margin\n---\n\nA v0.2 type.\n",
|
|
id="unknown-type-value",
|
|
),
|
|
pytest.param(
|
|
"---\ntype: dataset\nstale_after: 2027-01-01\nnot_a_key_we_know: x\n---\n\nBody.\n",
|
|
id="unknown-additional-keys",
|
|
),
|
|
pytest.param(
|
|
"---\ntype: dataset\n---\n\nSee [the other one](does-not-exist.md).\n",
|
|
id="broken-cross-link",
|
|
),
|
|
pytest.param(
|
|
"---\ntype: dataset\nverified: { by: human:jsmith@acme, at: 2026-07-01T09:00:00Z }\n"
|
|
"---\n\nBody.\n",
|
|
id="bare-verified-mapping",
|
|
),
|
|
]
|
|
|
|
|
|
@pytest.mark.parametrize("document", TOLERANCE_CASES)
|
|
def test_door_c_merges_what_a_consumer_must_not_reject(tmp_path: Path, document: str) -> None:
|
|
"""D3/§14. Each case is one thing a conformant consumer MUST NOT reject.
|
|
|
|
Door C's tolerance is structural rather than lenient: it writes the guard's
|
|
bytes verbatim and never parses the sender's frontmatter, so there is no
|
|
place for a shape judgement to be made. That is what these cases pin — a
|
|
future reader (D1b) that starts judging frontmatter at this door would
|
|
break them, which is exactly when we want to hear about it.
|
|
|
|
The bare-`verified` case is the §5.2 MUST in its Door C form: the mapping is
|
|
persisted unmodified. Coercing it to a one-element list is the READER's
|
|
obligation and lands with D1b; nothing here reads it.
|
|
"""
|
|
place(tmp_path / "source", "notes/tolerated.md", document)
|
|
|
|
result, bundle = run(tmp_path, StubImportGate(), ingested_at=INGESTED_AT)
|
|
|
|
assert result.failed == ()
|
|
assert [entry.concept_path for entry in result.merged] == ["notes/tolerated.md"]
|
|
assert (bundle / "import-notes-tolerated.md").read_bytes() == document.encode("utf-8")
|
|
|
|
|
|
def test_door_c_does_not_require_the_source_bundle_to_carry_an_index(tmp_path: Path) -> None:
|
|
"""The fifth MUST NOT: a missing `index.md` is not grounds for rejection.
|
|
|
|
Door C generates the target index itself, so the sender's bundle need not
|
|
carry one — asserted explicitly here because every other test in the suite
|
|
omits the source index incidentally rather than as a stated rule.
|
|
"""
|
|
document = "---\ntype: dataset\n---\n\nNo index anywhere in the source.\n"
|
|
place(tmp_path / "source", "tables/users.md", document)
|
|
assert not (tmp_path / "source" / "index.md").exists()
|
|
|
|
result, bundle = run(tmp_path, StubImportGate(), ingested_at=INGESTED_AT)
|
|
|
|
assert result.failed == ()
|
|
assert [entry.concept_path for entry in result.merged] == ["tables/users.md"]
|
|
assert (bundle / "index.md").read_text(encoding="utf-8") == (
|
|
"- [tables/users](import-tables-users.md)\n"
|
|
)
|
|
|
|
|
|
# --- step 3: no profile hard-codes an upstream version --------------------
|
|
|
|
# A version LITERAL — `0.1`, `0.2`, `2026.7.3`. Compiled patterns are walked
|
|
# past deliberately: a regex is a shape, and a form gate for version values is
|
|
# the opposite of hard-coding one.
|
|
_VERSION_LITERAL_RE = re.compile(r"^\d+(\.\d+)+$")
|
|
|
|
|
|
def _strings_in(value: Any) -> Iterator[str]:
|
|
"""Every string reachable from a profile, through dataclasses and
|
|
collections alike. Recursive so that a version added to a nested policy —
|
|
the only place it could plausibly be added — is still found."""
|
|
if isinstance(value, str):
|
|
yield value
|
|
elif dataclasses.is_dataclass(value) and not isinstance(value, type):
|
|
for field in dataclasses.fields(value):
|
|
yield from _strings_in(getattr(value, field.name))
|
|
elif isinstance(value, (tuple, list, set, frozenset)):
|
|
for item in value:
|
|
yield from _strings_in(item)
|
|
|
|
|
|
@pytest.mark.parametrize("profile", [DEFAULT, STRICT_V1], ids=["DEFAULT", "STRICT_V1"])
|
|
def test_no_profile_hard_codes_an_upstream_version(profile: BundleProfile) -> None:
|
|
"""V4/V-A5. A profile names `okf_version` as a key and never carries its
|
|
value. The value tracks the upstream Google version and belongs to catalog
|
|
(decision E1), so a constant here would be this repo claiming a decision it
|
|
does not own — and the one that would have to be chased on every upstream
|
|
release.
|
|
"""
|
|
hard_coded = sorted({s for s in _strings_in(profile) if _VERSION_LITERAL_RE.match(s)})
|
|
|
|
assert hard_coded == []
|
|
|
|
|
|
def test_the_key_name_is_what_a_profile_pins(tmp_path: Path) -> None:
|
|
"""The other half of V4: naming the key is allowed and is what STRICT_V1
|
|
does. Without this, the test above would also pass on a profile that had
|
|
stopped requiring the declaration at all."""
|
|
assert "okf_version" in STRICT_V1.index.root_frontmatter
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"schema_kwargs",
|
|
[
|
|
pytest.param({"order": ("timestamp", "generated")}, id="both-emitted"),
|
|
pytest.param(
|
|
{"order": ("timestamp",), "allowed": frozenset({"timestamp", "generated"})},
|
|
id="one-emitted-one-admitted",
|
|
),
|
|
pytest.param(
|
|
{"order": (), "required": frozenset({"timestamp", "generated"})},
|
|
id="both-required",
|
|
),
|
|
],
|
|
)
|
|
def test_a_schema_naming_both_timestamp_and_generated_cannot_be_built(
|
|
schema_kwargs: dict[str, Any],
|
|
) -> None:
|
|
"""V-A7. OKF §13.1 grants the `timestamp` fallback only while `generated` is
|
|
ABSENT, so a schema able to name both can describe a document with neither a
|
|
valid `generated.at` nor an eligible fallback. Refused at construction, the
|
|
same shape as the reserved `verdict` layer: a profile that could reach the
|
|
combination cannot be built, let alone handed to a door.
|
|
"""
|
|
with pytest.raises(ValueError, match="timestamp"):
|
|
FrontmatterSchema(**schema_kwargs)
|
|
|
|
|
|
def test_the_two_shipped_profiles_sit_on_opposite_sides_of_the_fallback() -> None:
|
|
"""Why the gate above costs nothing: neither shipped profile is near the
|
|
combination, and they are not near it in opposite directions. `DEFAULT`
|
|
emits `generated` and no `timestamp`; `STRICT_V1` emits `timestamp` and no
|
|
`generated`, which is precisely what puts the wiki on the §13.1 legacy path
|
|
rather than in a defect.
|
|
"""
|
|
assert "generated" in DEFAULT.frontmatter.order
|
|
assert "timestamp" not in DEFAULT.frontmatter.order
|
|
assert "timestamp" in STRICT_V1.frontmatter.order
|
|
assert "generated" not in STRICT_V1.frontmatter.order
|
|
assert STRICT_V1.frontmatter.allowed is not None
|
|
assert "generated" not in STRICT_V1.frontmatter.allowed
|
|
|
|
|
|
@pytest.mark.parametrize("declared", ["0.1", "0.2", "1.0", "2026.7.3"])
|
|
def test_the_root_frontmatter_policy_expresses_any_version_value(declared: str) -> None:
|
|
"""V4. The policy judges presence and ORDER, never the value — so the day
|
|
upstream ships v0.3, no index policy in this library has to change.
|
|
"""
|
|
text = (
|
|
f"---\nokf_version: {declared}\nbundle_profile: strict-v1\n"
|
|
"okf_spec_commit: 0123456789abcdef\n---\n\n# Bundle\n\n* [A](a.md) - a\n"
|
|
)
|
|
|
|
violations = STRICT_V1.index.violations(text, is_root=True, expected_targets={"a.md"})
|
|
|
|
assert violations == ()
|