feat(phase-3): STRICT_V1's type enum and frontmatter schema, with the reader
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.
This commit is contained in:
parent
cf1f7984c4
commit
848e3953fc
2 changed files with 480 additions and 5 deletions
|
|
@ -79,9 +79,38 @@ class TypePolicy:
|
|||
return None
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class SchemaViolation:
|
||||
"""One way a document's frontmatter departs from the schema.
|
||||
|
||||
Reported rather than raised, like `TypeRejection`: a document can depart in
|
||||
several ways at once, and the caller decides whether that is a refusal, a
|
||||
report line, or a migration to-do. `code` is the stable machine-readable
|
||||
part; `reason` completes the sentence "<key> ...".
|
||||
"""
|
||||
|
||||
key: str
|
||||
reason: str
|
||||
code: str
|
||||
|
||||
|
||||
def _is_legal_value(value: object) -> bool:
|
||||
"""A string scalar, or a non-empty list of strings — nothing else.
|
||||
|
||||
The list shape is real, not hypothetical: the proving consumer's `Release`
|
||||
pages carry block lists. A schema that assumed scalars-only would reject
|
||||
pages they ship today.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
return True
|
||||
if isinstance(value, list):
|
||||
return bool(value) and all(isinstance(item, str) for item in value)
|
||||
return False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class FrontmatterSchema:
|
||||
"""The frontmatter key namespace and how it is emitted.
|
||||
"""The frontmatter key namespace, what it admits, and how it is emitted.
|
||||
|
||||
`order` is the canonical emission order and `collapsed_keys` names the keys
|
||||
whose whitespace runs collapse to single spaces on the way out. The DEFAULT
|
||||
|
|
@ -89,14 +118,81 @@ class FrontmatterSchema:
|
|||
B six, and each door's subset comes out in exactly the order it wrote by
|
||||
hand in Phases 1 and 2.
|
||||
|
||||
The required/allowlisted key sets the proving consumer needs (see
|
||||
`docs/phase-3-split-table.md`) are not here yet: no code reads them until
|
||||
the STRICT_V1 validator exists, and a field nothing reads is a claim
|
||||
nothing tests.
|
||||
The judging fields — `required`, `allowed`, `nullable`, `key_pattern` — are
|
||||
read by `violations`. `allowed` is `None` for an open namespace, which is
|
||||
what DEFAULT keeps: Door A emits seven keys, Door B six, and Door C writes
|
||||
an imported concept verbatim with whatever the sender wrote, so a closed
|
||||
namespace or a required set on DEFAULT would declare invalid the very
|
||||
bundles this library produces.
|
||||
"""
|
||||
|
||||
order: tuple[str, ...]
|
||||
collapsed_keys: frozenset[str] = field(default_factory=frozenset)
|
||||
required: frozenset[str] = field(default_factory=frozenset)
|
||||
allowed: frozenset[str] | None = None
|
||||
nullable: frozenset[str] = field(default_factory=frozenset)
|
||||
key_pattern: re.Pattern[str] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
if self.allowed is None:
|
||||
return
|
||||
for label, keys in (("required", self.required), ("nullable", self.nullable)):
|
||||
stray = sorted(keys - self.allowed)
|
||||
if stray:
|
||||
raise ValueError(
|
||||
f"{label} keys must be inside the allowlist "
|
||||
f"(got {', '.join(repr(key) for key in stray)}) — a schema that "
|
||||
"demands a key it also forbids can never be satisfied"
|
||||
)
|
||||
|
||||
def violations(self, values: Mapping[str, object]) -> tuple[SchemaViolation, ...]:
|
||||
"""Every way `values` departs from this schema, deterministically ordered.
|
||||
|
||||
Sorted by `(key, code)` rather than reported in mapping order: two runs
|
||||
over the same document must produce the same report, and mapping order
|
||||
is an accident of how the document was parsed.
|
||||
|
||||
Takes an already-parsed mapping. Parsing strict frontmatter is a
|
||||
separate concern — this library's line-oriented parser cannot represent
|
||||
the block lists and nulls a strict schema admits, which is why Door C
|
||||
writes imported concepts verbatim rather than round-tripping them.
|
||||
"""
|
||||
found: list[SchemaViolation] = []
|
||||
|
||||
for key in self.required - set(values):
|
||||
found.append(SchemaViolation(key, "is required and absent", "frontmatter_key_missing"))
|
||||
|
||||
for key, value in values.items():
|
||||
if self.key_pattern is not None and not self.key_pattern.fullmatch(key):
|
||||
found.append(
|
||||
SchemaViolation(
|
||||
key,
|
||||
f"is not a legal key name (must match {self.key_pattern.pattern})",
|
||||
"frontmatter_key_malformed",
|
||||
)
|
||||
)
|
||||
continue
|
||||
if self.allowed is not None and key not in self.allowed:
|
||||
found.append(
|
||||
SchemaViolation(
|
||||
key, "is not on the key allowlist", "frontmatter_key_not_allowed"
|
||||
)
|
||||
)
|
||||
continue
|
||||
if value is None:
|
||||
if key not in self.nullable:
|
||||
found.append(SchemaViolation(key, "must not be null", "frontmatter_value_null"))
|
||||
continue
|
||||
if not _is_legal_value(value):
|
||||
found.append(
|
||||
SchemaViolation(
|
||||
key,
|
||||
"must be a string or a non-empty list of strings",
|
||||
"frontmatter_value_shape",
|
||||
)
|
||||
)
|
||||
|
||||
return tuple(sorted(found, key=lambda violation: (violation.key, violation.code)))
|
||||
|
||||
def emit(self, values: Mapping[str, str]) -> str:
|
||||
"""Render `values` as line-oriented `key: value`, one line per key.
|
||||
|
|
@ -201,3 +297,52 @@ DEFAULT = BundleProfile(
|
|||
link_pattern=re.compile(r"^- \[(?P<label>[^\]]*)\]\((?P<target>[^)]+)\)$"),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
# `FRONTMATTER_KEY_ORDER` in the proving consumer's `bundle.py`: an eleven-key
|
||||
# allowlist that doubles as canonical emission order. The first eight are the
|
||||
# required set their operator ratified on 2026-07-25 (measured present on
|
||||
# 522/522 documents); the last three are layer-specific and stay optional.
|
||||
# Deriving both sets from this one tuple is what keeps them from drifting apart
|
||||
# — their BUNDLE_HASH_REGISTRY gate depends on the order.
|
||||
_STRICT_V1_KEY_ORDER = (
|
||||
"type",
|
||||
"title",
|
||||
"description",
|
||||
"timestamp",
|
||||
"layer",
|
||||
"source_tier",
|
||||
"source_url",
|
||||
"source_sha",
|
||||
"version",
|
||||
"date",
|
||||
"summary",
|
||||
)
|
||||
|
||||
# The `claude-code-llm-wiki` contract, from `docs/phase-3-split-table.md`.
|
||||
#
|
||||
# Deliberately stricter than the consumer's own validator: their REQUIRED_KEYS
|
||||
# constant is the first four keys, which is their emit-path minimum rather than
|
||||
# their contract. If they ever ship a document class that legitimately lacks
|
||||
# `source_sha`, the agreed resolution is to bump this profile — they send the
|
||||
# requirement before shipping the class, not after.
|
||||
#
|
||||
# `paths` and `index` are DEFAULT's, and only `paths` is the whole truth: the
|
||||
# filename namespaces are an ingest-spec invariant about what the doors write,
|
||||
# not a consumer preference. The consumer's index shape — one index per
|
||||
# directory, `* [Title](link) - description` entries, a root index carrying
|
||||
# okf_version/bundle_profile/okf_spec_commit — is NOT expressible by IndexPolicy
|
||||
# yet and lands with the reader that judges it. Nothing consumes a profile's
|
||||
# index policy today, so the gap is pinned by a test rather than left to drift.
|
||||
STRICT_V1 = BundleProfile(
|
||||
types=TypePolicy(allowed=frozenset({"Concept", "Guide", "Reference", "Release"})),
|
||||
frontmatter=FrontmatterSchema(
|
||||
order=_STRICT_V1_KEY_ORDER,
|
||||
required=frozenset(_STRICT_V1_KEY_ORDER[:8]),
|
||||
allowed=frozenset(_STRICT_V1_KEY_ORDER),
|
||||
nullable=frozenset({"summary"}),
|
||||
key_pattern=re.compile(r"^[a-z_]+$"),
|
||||
),
|
||||
paths=DEFAULT.paths,
|
||||
index=DEFAULT.index,
|
||||
)
|
||||
|
|
|
|||
330
tests/test_strict_v1.py
Normal file
330
tests/test_strict_v1.py
Normal file
|
|
@ -0,0 +1,330 @@
|
|||
"""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"
|
||||
Loading…
Add table
Add a link
Reference in a new issue