llm-ingestion-okf/src/llm_ingestion_okf/profiles.py
Kjell Tore Guttormsen ed0418f228 fix(frontmatter): write a value a YAML reader reads back, and read both forms
K3-22. SPEC SS 11 point 1: "Every non-reserved `.md` file in the tree
contains a parseable YAML frontmatter block." Measured with PyYAML 6.0.3,
okf's own default K2 bundle failed safe_load on 41 of 455 blocks and the
R761 build on 1 of 2 763, every one a block scalar written verbatim.

Block (the profile emitter, every key): a value the K3-19 rule refuses as
plain is written double-quoted, `\` and `"` escaped; every other value keeps
its bytes, and a flow collection or an empty value is written as it stands.
The rule, now `profiles.yaml_block_plain`, agrees with PyYAML on every
top-level value in eleven measured trees (0 refused that it reads verbatim,
0 kept that it does not). Double, never single: 0 values in those trees are
`"`-wrapped and 11 193 are `'`-wrapped.

Flow (`sources`, Door A and Door B, and a run-stated flow value): the pinned
guard refuses ANY quote in a flow mapping (1.3.0, measured), so a leaf PyYAML
needs quoted has no form both read. `yaml_flow_plain` refuses it instead:
`,[]{}`, `?`, a quote, ": ", " #", a trailing `:`, a leading indicator -- a
leading `-` before a non-space excepted, which both readers take. The file
name is checked too, because it is the entry's `title` when the document
declares none. Existing codes: inbox_source_file_unaddressable,
inbox_source_title_unaddressable, source_reference_unquotable,
run_frontmatter_invalid.

Readers: parse_frontmatter, profiles' and structure's copies, and both
read_sources branches unquote a `"`-wrapped value (`\"` and `\\` decoded,
nothing else); `'`-wrapped values are untouched, and structure keeps the
single-quote rule it already had. The flow-mapping split is quote-aware, so
`{ title: "a, b" }` is one pair. The generated SKILL.md header goes through
the same block rule.

TWO K3-19 TESTS MOVED, deliberately: test_run_frontmatter built with
`sources=[{ resource: ...?languageCode=nb, ... }]`, the exact form PyYAML
refused on 2 761 of 2 761 frontmatters of K3-19's flagged build. The two
build tests now write an address without `?`; the flag-grammar test keeps
the `?` address (it only splits), and a new test holds that the build
refuses it with exit 2 and writes nothing.

1753 passed, 1 skipped (OKF_HTML_CORPUS, known). No golden moved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-11 11:09:49 +02:00

1567 lines
72 KiB
Python

"""The bundle contract as configuration (Phase 3).
What a valid bundle looks like — which concept types exist, which frontmatter
keys are emitted and in which order, which filename namespaces the doors own,
and what an index line looks like — is a profile, not a set of constants
scattered across the doors. `DEFAULT` is exactly the ingest-spec v1 + Phase 2
contract, so nothing observable changes for a caller that never mentions a
profile; the golden fixtures are the byte-level proof.
Two things deliberately do NOT live here. Security is the guard's, always: no
disposition, origin or channel vocabulary belongs on a profile. And the
reserved `verdict` layer is a spec invariant rather than profile config — it is
refused at construction, so a profile admitting it cannot be built, let alone
passed to a door. The `timestamp`/`generated` pair is refused the same way, and
for the same reason: both are things a profile must not be able to express.
Profiles are constructed in code. Config-file loading and inheritance chains
are extension points, not v1 (settled with the operator at phase start).
"""
from __future__ import annotations
import re
from collections.abc import Collection, Iterable, Mapping, Sequence
from dataclasses import dataclass, field, replace
# The one layer no profile may admit (ingest-spec §3): the promotion gate is
# the only path into it. Compared case-insensitively, as both doors already do.
RESERVED_OKF_TYPE = "verdict"
# The one key PAIR no profile may name (OKF §13.1): `timestamp` is readable as
# a legacy stand-in for `generated.at` only while `generated` is ABSENT, so a
# schema able to name both can describe a document that has neither. Stated as
# key names rather than as a judgement on values because every `generated` a
# schema can express today is a scalar (`_is_legal_value`) and therefore
# malformed as a v0.2 mapping — naming both IS the hazard here. When a value
# model can express a well-formed `generated`, this narrows with it.
# "Naming" spans EVERY field that puts a key in the schema's namespace — `order`,
# `required`, `allowed` and `nullable` alike. A field left out of that union is a
# hole in the gate, not a narrower gate: the namespace is open by default, so a
# key named only by `nullable` is admitted just as surely as an emitted one.
_TIMESTAMP_FALLBACK_PAIR = frozenset({"timestamp", "generated"})
# --- YAML scalar forms (K3-22) -----------------------------------------------
#
# SPEC SS 11 point 1 requires "a parseable YAML frontmatter block" and SS 4
# names no YAML version and no subset, so the reader a consumer has decides;
# PyYAML is the common one. The forms are decided by RULES here and never by a
# parser -- this package's one runtime dependency is the guard -- and the rules
# are validated against PyYAML in `tests/test_yaml_frontmatter.py`, both error
# directions counted.
# What a YAML reader takes as syntax at the START of a plain scalar.
_YAML_INDICATORS = frozenset("-?:,[]{}#&*!|>'\"%@`")
# What a plain scalar INSIDE a flow mapping may not carry, for one of the two
# readers a `sources` entry has to survive. `,[]{}` end it for both. `?` ends
# it for PyYAML, whose scanner stops a flow plain scalar there, so a URL with a
# query string fails `safe_load`. A quote anywhere is refused by the pinned
# guard, which admits no quoted leaf in a flow mapping (1.3.0, measured). So
# quoting cannot rescue a flow value: plain fails one reader, quoted the other.
_FLOW_UNSAFE = frozenset(",[]{}?'\"")
# A key inside a flow mapping, as the guard's `_KEY_RE` and our readers take it.
_FLOW_KEY = re.compile(r"^[A-Za-z_][A-Za-z0-9_-]*$")
def yaml_block_plain(value: str) -> bool:
"""Whether `value` reads back verbatim as a plain scalar in a block mapping.
MEASURED ON R761 (K3-19): 217 of 2 024 first spec points carry `": "`, and
PyYAML's `safe_load` refused exactly those 217 concepts' frontmatter. K3-22
measured the same rule over every top-level value in eleven trees (the K2
default bundle, the R761 HTML and XML builds, a five-document project, this
repository's examples and fixtures, and five consumer bundles): 0 refused
that PyYAML returns verbatim, 0 kept that it does not. `" #"` is here
although PyYAML does not refuse it: `title: Kap #3` loads, as `Kap`.
"""
return (
bool(value)
and value[0] not in _YAML_INDICATORS
and ": " not in value
and " #" not in value
and not value.endswith(":")
and not any(char in value for char in "\t\n\r")
)
def yaml_flow_plain(value: str) -> bool:
"""Whether `value` reads back verbatim as a plain scalar inside a flow
mapping, for PyYAML AND the pinned guard.
The block rule plus `_FLOW_UNSAFE`, with one exception the block rule does
not make: a leading `-` followed by a non-space is an ordinary character in
a flow mapping for both readers (`del/-utkast.pdf`), and refusing it would
refuse an address both of them read. The block rule keeps refusing it
because it also decides which spec points are written at all (K3-19), and
in a block mapping a refusal only costs a pair of quotes.
"""
if not value or any(char in value for char in _FLOW_UNSAFE):
return False
rest = value[1:] if value[0] == "-" and value[1:2] not in ("", " ", "\t") else value
return yaml_block_plain(rest)
def yaml_flow_collection(value: str) -> bool:
"""Whether `value` is SHAPED as a flow collection (`[...]` or `{...}`).
Structure a producer built -- `source_offset: [0, 4]`, `generated: { by: x,
at: y }` -- is written as it stands; measured over the same eleven trees,
only structural keys carry this shape and no `title` does.
"""
return (value[:1], value[-1:]) in (("[", "]"), ("{", "}"))
def yaml_flow_collection_plain(value: str) -> bool:
"""Whether a flow collection parses, for PyYAML and the guard, into what
was written: one `{ key: leaf, ... }` mapping, or a `[...]` sequence of
such mappings or of scalars, every leaf `yaml_flow_plain`."""
if value[:1] == "{" and value[-1:] == "}":
return _flow_mapping_plain(value)
if not (value[:1] == "[" and value[-1:] == "]"):
return False
items: list[str] = []
depth = 0
current: list[str] = []
for char in value[1:-1]:
depth += {"{": 1, "}": -1}.get(char, 0)
if depth not in (0, 1):
return False
if char == "," and depth == 0:
items.append("".join(current).strip())
current = []
else:
current.append(char)
items.append("".join(current).strip())
if depth != 0:
return False
return all(
_flow_mapping_plain(item) if item[:1] == "{" else yaml_flow_plain(item) for item in items
)
def _flow_mapping_plain(item: str) -> bool:
if not (item[:1] == "{" and item[-1:] == "}"):
return False
inner = item[1:-1].strip()
if not inner:
return False
for entry in inner.split(","):
key, separator, leaf = entry.strip().partition(": ")
if not separator or not _FLOW_KEY.match(key) or not yaml_flow_plain(leaf.strip()):
return False
return True
def quote_scalar(value: str) -> str:
"""`value` as a double-quoted YAML scalar: `\\` and `"` escaped, nothing else.
Double and never single: over every bundle measured, 0 values carry a
surrounding `"` pair and 11 193 a surrounding `'` pair, so a reader that
unquotes `"` changes the meaning of no value already written.
"""
return '"' + value.replace("\\", "\\\\").replace('"', '\\"') + '"'
def unquote_scalar(value: str) -> str:
"""A surrounding `"` pair removed and `\\"` / `\\\\` decoded; else `value`.
The two escapes `quote_scalar` writes are the two decoded. Any other
backslash sequence (`\\n`, `\\t`, `\\x..`, `\\u....`) is kept as written:
a YAML reader would decode it and this reader does not claim to. A
single-quoted value is returned as it stands -- `'1'` stays `'1'`.
"""
if len(value) < 2 or value[0] != '"' or value[-1] != '"':
return value
inner = value[1:-1]
out: list[str] = []
index = 0
while index < len(inner):
char = inner[index]
if char == "\\" and inner[index + 1 : index + 2] in ('"', "\\"):
out.append(inner[index + 1])
index += 2
continue
out.append(char)
index += 1
return "".join(out)
def block_scalar(value: str) -> str:
"""`value` as written after `key: ` in a block mapping: plain where a YAML
reader returns it verbatim, double-quoted otherwise."""
return value if yaml_block_plain(value) else quote_scalar(value)
@dataclass(frozen=True)
class TypeRejection:
"""Why a profile refuses an `okf_type`, for the door to frame and raise.
The policy does not raise: Door A refuses with `ManifestError` and Door B
with `MaterializationError`, so the refusal has to be reported rather than
thrown. `reason` completes the sentence "<label>okf_type ..." and `code` is
the stable `IngestError.code` the caller asserts on.
"""
reason: str
code: str
@dataclass(frozen=True)
class TypePolicy:
"""Which `okf_type` values a bundle admits.
`allowed` is `None` for an open set — Phase 1/2 accept any type the
operator names — or a closed enum. Either way the reserved layer is
excluded, and a closed set that names it fails at construction.
"""
allowed: frozenset[str] | None = None
def __post_init__(self) -> None:
if self.allowed is None:
return
named = sorted(value for value in self.allowed if value.lower() == RESERVED_OKF_TYPE)
if named:
raise ValueError(
f"a profile must not admit the reserved {RESERVED_OKF_TYPE!r} layer "
f"(got {', '.join(repr(value) for value in named)}) — the promotion "
"gate is the only path into it (ingest-spec §3)"
)
def rejection(self, okf_type: str) -> TypeRejection | None:
"""The refusal for `okf_type`, or `None` when the profile admits it."""
if okf_type.lower() == RESERVED_OKF_TYPE:
return TypeRejection(
reason=f"must not be {RESERVED_OKF_TYPE!r} (reserved layer)",
code="okf_type_reserved",
)
if self.allowed is not None and okf_type not in self.allowed:
return TypeRejection(
reason=f"must be one of {', '.join(sorted(self.allowed))}, got {okf_type!r}",
code="okf_type_not_allowed",
)
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, 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
order spans both doors' key sets: Door A emits seven of these keys and Door
B six, and each door's subset comes out in exactly the order it wrote by
hand in Phases 1 and 2.
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.
`required_by_type` is the one judging field that keys off a frontmatter
*value* rather than a key: `{<type value>: {<keys that type must carry>}}`.
OKF v0.2 §10.2 introduces exactly one such rule — `runtime` is REQUIRED for
`Attested Computation` and for no other type — and it cannot be expressed
through `required`, which would demand the key of every document. A type
the mapping does not name carries no extra requirement, which is what keeps
the field inside §14: a consumer must not reject on an unknown `type`, so a
conditional keyed on a type we do not know stays silent rather than guesses.
"""
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
required_by_type: Mapping[str, frozenset[str]] = field(default_factory=dict)
def __post_init__(self) -> None:
conditional: frozenset[str] = frozenset().union(*self.required_by_type.values())
named = set(self.order) | set(self.required) | set(self.nullable) | conditional
if self.allowed is not None:
named |= set(self.allowed)
if _TIMESTAMP_FALLBACK_PAIR <= named:
raise ValueError(
"a profile must not name both 'timestamp' and 'generated' (OKF "
"§13.1 grants the timestamp fallback only while `generated` is "
"ABSENT, so a schema naming both can describe a document with "
"neither a valid `generated.at` nor an eligible fallback)"
)
if self.allowed is None:
return
for label, keys in (
("required", self.required),
("nullable", self.nullable),
("type-conditional required", conditional),
):
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"))
# The type-conditional rules (§10.2 today). Keyed off the VALUE, so it
# fires only on a type this schema names: an absent, non-scalar or
# unknown `type` carries no extra requirement. A separate code from the
# unconditional case because the two are different claims — one says the
# document is malformed, the other that it is malformed *for what it
# says it is* — and a caller may well treat them differently.
declared = values.get("type")
if isinstance(declared, str):
for key in self.required_by_type.get(declared, frozenset()) - set(values):
found.append(
SchemaViolation(
key,
f"is required for type {declared!r} and absent",
"frontmatter_key_missing_for_type",
)
)
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.
Commons decision D1: the keys named in `order` come first, in that
order, followed by any remaining keys SORTED. Ordering the tail rather
than trusting insertion order is what makes a regeneration over the
same data byte-identical.
A scalar a YAML reader would refuse or misread as plain is written
double-quoted (K3-22); every other value keeps its bytes. A flow
collection is written as it stands -- its leaves are validated where it
is built -- and so is an empty value.
Returns the lines only — the caller owns the `---` fences.
"""
named = [key for key in self.order if key in values]
tail = sorted(key for key in values if key not in self.order)
return "\n".join(
f"{key}: {_emitted(self._render(key, values[key]))}" for key in [*named, *tail]
)
def _render(self, key: str, value: str) -> str:
# §5 mandates whitespace-run collapse for `source_query` only
# (ingest-spec.md:140-141), where a legitimately multi-line SQL SELECT
# must render on one line. Every other value is validated single-line
# at load and emitted verbatim — validation, not repair — so
# operator-supplied bytes (e.g. a title's internal double space)
# survive.
return " ".join(value.split()) if key in self.collapsed_keys else value
def _emitted(value: str) -> str:
if not value or yaml_flow_collection(value):
return value
return block_scalar(value)
# The v0.1 ingest stamp. A literal rather than a configurable value: it is what
# every bundle this library has already written carries, and recognising it is
# what keeps those bundles re-runnable under a later profile.
_V0_1_STAMP = "true"
@dataclass(frozen=True)
class OwnershipPolicy:
"""The `generated` value this profile writes, and the values it owns back.
Ownership is the §3 collision gate's question — may this run replace the
file already sitting at a generated name? — and the answer is a profile's,
because the stamp differs per profile. The literal `true` is the older
form; the O2 form is `generated: { by: <actor>, at: <ingested_at> }` (§5),
where the actor takes §7's `process:<id>` form.
Which form a profile writes is NOT read off an upstream version. `DEFAULT`
states commons' ingest-spec layer and writes the O2 form because commons
ratified it (V1, `54e0ec7`), while remaining v0.1 on every axis upstream
owns; `STRICT_V1` names no `generated` at all. The stamp tracks whichever
contract the profile states, and those contracts move independently.
`actor` is `None` for the literal stamp. Where it is set it carries no version,
deliberately: the value sits inside a byte-compared golden, so a producer
version there would fire golden regression on every release without any
contract having changed, and would make a shared cross-implementation
fixture impossible by construction (plan V1(d), operator 2026-07-27).
Recognition is ONE-WAY, and both directions are decisions rather than
accidents. A profile with an actor owns the literal stamp as well, so a
bundle written before V1 re-runs IN PLACE — the black-box promise is that a
spec release costs a consumer a re-run and nothing more. The reverse is
refused: a profile without an actor fails the run rather than replacing a
file whose shape it does not read (V-A3).
"Owns the O2 form" is never "owns any mapping". The prefix binds the
profile's OWN actor, so a mapping naming a different one — another
implementation's, or §7's `human:` actor on curated content — stays
unowned. That is what keeps the key's mere presence from proving authorship,
which upstream v0.2 makes load-bearing by writing `generated` on
hand-authored files too.
The actor test is a PREFIX rather than an equality, because the value carries
`ingested_at` and therefore differs on every run by design. It works because
`parse_frontmatter` returns the whole flow mapping as one opaque string
(V-A2) — no structure this library cannot yet read is parsed here.
"""
actor: str | None = None
def stamp(self, ingested_at: str) -> str:
"""The `generated` value a run at `ingested_at` writes."""
if self.actor is None:
return _V0_1_STAMP
return f"{{ by: {self.actor}, at: {ingested_at} }}"
def owns(self, value: str) -> bool:
"""Whether a `generated` value read back marks this library's output."""
if value == _V0_1_STAMP:
return True
if self.actor is None:
return False
return value.startswith(f"{{ by: {self.actor},")
@dataclass(frozen=True)
class PathPolicy:
"""The filename namespaces the three doors write into.
Each prefix keeps its door's generated names disjoint from `index.md`, from
the other doors, and from `promoted-verdict-*`, for every id the grammar
admits. The id grammar itself stays in `materialize`: it is a pattern the
slugger derives a separator class from, not a name to configure.
"""
concept_suffix: str
ingest_prefix: str
inbox_prefix: str
import_prefix: str
def _split_frontmatter(text: str) -> tuple[dict[str, str], list[str]]:
"""The leading `---` block as ordered keys, and the body lines after it.
Line-oriented, the same shape `materialize.parse_frontmatter` reads —
duplicated rather than imported because `materialize` imports this module,
and because that one takes a path where an index reader has only text.
"""
lines = text.splitlines()
if not lines or lines[0].strip() != "---":
return {}, lines
head: dict[str, str] = {}
for offset, line in enumerate(lines[1:], start=1):
if line.strip() == "---":
return head, lines[offset + 1 :]
# An INDENTED key belongs to the block above it, not to the document.
# Without this, `key.strip()` would flatten it into the same namespace
# as the top-level keys and, arriving later, SUBSTITUTE for one of them
# -- a `sources:` entry's own `title:` silently becoming the document's,
# carrying `number` and `parent` with it. Skipping is deliberately not
# parsing: the nested value is not read, only refused. The structured
# reader is D1b.
if line[:1] in (" ", "\t"):
continue
key, sep, value = line.partition(":")
if sep:
head[key.strip()] = unquote_scalar(value.strip())
return head, []
@dataclass(frozen=True)
class FacetPolicy:
"""The metadata an index ENTRY carries beside its label and target.
A flat index is a table of contents. A faceted one is a table a consumer
can reason over without opening a single document - which is the whole
difference the 2026-08-26 bake-off measured: every arm retrieved 40/40, and
the only axis that separated them was trap exposure, where the flat index
lost precisely because it carried title/date/status/supersedes 0 times
while its own documents carried them 55/55/55/5.
`keys` is the ordered, closed set. Ordering by the POLICY rather than by
the caller's mapping is what keeps two callers passing the same facts from
emitting different bytes, exactly as the root frontmatter does; closing the
set is what keeps a value out of a file no reader of this contract looks
at.
The grammar is deliberately thin - `separator` once, then `key: value`
joined by `joiner` - because an index line is read by regex on both sides
of this library. A value carrying either delimiter is REFUSED rather than
escaped or repaired: escaping would make the line unreadable to a consumer
that splits naively, and this library validates rather than repairs
everywhere else.
"""
keys: tuple[str, ...]
separator: str = " \u2014 "
joiner: str = "; "
def __post_init__(self) -> None:
if not self.keys:
raise ValueError(
"a facet policy must name at least one key - a policy naming "
"none would render a separator with nothing after it"
)
def render(self, values: Mapping[str, str]) -> str:
"""The facet tail for `values`, or "" when none of them are present."""
unknown = sorted(set(values) - set(self.keys))
if unknown:
raise ValueError(
f"facet key(s) {', '.join(repr(key) for key in unknown)} are not named "
f"by this index policy, which pins {self.keys} - rendering an unnamed "
"key would put a value in a file no reader of this contract looks at"
)
present = [(key, values[key]) for key in self.keys if values.get(key)]
for key, value in present:
if "\n" in value or "\r" in value:
raise ValueError(f"facet {key!r} must be single-line, got {value!r}")
for label, delimiter in (("separator", self.separator), ("joiner", self.joiner)):
if delimiter in value:
raise ValueError(
f"facet {key!r} contains this policy's {label} {delimiter!r} "
f"({value!r}) - refusing to escape or repair it, which would "
"make the line parse one way here and another way downstream"
)
if not present:
return ""
return self.separator + self.joiner.join(f"{key}: {value}" for key, value in present)
def parse(self, tail: str) -> dict[str, str]:
"""The facet tail read back. The inverse of `render` by construction."""
parsed: dict[str, str] = {}
for chunk in tail.split(self.joiner):
key, sep, value = chunk.partition(":")
if sep:
parsed[key.strip()] = value.strip()
return parsed
#: The closed set `IndexPolicy.sort_order` draws from. A CALLER-SUPPLIED
#: CALLABLE is deliberately not an option here: it cannot be serialised into
#: the bundle, cannot be reproduced from it, and cannot be audited by anyone
#: reading it back — which is the whole of what a deterministic bundle claims.
SORT_ASCENDING = "ascending"
SORT_DESCENDING = "descending"
SORT_ORDERS = (SORT_ASCENDING, SORT_DESCENDING)
#: The closed set `IndexPolicy.sort_missing` draws from: where the entries that
#: do not carry the key at all are put. Closed for the same reason.
SORT_MISSING_FIRST = "first"
SORT_MISSING_LAST = "last"
SORT_MISSING = (SORT_MISSING_FIRST, SORT_MISSING_LAST)
@dataclass(frozen=True)
class IndexEntry:
"""One managed index line, read back into its parts.
`concept_path` is the bundle-relative path of the concept the entry is
ABOUT, and it is the ordering tie-break. It is not part of the rendered
line and is therefore only ever populated on the write path — `parse_entry`
leaves it `None`, and the ordering then falls back to the link target.
That costs nothing today because no caller sorts entries it read back off
disk; every ordering happens where the entry is being built.
It exists because the two doors disagree about what the target IS. Door B's
target is the concept's own name, but Door C reduces a sender's concept
path to a generated filename, and the two do not order alike:
`notes-beta.md` precedes `notes/alpha.md` by concept path and follows it by
generated name. Ordering on the target would silently re-order every
existing Door C bundle.
"""
label: str
target: str
description: str | None = None
facets: Mapping[str, str] = field(default_factory=dict)
concept_path: str | None = None
@dataclass(frozen=True)
class IndexViolation:
"""One way an index file departs from the policy.
Reported rather than raised, like `SchemaViolation`. `subject` is whatever
the finding is about — a line, a link target, or a frontmatter key — and
`code` is the stable machine-readable part.
"""
subject: str
reason: str
code: str
@dataclass(frozen=True)
class IndexPolicy:
"""The index file, the shape of its entries, and where indexes must exist.
`link_template` renders a managed line and `link_pattern` recognises one.
Both are carried because §6 does both — append on write, rewrite on
maintenance — and a round-trip test is what keeps the pair honest. The
pattern is anchored to the whole line by construction: removal keys on this
exact shape, never a bare substring, so curated prose that mentions a
target inline survives verbatim.
The judging fields are read by `violations` and `required_indexes`, and
every one of them is off under DEFAULT. They exist because two consumers
disagree about this file in ways no single shape can hold:
- `per_directory` — an index at every level, or only at the bundle root.
Upstream OKF binds `index.md` to the root ALONE, so a default demanding
one per level would declare upstream-conforming bundles invalid. Policy,
never an OKF rule.
- `heading_required` / `allows_prose` — the wiki's index is a generated
heading plus entries and nothing else; the catalog's *requires*
progressive-disclosure prose. Opposite requirements, both expressible.
- `entries_match_directory` — whether the index must match the directory
exactly, in both directions. The wiki enforces this at ERROR
(`BUNDLE_INDEX_COMPLETE`); the catalog holds that an index is an
AUTHORED count of a directory's children and that a validator
enumerating the directory has implemented the wrong contract. Neither
posture is baked in, and this library never enumerates anything: the
caller supplies the listing, and a policy that does not judge it refuses
to be handed one at all — so code written to the wrong contract fails at
the call rather than passing every test one would think to write.
- `root_frontmatter` — the ordered keys the ROOT index may carry, where
nested indexes carry none. Confirmed independently in both consumers, so
the asymmetry is shape rather than one repo's preference. Naming a key
here PERMITS it and fixes its position; it does not demand it.
- `root_frontmatter_required` — the subset that must actually be present.
Separate from the tuple above because the two are different claims, and
collapsing them was P1-F1: `OKF_V0_2` names `okf_version` to fix its
position, but upstream §8/§12 grant it as a MAY, and judging the pair as
one field failed 14 of 17 real bundles — upstream's own four included —
each with exactly this one violation, while our emitter treated the same
key as optional. A profile whose consumer really does demand the keys
says so here, which is what `STRICT_V1` does.
"""
name: str
link_template: str
link_pattern: re.Pattern[str]
per_directory: bool = False
heading_required: bool = False
allows_prose: bool = True
entries_match_directory: bool = False
root_frontmatter: tuple[str, ...] = ()
root_frontmatter_required: frozenset[str] = field(default_factory=frozenset)
facets: FacetPolicy | None = None
#: Row forms this policy READS but never writes. Consulted by `parse_entry`
#: only, after `entry_pattern` misses, so no emitted byte can move: every
#: line this library writes still comes from `link_template`.
#:
#: WHY IT EXISTS (vegnormal-okf, FUNN 1/2, 2026-09-08). OKF SPEC section 8
#: shows `* [Title](file.md) - description` in its own example and Google's
#: generator writes it, while this library's segmented profiles write
#: `- [Title](file.md)`. Measured, the star row parsed as `None` -- curated
#: prose -- so the section 9.2 index walk could not reach a single concept
#: behind one. A bundle we cannot walk is the silent loss the "arbitrary
#: bundle" direction forbids.
#:
#: Reading a form is NOT a licence to emit it. That asymmetry is this
#: repository's existing posture, not a new one: `sources` is read in both
#: YAML forms and written in one, for the same reason -- the emitted shape
#: is what our own parser must round-trip.
#:
#: Every member must be anchored at both ends, for the reason
#: `_faceted_pattern` states: an unanchored alternative would match a
#: target mentioned inside curated prose.
also_reads: tuple[re.Pattern[str], ...] = ()
sort_key: str | None = None
sort_order: str = SORT_ASCENDING
sort_missing: str = SORT_MISSING_LAST
_faceted_pattern: re.Pattern[str] | None = field(
init=False, repr=False, compare=False, default=None
)
def __post_init__(self) -> None:
if self.facets is not None:
# The faceted pattern is BUILT from the base one, so an unanchored
# base would silently produce an unanchored faceted pattern - and
# index maintenance keys on this pattern to decide which lines it
# may rewrite. A substring match there edits curated prose.
if not self.link_pattern.pattern.endswith("$"):
raise ValueError(
"a link pattern carrying facets must be anchored to the end "
"of the line ('$'), because the faceted pattern is derived "
"from it and an unanchored match would rewrite curated prose"
)
object.__setattr__(
self,
"_faceted_pattern",
re.compile(
self.link_pattern.pattern[:-1]
+ f"(?:{re.escape(self.facets.separator)}(?P<facets>.+))?$"
),
)
for extra in self.also_reads:
if not extra.pattern.startswith("^") or not extra.pattern.endswith("$"):
raise ValueError(
f"an also-read pattern must be anchored at both ends, got "
f"{extra.pattern!r} — an unanchored alternative would read a "
"target mentioned inside curated prose as an entry"
)
stray = sorted(self.root_frontmatter_required - set(self.root_frontmatter))
if stray:
raise ValueError(
f"required root keys must be named in the ordered set "
f"(got {', '.join(repr(key) for key in stray)}) — a key demanded "
"but never named could not be judged for position"
)
if self.sort_order not in SORT_ORDERS:
raise ValueError(
f"sort_order must be one of {SORT_ORDERS}, got {self.sort_order!r}"
"the set is closed so that an index order is reproducible from the "
"bundle rather than from the caller that happened to write it"
)
if self.sort_missing not in SORT_MISSING:
raise ValueError(
f"sort_missing must be one of {SORT_MISSING}, got {self.sort_missing!r}"
)
if self.sort_key is not None and (
self.facets is None or self.sort_key not in self.facets.keys
):
named = self.facets.keys if self.facets is not None else ()
raise ValueError(
f"sort_key {self.sort_key!r} is not named by this index policy's facets, "
f"which pin {named or '()'} — every entry would be missing the key and "
"the ordering would silently do nothing"
)
if self.requires_description and "description" not in self.link_pattern.groupindex:
raise ValueError(
"link_template names {description} but link_pattern has no "
"'description' group — the pair would round-trip in one "
"direction only"
)
@property
def requires_description(self) -> bool:
"""Whether an entry carries a description alongside label and target."""
return "{description}" in self.link_template
@property
def entry_pattern(self) -> re.Pattern[str]:
"""The pattern that recognises a managed line, facets included.
IS `link_pattern` when this policy carries no facets, which is what
makes the feature additive: DEFAULT and STRICT_V1 match exactly the
lines they always matched, byte for byte.
"""
return self._faceted_pattern if self._faceted_pattern is not None else self.link_pattern
def render_link(
self,
label: str,
target: str,
description: str | None = None,
*,
facets: Mapping[str, str] | None = None,
) -> str:
if self.requires_description and description is None:
raise ValueError(
"this index policy's entries carry a description; rendering "
"without one emits a half-written entry that parses as nothing"
)
if not self.requires_description and description is not None:
raise ValueError(
"this index policy's entries carry no description; the value "
"offered would be dropped silently"
)
if self.facets is None and facets:
raise ValueError(
"this index policy carries no facets; the values offered would be dropped silently"
)
line = self.link_template.format(label=label, target=target, description=description)
if self.facets is None or facets is None:
return line
return line + self.facets.render(facets)
def parse_entry(self, line: str) -> IndexEntry | None:
"""One managed line read back into its parts, or `None` for anything else.
Anything this returns `None` for is curated content and survives
verbatim: the index is the one file where this library writes beside
somebody else's prose.
`also_reads` is tried only after the emitted form misses, and only
here: index MAINTENANCE keys on `entry_pattern` alone, so a foreign row
this reads is never a row this rewrites.
"""
stripped = line.rstrip("\r\n")
match = self.entry_pattern.match(stripped)
if match is None:
for extra in self.also_reads:
match = extra.match(stripped)
if match is not None:
break
if match is None:
return None
groups = match.groupdict()
tail = groups.get("facets")
return IndexEntry(
label=match.group("label"),
target=match.group("target"),
description=groups.get("description"),
facets=self.facets.parse(tail) if (self.facets is not None and tail) else {},
)
def sort_entries(self, entries: Iterable[IndexEntry]) -> tuple[IndexEntry, ...]:
"""The ONE ordering every index this library writes goes through.
Both doors call this. Not because sharing is tidy, but because the
alternative was measured: with Door B and Door C writing their indexes
through separate code, an ordering wired into one of them is a profile
field the other ignores in silence — nothing raises, and both files
still parse. Two implementations of one ordering ARE the drift.
Four passes, each a stable sort, so an earlier pass is exactly the
tie-break of a later one:
1. by concept path, which is the final tie-break and makes the order
total — two entries sharing a key never fall back to chance;
2. by the named key, reversed for `descending`;
3. the entries missing the key partitioned to whichever end
`sort_missing` says. Separate from pass 2 on purpose: folding the two
into one reversible key tuple would flip the missing group along with
the order, so `sort_missing="last"` would mean "first" under
`descending`;
4. navigation last. An outer GROUPING rather than a competitor to the
key: a link down to a child index is about a directory, carries no
facets, and would lead the file under `sort_missing="first"`.
A value that is present but empty counts as missing, because
`FacetPolicy.render` already drops it — an entry that renders without
the facet must not sort as though it carried one.
With no `sort_key` the whole of passes 2 and 3 is skipped and the result
is concepts before navigation, each group ascending by concept path.
That is what every profile shipped today already emitted.
"""
ordered = sorted(entries, key=lambda entry: entry.concept_path or entry.target)
if self.sort_key is not None:
key = self.sort_key
ordered = sorted(
ordered,
key=lambda entry: entry.facets.get(key) or "",
reverse=self.sort_order == SORT_DESCENDING,
)
missing = [entry for entry in ordered if not entry.facets.get(key)]
if missing:
present = [entry for entry in ordered if entry.facets.get(key)]
ordered = (
[*missing, *present]
if self.sort_missing == SORT_MISSING_FIRST
else [*present, *missing]
)
return tuple(
sorted(ordered, key=lambda entry: entry.target.rpartition("/")[2] == self.name)
)
def required_indexes(self, directories: Sequence[str]) -> tuple[str, ...]:
"""The index paths this policy requires, given the caller's directories.
Bundle-relative, with `""` for the root. The directories are an
argument precisely so that nothing here reaches the filesystem.
"""
if not self.per_directory:
return (self.name,)
return tuple(sorted(f"{d}/{self.name}" if d else self.name for d in directories))
def violations(
self,
text: str,
*,
is_root: bool = False,
expected_targets: Collection[str] | None = None,
) -> tuple[IndexViolation, ...]:
"""Every way `text` departs from this policy, deterministically ordered.
`expected_targets` is the directory's contents as the CALLER sees them,
required exactly when `entries_match_directory` is set and refused
otherwise. Sorted by `(subject, code)` so two runs over the same file
agree.
"""
if self.entries_match_directory and expected_targets is None:
raise ValueError(
"this index policy judges the index against the directory, so "
"a listing is required — skipping the check silently would "
"pass a gate whose input never arrived"
)
if not self.entries_match_directory and expected_targets is not None:
raise ValueError(
"this index policy's index is authored rather than derived "
"from the directory, so a listing has nothing to judge"
)
found: list[IndexViolation] = []
head, body = _split_frontmatter(text)
found.extend(self._frontmatter_violations(head, is_root=is_root))
headings: list[str] = []
listed: set[str] = set()
for line in body:
stripped = line.strip()
if not stripped:
continue
if line.startswith("# "):
headings.append(line)
continue
match = self.entry_pattern.match(line)
if match is not None:
listed.add(match.group("target"))
continue
if not self.allows_prose:
found.append(
IndexViolation(
stripped,
"is neither the heading nor an entry",
"index_prose_not_allowed",
)
)
if self.heading_required:
if not headings:
found.append(
IndexViolation("", "the index carries no `# ` heading", "index_heading_missing")
)
found.extend(
IndexViolation(extra, "is a second `# ` heading", "index_heading_extra")
for extra in headings[1:]
)
if expected_targets is not None:
expected = set(expected_targets)
found.extend(
IndexViolation(
target, "is in the directory but not in the index", "index_entry_missing"
)
for target in expected - listed
)
found.extend(
IndexViolation(
target, "is in the index but not in the directory", "index_entry_unexpected"
)
for target in listed - expected
)
return tuple(sorted(found, key=lambda violation: (violation.subject, violation.code)))
def _frontmatter_violations(
self, head: Mapping[str, str], *, is_root: bool
) -> list[IndexViolation]:
if not self.root_frontmatter:
return []
if not is_root:
# One finding about the block, not one per key: a nested index
# carrying frontmatter is a single structural fact.
first = next(iter(head), None)
if first is None:
return []
return [
IndexViolation(
first,
"is frontmatter on a nested index, which carries none",
"index_root_frontmatter_unexpected",
)
]
found = [
IndexViolation(key, "is pinned on the root index and absent", "index_root_key_missing")
for key in self.root_frontmatter
if key in self.root_frontmatter_required and key not in head
]
present = [key for key in head if key in self.root_frontmatter]
declared = [key for key in self.root_frontmatter if key in head]
if present != declared:
found.append(
IndexViolation(
present[0],
f"breaks the pinned key order {', '.join(self.root_frontmatter)}",
"index_root_key_order",
)
)
return found
@dataclass(frozen=True)
class SegmentationPolicy:
"""Whether a bundle admits one document expanding into many concepts.
OKF v0.2 §2 calls a concept "a single unit of knowledge within a bundle"
and a concept ID "the path of the concept's file within the bundle"
neither ties a concept to a source file. A door emitting exactly one flat
concept per dropped file therefore implements the shape Appendix A
presents v0.2 as migrating AWAY from, and §11 cannot notice: it checks
that every non-reserved `.md` parses with a non-empty `type`, so one giant
concept is fully conformant. Conformance is the floor, not the proof.
The presence of this object IS the capability. Every downstream branch
reads `profile.segmentation is not None` and never
`IndexPolicy.per_directory`: `STRICT_V1` already sets that field True
while Door B ignores it, so keying the 1-to-N path there would silently
change a shipped profile's output and break its byte-stability pin.
Every field NAMES a key and none supplies a value. The value of
`bundle_id` tracks the caller's own identity scheme, so it arrives through
`root_frontmatter_values` (decision D5): a constant here would claim a
decision this library does not own.
"""
hierarchical_paths: bool = True
bundle_id_key: str = "bundle_id"
segment_id_key: str = "segment_id"
offset_key: str = "source_offset"
nav_label: str = "index"
# The discriminator BETWEEN segmented profiles, and the reason it has to be
# a field with a value rather than a presence check: every 1-to-N branch
# keys on `profile.segmentation is not None`, which BOTH segmented profiles
# satisfy. A step that surfaced the adjudication state on that check would
# write it into `SEGMENTED_V1` as well and move a byte-pinned golden.
#
# `None` means this profile does not surface adjudication state at all,
# matching the shape used everywhere else here. When set, it NAMES the
# frontmatter key and index facet; the value written under it is the
# adjudicator's, never this module's.
adjudication_key: str | None = None
@dataclass(frozen=True)
class ProvenancePolicy:
"""Whether a concept carries an address back to the document it came from.
Two layers, and the split is load-bearing rather than tidy.
The ADDRESS is SPEC's. §5.1:303-306 makes `sources[].resource` REQUIRED
within an entry and lets it be "an absolute URL, a bundle-relative path, or
a path into a `references/` subdirectory (§6)" -- which is exactly what a
dropped file's inbox-relative path is. No new key is invented where the
spec already has one.
The LOCATOR is OURS, and it has to be. §5.1 has no field for a page, a
sheet row or a line, and the guard's frontmatter grammar (1.3.0, measured)
refuses every route to putting one inside a `sources` entry: a key outside
its `sources` allowlist is rejected by name, and a nested flow list is
rejected as "a flow mapping admits scalar leaves only". So a locator inside
the entry would be a bundle we emit and could never read back through Door
C. Top-level keys, in the shape `source_offset` already uses.
Every field NAMES a key and none supplies a value, like every other policy
here. The presence of this object IS the capability: a profile that names
no provenance writes none, which is what keeps the five shipped profiles
that do not name it byte-identical.
"""
sources_key: str = "sources"
pages_key: str = "source_pages"
sheet_key: str = "source_sheet"
rows_key: str = "source_rows"
lines_key: str = "source_lines"
@dataclass(frozen=True)
class BundleProfile:
"""One bundle contract: types, frontmatter, filenames, index."""
types: TypePolicy
frontmatter: FrontmatterSchema
paths: PathPolicy
index: IndexPolicy
ownership: OwnershipPolicy = field(default_factory=OwnershipPolicy)
# Defaulted, so all four existing constants construct unchanged and every
# positional call site stays source-compatible. `None` is not "segmentation
# off" as a setting — it is the profile not having the capability at all,
# which is what the downstream `is not None` checks read.
segmentation: SegmentationPolicy | None = None
# Arm E, capability only: a profile MAY name a renderer per suffix, applied
# to extracted text before it becomes a concept body. `None` reads the same
# way `segmentation` does -- the profile does not have the capability, not
# "the capability is switched off". No domain-aware renderer exists in this
# package; writing one is a Non-Goal and is named here as unassigned so the
# absence is deliberate rather than an oversight.
renderers: Mapping[str, str] | None = None
# Defaulted to `None` for the same reason `segmentation` is: `None` is not
# "provenance off", it is the profile not having the capability, which is
# what the door's `is not None` check reads. Five of the six shipped
# profiles leave it unset and keep their bytes.
provenance: ProvenancePolicy | None = None
# The ingest-spec + Phase 2 contract. Every value here was a constant in
# `manifest`, `materialize`, `inbox` or `importer` before this module existed;
# the golden suite is what proves that move changed no bytes.
#
# This profile tracks COMMONS' spec, not an upstream Google version — the two
# axes are independent, and `ownership` is where they visibly part company. Its
# `generated` is the O2 mapping because commons ratified that shape for their
# §5 layer, while the profile remains v0.1 on every axis upstream owns.
DEFAULT = BundleProfile(
types=TypePolicy(allowed=None),
frontmatter=FrontmatterSchema(
# Door A's seven keys and Door B's six, merged into one order that
# contains both as subsequences — neither door's output moves.
order=(
"type",
"title",
"source_system",
"source_query",
"source_file",
"source_sha256",
"ingested_at",
"ingest_manifest",
"generated",
),
collapsed_keys=frozenset({"source_query"}),
),
paths=PathPolicy(
concept_suffix=".md",
ingest_prefix="ingest-",
inbox_prefix="inbox-",
import_prefix="import-",
),
index=IndexPolicy(
name="index.md",
link_template="- [{label}]({target})",
link_pattern=re.compile(r"^- \[(?P<label>[^\]]*)\]\((?P<target>[^)]+)\)$"),
),
# V1, ratified 2026-08-02 and executed by commons 2026-08-09 (`54e0ec7`).
# The actor is the same constant `OKF_V0_2` carries, and that is commons'
# doing rather than a merge of the two profiles: ingest-spec §7 names
# `process:okf-ingest` as THE ingest actor, so any profile stating that
# spec's layer writes it. The profiles still differ everywhere else.
ownership=OwnershipPolicy(actor="process:okf-ingest"),
)
# `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` is DEFAULT's and that is the whole truth for it: the filename
# namespaces are an ingest-spec invariant about what the doors write, not a
# consumer preference.
#
# The index shape is read out of their code rather than recalled:
# `bundle.py:498-524` renders `* [Title](target) - description` with
# subdirectories linking to their own index; `bundle.py:527-567` walks every
# level, writing frontmatter at the bundle root only and a bare `# ` heading
# below it; `validate.py:1081-1120` (gate BUNDLE_INDEX_COMPLETE, ERROR) demands
# the index and the directory match exactly in both directions.
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=IndexPolicy(
name="index.md",
link_template="* [{label}]({target}) - {description}",
link_pattern=re.compile(
r"^\* \[(?P<label>[^\]]*)\]\((?P<target>[^)\s]+)\) - (?P<description>.+)$"
),
per_directory=True,
heading_required=True,
allows_prose=False,
entries_match_directory=True,
root_frontmatter=("okf_version", "bundle_profile", "okf_spec_commit"),
# The proving consumer demands all three, not merely permits them: their
# root index carries exactly these keys in exactly this order on every
# bundle measured (`c5141f8`). Stated explicitly so that separating
# "permitted" from "required" costs them nothing.
root_frontmatter_required=frozenset({"okf_version", "bundle_profile", "okf_spec_commit"}),
),
)
# The ordered structure keys: what `structure.py` derives, plus the two
# producer-declared keys the 2026-08-26 bake-off measured missing from the
# index (`status` 55/55 in the documents, 0/55 in the index; `date` likewise).
# One tuple feeds both the frontmatter order and the facet set, because two
# lists of the same keys drift.
_STRUCTURE_KEYS = (
"number",
"parent",
"status",
"date",
"version",
"supersedes",
"references",
# LAST, and load-bearing: it names which of the keys before it this library
# INFERRED rather than read. A consumer that trusts nothing derived can
# still use everything else, and one that accepts both knows which half it
# is betting on. An unmarked heuristic is worse than no heuristic.
"derived",
)
# DEFAULT plus structure. Additive in the strict sense: the namespaces, the
# type policy and the ownership stamp are DEFAULT's own objects, so a bundle
# written under either profile stays re-runnable under the other, and DEFAULT's
# bytes do not move.
#
# Why a new profile rather than facets on DEFAULT: DEFAULT states commons'
# ingest-spec 6 index layer. Changing its rendered bytes from here would be
# this repo editing another repo's contract (O2), and it would churn every
# golden fixture that door has ever written. The measured defect is real, but
# the fix belongs beside the contract it changes, not inside one we do not own.
#
# The cost is deliberate and small. Facets are rendered only where a value
# exists, so a bundle whose documents declare nothing pays nothing, and the
# 2026-08-26 arm that lost on trap exposure was 6 031 characters against
# 21 879 for the head-scan it lost to - the headroom for carrying the metadata
# back into the index is most of that gap.
STRUCTURED_V1 = BundleProfile(
types=DEFAULT.types,
frontmatter=FrontmatterSchema(
order=(*DEFAULT.frontmatter.order, *_STRUCTURE_KEYS),
collapsed_keys=DEFAULT.frontmatter.collapsed_keys,
),
paths=DEFAULT.paths,
index=replace(DEFAULT.index, facets=FacetPolicy(keys=_STRUCTURE_KEYS)),
ownership=DEFAULT.ownership,
)
# OKF v0.2, as an ADDITIVE profile: `DEFAULT` states commons' ingest-spec §5
# layer and keeps stating it, so nothing here migrates anything. The key order
# is DEFAULT's followed by the §5 families v0.2 adds, which is also the order
# upstream's own reference bundles emit them in (`generated` before `sources`).
#
# Naming a family is not writing it. `verified`, `status` and `stale_after` are
# expressible so a caller can emit them in canonical order and so the schema can
# judge a document that carries them; Door A writes none of them, because a
# field with no reader is not written. Their structured v0.2 values — block
# lists of mappings — are beyond this library's value model until D1's reader
# lands; that is a reader gap, not an ordering one.
# §10 `Attested Computation`: the type's contract fields, in §10.2's own listing
# order. FORMAT only — this library supports writing, judging and round-tripping
# the contract, and implements no execution: upstream defers the receipt and
# verdict wire formats, so there is nothing to build a runtime against, and
# "did this run produce the value the sanctioned way" sits on the far side of
# this library's boundary in any case.
ATTESTED_COMPUTATION_TYPE = "Attested Computation"
_ATTESTED_COMPUTATION_FIELDS = ("runtime", "parameters", "computation", "executor", "attester")
_OKF_V0_2_KEY_ORDER = (
*DEFAULT.frontmatter.order,
"sources",
"verified",
"status",
"stale_after",
# §10.2's contract fields, appended as one block and internally in the order
# that section enumerates them. Appended rather than interleaved because the
# keys before them are what the doors actually emit, and because `emit`
# writes only the keys present — so naming these costs no byte in any bundle
# that carries none of them. Without the block they would still be emitted,
# in `emit`'s sorted tail, where `attester` precedes `runtime`: alphabetical
# order standing in for the contract's own.
*_ATTESTED_COMPUTATION_FIELDS,
)
# PROVISIONAL. Shipped first as a pre-release (`v0.5.0a1`) to a named pilot set
# — `portfolio-optimiser-claude`, the plugin marketplace catalog, and
# `claude-code-llm-wiki` — and this surface may change on their feedback without
# a deprecation cycle. Saying so is what buys the freedom to act on the
# feedback; discovering it later is what would make the pilot a de-facto
# release. The versioned constants are the stable binding.
#
# Two things this profile deliberately does NOT do:
#
# - **It closes nothing.** §14 forbids a conformant consumer to reject on an
# unknown `type` value or on unknown additional keys, so an allowlist or a key
# pattern here would put the profile in violation of the version it is named
# for. `type` is required and is the only one (§4, §11).
# - **It NAMES `okf_version` but 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. The caller supplies
# it (`materialize_bundle(..., root_frontmatter_values=...)`); this policy
# fixes only the key and its position.
#
# WHERE it goes was open until 2026-07-31 between upstream's root-index
# frontmatter block and catalog's body-line convention. Catalog verified
# upstream themselves at the pinned commit `3fcbb9f` and reported §8:509-510
# ("Index files contain no frontmatter, with one exception: a bundle-root
# `index.md` MAY carry an `okf_version` key") and §12:773-775 ("in a
# bundle-root `index.md` frontmatter block (the only place frontmatter is
# permitted in an `index.md`)"). Frontmatter it is; their own spec diverges
# from upstream here, and that divergence is theirs to resolve.
#
# Declaring it stays a MAY: none of upstream's four reference bundles carries
# the key at all (catalog grepped `okf/bundles` and `okf/samples`: zero hits),
# so omitting `root_frontmatter_values` emits no block. The JUDGE says the same
# thing since P1-F1 — the key is named here and left out of
# `root_frontmatter_required` — because for one release it did not, and a
# bundle exercising the MAY was reported as violating by the very profile that
# had emitted it correctly.
#
# **Measured limitation (guard 0.2.0, 2026-07-26):** a bundle emitted under this
# profile cannot be read back through a guard-gated import. The guard's T2
# frontmatter grammar admits scalars and flat lists of strings, and refuses every
# route to a mapping — flow on the disallowed-indicator set, block on the
# nested-mapping check, dotted keys on the key pattern. So `generated` as the
# mapping v0.2 specifies has no expressible form through that gate at all. This
# binds what can be IMPORTED (Door C), never what we emit: Door B's
# `screen_output` does not run that parser.
OKF_V0_2 = BundleProfile(
types=TypePolicy(allowed=None),
frontmatter=FrontmatterSchema(
order=_OKF_V0_2_KEY_ORDER,
collapsed_keys=DEFAULT.frontmatter.collapsed_keys,
required=frozenset({"type"}),
# §10.2's one type-conditional rule, and the whole of it: `runtime` is
# REQUIRED for this type because it is what says how to run the
# computation and therefore what `parameters` mean. The other four
# contract fields stay optional — `computation` absent means the body
# fence IS the computation (§10.3), which is a valid concept.
required_by_type={ATTESTED_COMPUTATION_TYPE: frozenset({"runtime"})},
),
paths=DEFAULT.paths,
# DEFAULT's index in every respect but one: the root MAY carry `okf_version`
# (§8, §12). Built with `replace` rather than restated so a later change to
# the shared shape cannot drift between the two.
index=replace(DEFAULT.index, root_frontmatter=("okf_version",)),
# Byte-identical to `DEFAULT.ownership` since V1, and deliberately NOT
# written as a reference to it. The two agree by coincidence of commons
# adopting §7's actor, not by dependency: this profile states UPSTREAM's
# v0.2, so if commons ever moves their actor again, this one must not
# follow. Restating it is what keeps that independence expressible.
ownership=OwnershipPolicy(actor="process:okf-ingest"),
)
# STRUCTURED_V1 plus the capability to split ONE document into MANY concepts.
#
# A new profile rather than a flag on an existing one, for the same reason
# STRUCTURED_V1 was: `DEFAULT` states commons' ingest-spec §6 layer and
# `STRICT_V1` mirrors a consumer's ratified contract, so moving either one's
# bytes from here would be this repo editing another repo's contract (O2).
# `OKF_V0_2` states upstream's. What stays byte-stable is therefore all four of
# them, proven by `tests/test_segmented_profile.py` at the profile level and by
# the golden suite at the byte level.
#
# What this profile adds on top of STRUCTURED_V1's facets and structure keys:
#
# - `segmentation` — the capability itself. Its presence is what every 1-to-N
# branch keys on.
# - `per_directory` — a segmented bundle has directories, and a bundle whose
# nested concepts are reachable only by guessing a path is a filing cabinet
# again. Set HERE rather than inherited, because STRUCTURED_V1 leaves it off.
# - `root_frontmatter=("bundle_id",)` — the identity carrier settled by order
# `…2527032751`: form (c), a root identifier consumers join on. Naming the
# key PERMITS it and fixes its position; the caller supplies the value (D5),
# because a bundle is a collection the caller delimits and only the caller
# knows what it is called. It is deliberately absent from
# `root_frontmatter_required`: Door B refuses a segmentation plan without an
# id, which is a rule about the door, not about every index this profile
# might ever write.
SEGMENTED_V1 = BundleProfile(
types=STRUCTURED_V1.types,
frontmatter=STRUCTURED_V1.frontmatter,
paths=STRUCTURED_V1.paths,
index=replace(
STRUCTURED_V1.index,
per_directory=True,
root_frontmatter=("bundle_id",),
# SPEC section 8's own row form, read and never written. See
# `IndexPolicy.also_reads`. Set on the SEGMENTED profiles alone:
# `DEFAULT` states commons' spec and `STRICT_V1` the wiki's ratified
# contract, and widening either from here would be this repository
# editing another repository's contract (O2).
also_reads=(
re.compile(
r"^\* \[(?P<label>[^\]]*)\]\((?P<target>[^)\s]+)\)"
r"(?: - (?P<description>.+))?$"
),
),
),
ownership=STRUCTURED_V1.ownership,
segmentation=SegmentationPolicy(),
)
# Bound so the sixth profile's facet extension is typed: `IndexPolicy.facets`
# is `FacetPolicy | None`, and `SEGMENTED_V1` is known here to carry one.
_SEGMENTED_FACETS = SEGMENTED_V1.index.facets
assert _SEGMENTED_FACETS is not None
# The sixth profile. A segmented bundle could not declare which upstream spec
# it targets: `SEGMENTED_V1` names `bundle_id`, `OKF_V0_2` names `okf_version`,
# and the two never intersected. Additive, as upstream support always is here --
# a new profile, never a migration of an existing one.
#
# THE INDEX POLICY IS DECIDED HERE, NOT INHERITED, and that is the one thing in
# this construction that is easy to get wrong. Measured: `OKF_V0_2.index` has
# `facets=None` and `per_directory=False`, while `SEGMENTED_V1.index` has both.
# Building the sixth profile on `OKF_V0_2`'s index would have produced a
# segmented bundle with NO faceted index -- structurally valid, conformant, and
# missing the surface a consumer reads. So the index comes from `SEGMENTED_V1`
# with both root keys named, and the spec declaration comes from `OKF_V0_2`.
#
# `okf_version`'s VALUE is not here and must never be: a profile names a key,
# the caller owns its value (decision E1). The value tracks the upstream Google
# version and belongs to catalog; a constant here would claim a decision this
# library does not own, and would be the one thing to chase on every upstream
# release.
SEGMENTED_OKF_V0_2 = BundleProfile(
types=OKF_V0_2.types,
frontmatter=OKF_V0_2.frontmatter,
paths=SEGMENTED_V1.paths,
# The facet tuple is EXTENDED here rather than shared, and that is the
# discriminator doing its job: `SEGMENTED_V1.index.facets` is one object
# both profiles would otherwise point at, so appending `adjudication` to it
# would surface the state under the older profile too and move a
# byte-pinned golden. `FacetPolicy.render` refuses any key a policy does
# not name, which is why the key has to live here and cannot be added by
# the door at write time.
index=replace(
SEGMENTED_V1.index,
root_frontmatter=("okf_version", "bundle_id"),
facets=replace(
_SEGMENTED_FACETS,
keys=_SEGMENTED_FACETS.keys + ("adjudication",),
),
),
ownership=OKF_V0_2.ownership,
# Constructed rather than `replace`d off `SEGMENTED_V1.segmentation`: that
# attribute is typed `| None`, and the equality is asserted in the suite so
# this stays a fresh copy of the same policy plus the discriminator.
segmentation=SegmentationPolicy(adjudication_key="adjudication"),
# O3, and set on THIS profile alone. `sources` is a v0.2 key, so a profile
# stating v0.1 must not name it; `DEFAULT` and `STRICT_V1` state contracts
# owned in other repositories, so adding a key to either from here would be
# this repository editing someone else's contract (O2); and `OKF_V0_2` is
# Door A's, where `sources` is already written from the manifest. What is
# left is the segmented v0.2 profile -- the one whose concepts come from a
# dropped binary document and therefore the only one with an original to
# point at.
provenance=ProvenancePolicy(),
)
# "The latest version supported as STABLE", not the latest present in this
# module. It therefore keeps v0.1 UPSTREAM semantics for as long as v0.2 is
# provisional, and flipping it is the GA event — one auditable action rather
# than a side effect of a merge.
#
# "v0.1 semantics" is about upstream and has never covered commons' layer. V1
# moved `DEFAULT`'s stamp, so this alias's bytes moved with it, before GA and
# without the flip. That is not a leak in the alias: the two contracts are
# independent axes, and a consumer bound here tracks both by construction.
#
# The tradeoff is stated rather than hidden: an alias that moves means a consumer
# bound to it inherits upstream's breaking changes on a library upgrade. The
# versioned constants are the stable binding and are what a consumer should pin;
# this is for callers who have explicitly opted into tracking.
OKF_LATEST = DEFAULT