feat(okf-v0.2): D4 — the Attested Computation contract, format only

Upstream §10 adds a concept type carrying a sanctioned way to compute a
value. This lands its FORMAT: the five contract fields for emission and
round-trip, and §10.2's one requirement. No execution — upstream defers
the receipt and verdict wire formats, so there is nothing to build a
runtime against.

Two additions, both additive:

- The five fields (`runtime`, `parameters`, `computation`, `executor`,
  `attester`) join `OKF_V0_2`'s emission order as one block, internally
  in §10.2's own listing order. Without it they still emit — in `emit`'s
  sorted tail, where `attester` precedes `runtime`, alphabetical order
  standing in for the contract's own. No bundle that carries none of the
  keys changes by a byte, and the v0.1 profiles gain nothing.

- `FrontmatterSchema.required_by_type` expresses "`runtime` is REQUIRED
  for this type and no other" — the first rule here keyed off a
  frontmatter VALUE rather than a key. It cannot be `required`, which
  would demand `runtime` of every document. A type the mapping does not
  name carries no extra requirement, which is what keeps it inside §14:
  a consumer must not reject on an unknown `type`, so a conditional
  keyed on an unknown type stays silent rather than guesses.

Also pinned, measured today: the line-oriented parser cannot read §10's
canonical BLOCK form. `executor` and `attester` both carry a `resource`,
and with no indentation model the second overwrites the first —
`executor.resource` is lost silently, no error. Characterized rather
than fixed: reading that form needs the structured reader (D1b), and a
half-reader that drops half a contract is worse than one that never
claimed to read it. CLAUDE.md gains the invariant that falls out of it:
we emit flow form, never block, or we write bundles we cannot read back.

578 tests, mypy --strict clean, goldens byte-identical.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KKKMwi7e7PVHoFW6dJK5XP
This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 21:35:23 +02:00
commit deeb248091
4 changed files with 287 additions and 2 deletions

View file

@ -138,6 +138,15 @@ class FrontmatterSchema:
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, ...]
@ -146,9 +155,11 @@ class FrontmatterSchema:
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:
named = set(self.order) | set(self.required) | set(self.nullable)
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:
@ -160,7 +171,11 @@ class FrontmatterSchema:
)
if self.allowed is None:
return
for label, keys in (("required", self.required), ("nullable", self.nullable)):
for label, keys in (
("required", self.required),
("nullable", self.nullable),
("type-conditional required", conditional),
):
stray = sorted(keys - self.allowed)
if stray:
raise ValueError(
@ -186,6 +201,23 @@ class FrontmatterSchema:
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(
@ -683,12 +715,30 @@ STRICT_V1 = BundleProfile(
# 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
@ -742,6 +792,12 @@ OKF_V0_2 = BundleProfile(
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`