`IndexPolicy` grew five judging fields and `IndexPolicy.violations`, closing the
gap a test has been pinning since `848e395`. `STRICT_V1.index` is now the proving
consumer's shape rather than DEFAULT's.
The design was settled by a conflict rather than by a preference. The convention
owner reported that an index is an authored count of a directory's children and
that a validator enumerating the directory has implemented the wrong contract.
Checked against the proving consumer before adopting it, the two turn out to be
directly opposed: gate BUNDLE_INDEX_COMPLETE (validate.py:1081-1120, ERROR) builds
its expected set by enumerating the directory and demands an exact bidirectional
match, and bundle.py:498-567 writes every index from a tree walk. Neither is
incoherent inside its own spec, so authored-versus-derived is a policy field in
both directions and a library invariant in neither.
Consequences encoded rather than documented: nothing here enumerates a directory
— the caller supplies the listing, `violations` refuses one when the profile's
index is authored and refuses to run without one when it is derived, so code
written to the wrong contract fails at the call instead of passing every test one
would think to write. Root and nested indexes are asymmetric (confirmed in both
consumers, different key sets). A per-entry description is template-level, so
`render_link` refuses both a missing description and an unwanted one.
DEFAULT keeps upstream's root-only index and judges nothing, for the same measured
reason it carries no required frontmatter key set: upstream binds `index.md` to
the bundle root alone, so a judging default would condemn conforming bundles.
25 new tests, 493 green. C1 re-proven: `git diff --stat examples/` empty.
371 lines
14 KiB
Python
371 lines
14 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_the_consumers_shape() -> None:
|
|
"""The gap this test used to pin is closed; it now states the shape.
|
|
|
|
Every value is read out of their code rather than recalled — the entry
|
|
template and the root/nested frontmatter split from `bundle.py:498-567`,
|
|
the bidirectional match from gate `BUNDLE_INDEX_COMPLETE`
|
|
(`validate.py:1081-1120`, severity ERROR). The reader that judges all of
|
|
it lives on `IndexPolicy.violations`, so no field here is written without
|
|
something that reads it.
|
|
"""
|
|
index = STRICT_V1.index
|
|
assert index != DEFAULT.index
|
|
assert index.name == DEFAULT.index.name
|
|
assert index.render_link("Hooks", "hooks.md", "How hooks fire.") == (
|
|
"* [Hooks](hooks.md) - How hooks fire."
|
|
)
|
|
assert index.per_directory is True
|
|
assert index.heading_required is True
|
|
assert index.allows_prose is False
|
|
assert index.entries_match_directory is True
|
|
assert index.root_frontmatter == ("okf_version", "bundle_profile", "okf_spec_commit")
|
|
|
|
|
|
def test_a_conforming_wiki_root_index_passes_and_a_default_one_does_not() -> None:
|
|
"""The plan's second cross-profile test: a bundle valid under one profile
|
|
is refused by the other. It could not come from frontmatter — DEFAULT has
|
|
no required key set, by measurement — so it comes from index shape.
|
|
"""
|
|
conforming = (
|
|
"---\nokf_version: 0.1\nbundle_profile: strict-v1\n"
|
|
"okf_spec_commit: d44368c\n---\n\n# Wiki\n\n"
|
|
"* [Concepts](concepts/index.md) - Machine-generated index of concepts.\n"
|
|
)
|
|
assert (
|
|
STRICT_V1.index.violations(conforming, is_root=True, expected_targets={"concepts/index.md"})
|
|
== ()
|
|
)
|
|
|
|
default_shaped = "# Wiki\n\nCurated prose.\n\n- [Concepts](concepts/index.md)\n"
|
|
codes = {
|
|
violation.code
|
|
for violation in STRICT_V1.index.violations(
|
|
default_shaped, is_root=True, expected_targets={"concepts/index.md"}
|
|
)
|
|
}
|
|
assert codes == {
|
|
"index_root_key_missing",
|
|
"index_prose_not_allowed",
|
|
"index_entry_missing",
|
|
}
|
|
# ...and the reverse direction: DEFAULT judges neither of them, because a
|
|
# judging default would condemn the bundles this library itself writes.
|
|
assert DEFAULT.index.violations(conforming, is_root=True) == ()
|
|
assert DEFAULT.index.violations(default_shaped, is_root=True) == ()
|
|
|
|
|
|
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"
|