Phase 3 step 3, frontmatter half. FrontmatterSchema grows the four judging fields the proving consumer's contract needs — required, allowed, nullable, key_pattern — and `violations` is what reads them, so no field lands as an untested claim. STRICT_V1 requires the eight keys their operator ratified on 2026-07-25 inside the eleven-key allowlist that doubles as emission order, both derived from one tuple so they cannot drift; `summary` is the single nullable key; a value is a string scalar or a NON-EMPTY LIST OF STRINGS, because their Release pages carry block lists and a scalars-only schema would reject pages they ship today. Measured, not assumed: DEFAULT must NOT grow a required set. Door A emits seven keys, Door B six, and Door C writes an imported concept verbatim with whatever the sender wrote, so any required set on DEFAULT would declare invalid the bundles this library itself produces. The plan's other cross-profile direction — a wiki bundle rejected under DEFAULT — therefore has to come from index and path shape, not from frontmatter. Recorded here because the plan implied otherwise. Violations are reported, not raised, like TypeRejection, and sorted by (key, code): a report that depended on mapping order would not be reproducible. An unsatisfiable schema — a required or nullable key outside its own allowlist — fails at construction. Two boundaries kept explicit. `violations` judges an already-parsed mapping; parsing strict frontmatter needs a parser this library does not have, which is the same reason Door C writes verbatim. And STRICT_V1's index policy is still DEFAULT's, pinned by a test as a known gap: per-directory indexes and an entry description have no field on IndexPolicy yet, and nothing consumes a profile's index policy today. C1 re-proven: 443 existing tests unmodified and green (468 total), and `git diff --stat examples/` empty.
330 lines
12 KiB
Python
330 lines
12 KiB
Python
"""The proving consumer's contract as a profile (Phase 3, step 3).
|
|
|
|
`STRICT_V1` is the `claude-code-llm-wiki` bundle contract expressed in the same
|
|
object `DEFAULT` uses. Everything pinned here was measured in their repository
|
|
on 2026-07-25 and settled in `docs/phase-3-split-table.md`; the eight-key
|
|
required set is their operator's ratification, deliberately stricter than the
|
|
four-key constant their own validator enforces.
|
|
|
|
These tests are also the reader that earns the new schema fields. A profile
|
|
field no code reads is a claim no test covers, so `required`, `allowed`,
|
|
`nullable` and `key_pattern` land together with `FrontmatterSchema.violations`,
|
|
which is what reads them.
|
|
|
|
What is NOT here, and why: `violations` takes an already-parsed mapping. This
|
|
library's line-oriented parser cannot represent the block lists and nulls the
|
|
consumer's frontmatter admits — that is precisely why Door C writes imported
|
|
concepts verbatim — so parsing strict-v1 frontmatter is a separate concern from
|
|
judging it, and lands separately.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from llm_ingestion_okf.profiles import (
|
|
DEFAULT,
|
|
STRICT_V1,
|
|
FrontmatterSchema,
|
|
)
|
|
|
|
# The eight keys the consumer's operator ratified on 2026-07-25, measured
|
|
# present on 522/522 of their documents.
|
|
RATIFIED_REQUIRED = frozenset(
|
|
{
|
|
"type",
|
|
"title",
|
|
"description",
|
|
"timestamp",
|
|
"layer",
|
|
"source_tier",
|
|
"source_url",
|
|
"source_sha",
|
|
}
|
|
)
|
|
|
|
# `FRONTMATTER_KEY_ORDER` in their `bundle.py`: the eight above plus three
|
|
# layer-specific keys, doubling as canonical emission order.
|
|
RATIFIED_ORDER = (
|
|
"type",
|
|
"title",
|
|
"description",
|
|
"timestamp",
|
|
"layer",
|
|
"source_tier",
|
|
"source_url",
|
|
"source_sha",
|
|
"version",
|
|
"date",
|
|
"summary",
|
|
)
|
|
|
|
|
|
def wiki_document() -> dict[str, object]:
|
|
"""A conforming `concepts/` page, every allowlisted key present."""
|
|
return {
|
|
"type": "Concept",
|
|
"title": "Hooks",
|
|
"description": "Event-driven automation in Claude Code.",
|
|
"timestamp": "2026-07-25T12:00:00Z",
|
|
"layer": "concepts",
|
|
"source_tier": "official-docs",
|
|
"source_url": "https://docs.claude.com/en/docs/claude-code/hooks",
|
|
"source_sha": "0" * 64,
|
|
"version": "2.1.183",
|
|
"date": "2026-07-25",
|
|
"summary": "What hooks are and when they fire.",
|
|
}
|
|
|
|
|
|
def codes(schema: FrontmatterSchema, values: dict[str, object]) -> list[str]:
|
|
return [violation.code for violation in schema.violations(values)]
|
|
|
|
|
|
# --- the profile itself ---------------------------------------------------
|
|
|
|
|
|
def test_strict_v1_closes_the_type_enum_to_the_four_document_classes() -> None:
|
|
"""Their four classes exactly — an open set would admit anything."""
|
|
assert STRICT_V1.types.allowed == frozenset({"Concept", "Guide", "Reference", "Release"})
|
|
|
|
|
|
def test_strict_v1_requires_the_eight_ratified_keys() -> None:
|
|
"""Eight, not the four their `REQUIRED_KEYS` constant spells.
|
|
|
|
Their constant is the emit-path minimum; the contract their operator
|
|
ratified is all eight. This is the one place the profile is deliberately
|
|
stricter than the consumer's own validator, and the agreed resolution if a
|
|
document class ever legitimately lacks `source_sha` is to bump the profile,
|
|
not to bend the bundle.
|
|
"""
|
|
assert STRICT_V1.frontmatter.required == RATIFIED_REQUIRED
|
|
|
|
|
|
def test_strict_v1_allowlist_is_eleven_keys_and_doubles_as_emission_order() -> None:
|
|
"""The allowlist and the order are one list, not two that can drift apart.
|
|
|
|
Their `BUNDLE_HASH_REGISTRY` gate depends on regeneration being
|
|
byte-identical, which is what the fixed order buys.
|
|
"""
|
|
assert STRICT_V1.frontmatter.order == RATIFIED_ORDER
|
|
assert STRICT_V1.frontmatter.allowed == frozenset(RATIFIED_ORDER)
|
|
|
|
|
|
def test_the_three_layer_specific_keys_are_optional_not_required() -> None:
|
|
"""`version`/`date` sit on 351/522 documents and `summary` on 349/522.
|
|
|
|
Requiring them would reject pages the consumer ships today.
|
|
"""
|
|
allowed = STRICT_V1.frontmatter.allowed
|
|
assert allowed is not None
|
|
for key in ("version", "date", "summary"):
|
|
assert key in allowed
|
|
assert key not in STRICT_V1.frontmatter.required
|
|
|
|
|
|
def test_summary_is_the_only_nullable_key() -> None:
|
|
"""The single deliberate escape from strings-only frontmatter."""
|
|
assert STRICT_V1.frontmatter.nullable == frozenset({"summary"})
|
|
|
|
|
|
# --- the reader: a conforming document -----------------------------------
|
|
|
|
|
|
def test_a_conforming_wiki_document_has_no_violations() -> None:
|
|
assert STRICT_V1.frontmatter.violations(wiki_document()) == ()
|
|
|
|
|
|
def test_the_optional_keys_may_simply_be_absent() -> None:
|
|
"""A `concepts/` page carries neither `version` nor `date`."""
|
|
document = wiki_document()
|
|
for key in ("version", "date", "summary"):
|
|
del document[key]
|
|
assert STRICT_V1.frontmatter.violations(document) == ()
|
|
|
|
|
|
# --- the reader: cross-profile rejection ---------------------------------
|
|
|
|
|
|
def test_a_door_a_ingest_document_is_rejected_under_strict_v1() -> None:
|
|
"""The plan's cross-profile test, frontmatter half.
|
|
|
|
A bundle this library generates under `DEFAULT` carries the ingest-spec
|
|
layer, which shares only `type` and `title` with the consumer's contract:
|
|
six required keys are missing and five of its own keys are off-allowlist.
|
|
"""
|
|
door_a = {
|
|
"type": "dataset",
|
|
"title": "Orders",
|
|
"source_system": "golden-catalogue",
|
|
"source_query": "orders.csv",
|
|
"ingested_at": "2026-07-16T12:00:00Z",
|
|
"ingest_manifest": "manifest@37674ac20059e788",
|
|
"generated": "true",
|
|
}
|
|
violations = STRICT_V1.frontmatter.violations(door_a)
|
|
assert violations != ()
|
|
|
|
missing = {v.key for v in violations if v.code == "frontmatter_key_missing"}
|
|
assert missing == RATIFIED_REQUIRED - {"type", "title"}
|
|
|
|
off_allowlist = {v.key for v in violations if v.code == "frontmatter_key_not_allowed"}
|
|
assert off_allowlist == {
|
|
"source_system",
|
|
"source_query",
|
|
"ingested_at",
|
|
"ingest_manifest",
|
|
"generated",
|
|
}
|
|
|
|
|
|
def test_default_judges_nothing_about_keys_so_both_doors_stay_valid() -> None:
|
|
"""`DEFAULT` must NOT grow a required set — measured, not assumed.
|
|
|
|
Door A emits seven keys, Door B six, and Door C writes an imported concept
|
|
verbatim with whatever frontmatter the sender wrote. A required set on
|
|
`DEFAULT` would therefore declare invalid the very bundles this library
|
|
produces. `DEFAULT` keeps an open namespace; the rejection of a wiki-shaped
|
|
bundle under `DEFAULT` comes from the index and path shape instead.
|
|
"""
|
|
assert DEFAULT.frontmatter.required == frozenset()
|
|
assert DEFAULT.frontmatter.allowed is None
|
|
assert DEFAULT.frontmatter.violations(wiki_document()) == ()
|
|
assert DEFAULT.frontmatter.violations({"type": "dataset", "title": "Orders"}) == ()
|
|
|
|
|
|
# --- the reader: each check ----------------------------------------------
|
|
|
|
|
|
def test_a_missing_required_key_is_named_individually() -> None:
|
|
document = wiki_document()
|
|
del document["source_sha"]
|
|
violations = STRICT_V1.frontmatter.violations(document)
|
|
assert [(v.key, v.code) for v in violations] == [("source_sha", "frontmatter_key_missing")]
|
|
|
|
|
|
def test_a_key_outside_the_allowlist_is_refused() -> None:
|
|
"""A closed allowlist is what makes the contract enumerable at all."""
|
|
document = wiki_document()
|
|
document["author"] = "someone"
|
|
assert codes(STRICT_V1.frontmatter, document) == ["frontmatter_key_not_allowed"]
|
|
|
|
|
|
def test_a_key_name_outside_the_grammar_is_refused() -> None:
|
|
"""`^[a-z_]+$`: a key that reaches the allowlist check malformed would
|
|
otherwise be reported only as 'not allowed', which hides the real defect."""
|
|
schema = FrontmatterSchema(order=("type",), key_pattern=STRICT_V1.frontmatter.key_pattern)
|
|
violations = schema.violations({"Source-URL": "x"})
|
|
assert [(v.key, v.code) for v in violations] == [("Source-URL", "frontmatter_key_malformed")]
|
|
|
|
|
|
def test_summary_may_be_null_and_no_other_key_may() -> None:
|
|
document = wiki_document()
|
|
document["summary"] = None
|
|
assert STRICT_V1.frontmatter.violations(document) == ()
|
|
|
|
document["description"] = None
|
|
assert [(v.key, v.code) for v in STRICT_V1.frontmatter.violations(document)] == [
|
|
("description", "frontmatter_value_null")
|
|
]
|
|
|
|
|
|
def test_a_non_empty_list_of_strings_is_a_legal_value() -> None:
|
|
"""The block-list shape their `Release` pages carry.
|
|
|
|
A profile that assumed scalars-only would reject the consumer's own pages —
|
|
and a parser that assumed scalars-only would silently destroy them, which
|
|
is why Door C writes imported concepts verbatim.
|
|
"""
|
|
document = wiki_document()
|
|
document["source_url"] = ["https://a.example/one", "https://a.example/two"]
|
|
assert STRICT_V1.frontmatter.violations(document) == ()
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"value",
|
|
[[], ["ok", 3], {"nested": "map"}, 7, True],
|
|
ids=["empty-list", "list-with-non-string", "mapping", "int", "bool"],
|
|
)
|
|
def test_a_value_outside_the_scalar_or_string_list_shape_is_refused(value: object) -> None:
|
|
document = wiki_document()
|
|
document["title"] = value
|
|
assert [(v.key, v.code) for v in STRICT_V1.frontmatter.violations(document)] == [
|
|
("title", "frontmatter_value_shape")
|
|
]
|
|
|
|
|
|
# --- determinism and construction-time invariants -------------------------
|
|
|
|
|
|
def test_violations_come_back_in_a_deterministic_order() -> None:
|
|
"""Two runs over the same document must report identically ordered.
|
|
|
|
Determinism is bit-exact in this library; a violation list that depended on
|
|
mapping insertion order would make a report irreproducible.
|
|
"""
|
|
document = wiki_document()
|
|
del document["source_sha"]
|
|
del document["layer"]
|
|
document["zzz"] = "off-allowlist"
|
|
document["aaa"] = "off-allowlist"
|
|
|
|
reported = [(v.key, v.code) for v in STRICT_V1.frontmatter.violations(document)]
|
|
assert reported == sorted(reported)
|
|
|
|
reversed_document = dict(reversed(list(document.items())))
|
|
assert [
|
|
(v.key, v.code) for v in STRICT_V1.frontmatter.violations(reversed_document)
|
|
] == reported
|
|
|
|
|
|
def test_a_required_key_outside_the_allowlist_cannot_be_constructed() -> None:
|
|
"""An unsatisfiable schema is a defect in the profile, not in a document."""
|
|
with pytest.raises(ValueError, match="required"):
|
|
FrontmatterSchema(
|
|
order=("type", "title"),
|
|
required=frozenset({"type", "absent_from_allowlist"}),
|
|
allowed=frozenset({"type", "title"}),
|
|
)
|
|
|
|
|
|
def test_a_nullable_key_outside_the_allowlist_cannot_be_constructed() -> None:
|
|
with pytest.raises(ValueError, match="nullable"):
|
|
FrontmatterSchema(
|
|
order=("type",),
|
|
allowed=frozenset({"type"}),
|
|
nullable=frozenset({"summary"}),
|
|
)
|
|
|
|
|
|
def test_strict_v1_path_namespaces_are_the_spec_namespaces() -> None:
|
|
"""The door filename prefixes are an ingest-spec invariant, not a preference.
|
|
|
|
They describe what Doors A/B/C write, so they are the same under any
|
|
profile; the consumer's own files are written by their pipeline, not by a
|
|
door, and are unaffected either way.
|
|
"""
|
|
assert STRICT_V1.paths == DEFAULT.paths
|
|
|
|
|
|
def test_strict_v1_index_policy_is_still_the_default_shape() -> None:
|
|
"""A known gap, pinned so it cannot drift silently.
|
|
|
|
The consumer's index shape — one index per directory, entries
|
|
`* [Title](link) - description`, exactly one `# ` heading, and a root index
|
|
carrying okf_version/bundle_profile/okf_spec_commit — is not expressible by
|
|
`IndexPolicy` as it stands: neither per-directory indexes nor a description
|
|
on an entry has a field. It lands with the reader that judges it. Until
|
|
then, nothing consumes a profile's index policy, so this assertion is the
|
|
honest statement of where the port has reached.
|
|
"""
|
|
assert STRICT_V1.index == DEFAULT.index
|
|
assert "description" not in STRICT_V1.index.link_template
|
|
|
|
|
|
def test_strict_v1_emission_is_the_ordered_prefix_then_the_sorted_tail() -> None:
|
|
"""Commons decision D1, unchanged by the stricter namespace."""
|
|
emitted = STRICT_V1.frontmatter.emit(
|
|
{"title": "Hooks", "type": "Concept", "description": "d", "timestamp": "t"}
|
|
)
|
|
assert emitted == "type: Concept\ntitle: Hooks\ndescription: d\ntimestamp: t"
|