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:
parent
99e5e17c00
commit
deeb248091
4 changed files with 287 additions and 2 deletions
|
|
@ -106,6 +106,15 @@ supported and the unspecified runtime is not; it re-enters scope when upstream
|
|||
specifies it. Because "always latest" decays silently, the release checklist
|
||||
carries an upstream-version re-check.
|
||||
|
||||
**Structured frontmatter values are emitted in YAML *flow* form, never block.**
|
||||
Both are valid YAML and an upstream reader recovers the same structure from
|
||||
either, but this library's parser is line-oriented: it round-trips a flow
|
||||
mapping as an opaque value and cannot read the block form at all — two block
|
||||
mappings sharing an inner key (§10.2's `executor` and `attester`, both carrying
|
||||
`resource`) collapse into one namespace and the first is lost silently.
|
||||
Emitting block would produce bundles we cannot read back. Reading it needs the
|
||||
structured reader (D1b); until then the constraint binds what we write.
|
||||
|
||||
**Every upstream release runs `docs/upstream-okf-upgrade-runbook.md`.** Pin the
|
||||
commit, enumerate the whole `okf/` tree, **read the shipped example bundles and not
|
||||
only `SPEC.md`**, classify the diff, measure our exposure and each consumer's, plan
|
||||
|
|
|
|||
25
README.md
25
README.md
|
|
@ -146,6 +146,31 @@ materialize_bundle(
|
|||
Omit the argument and no frontmatter block is written. Offering a key the
|
||||
profile does not name is refused before anything is written to disk.
|
||||
|
||||
### Attested computations (v0.2 §10)
|
||||
|
||||
`OKF_V0_2` supports the `Attested Computation` type as a **format**: its five
|
||||
contract fields — `runtime`, `parameters`, `computation`, `executor`,
|
||||
`attester` — are emitted in canonical position, judged, and round-tripped.
|
||||
`runtime` is required for that type and for no other, which the profile
|
||||
expresses through `FrontmatterSchema.required_by_type`; a type the mapping does
|
||||
not name carries no extra requirement, because §14 forbids a consumer to reject
|
||||
on an unknown `type`.
|
||||
|
||||
Nothing here executes a computation or checks an attestation. Upstream defers
|
||||
the receipt and verdict wire formats, so there is no contract to implement, and
|
||||
the question an attestation answers — was this value produced the sanctioned
|
||||
way — is not this library's. It re-enters scope when upstream specifies the
|
||||
protocol.
|
||||
|
||||
One limit worth knowing before you write such a concept: §10.2 presents
|
||||
`executor` and `attester` as nested block mappings, and this library's
|
||||
frontmatter parser is line-oriented. It reads inline **flow** mappings
|
||||
(`executor: { resource: …, receipt: [ … ] }`) as opaque values that round-trip
|
||||
unchanged, but it cannot read the block form — two block mappings that both
|
||||
carry a `resource` collapse into one namespace and the first is lost. Write the
|
||||
flow form; both are valid YAML, and a real YAML consumer recovers the same
|
||||
structure from either.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Verdict/feedback machinery from the method specification (stays in the
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
|
|
|||
195
tests/test_attested_computation.py
Normal file
195
tests/test_attested_computation.py
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
"""D4 — `Attested Computation`: format yes, runtime no.
|
||||
|
||||
Upstream §10 adds a concept type that carries not just what a value means but a
|
||||
sanctioned way to compute it. This library supports its *format* — the five
|
||||
contract fields for emission, judging and round-trip — and implements no
|
||||
execution: the receipt and verdict wire formats are explicitly deferred
|
||||
upstream, so there is no contract to build against.
|
||||
|
||||
Three groups:
|
||||
|
||||
1. **The five contract fields (§10.2).** Named in the profile's emission order
|
||||
so a caller can write them in canonical position. Before this they fell into
|
||||
`emit`'s sorted tail, which put `attester` ahead of `runtime` — alphabetical
|
||||
order standing in for the contract's own.
|
||||
2. **§10.2's one type-conditional requirement.** `runtime` is REQUIRED for this
|
||||
type and for no other, which is the first rule in this library that keys off
|
||||
a frontmatter *value* rather than a key. Reported, never raised: §14 forbids
|
||||
a *consumer* to reject on much of what this schema judges, so the caller
|
||||
decides what a violation means.
|
||||
3. **What the scalar parser can and cannot read.** §10's canonical presentation
|
||||
is nested block mappings. This library's line-oriented parser cannot
|
||||
represent them — and the failure is silent data loss, not an error, which is
|
||||
why it is pinned here rather than left to be discovered.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from llm_ingestion_okf.materialize import parse_frontmatter
|
||||
from llm_ingestion_okf.profiles import DEFAULT, OKF_V0_2, STRICT_V1, FrontmatterSchema
|
||||
|
||||
ATTESTED = "Attested Computation"
|
||||
|
||||
# The §10.2 contract fields, in the order that section enumerates them.
|
||||
CONTRACT_FIELDS = ("runtime", "parameters", "computation", "executor", "attester")
|
||||
|
||||
|
||||
# --- the five contract fields (§10.2) -------------------------------------
|
||||
|
||||
|
||||
def test_the_v0_2_profile_names_the_five_contract_fields() -> None:
|
||||
"""Naming a key fixes its emission position; it does not demand it. §10.2
|
||||
requires only `runtime`, and only for this type — the other four are
|
||||
optional even on an Attested Computation."""
|
||||
for key in CONTRACT_FIELDS:
|
||||
assert key in OKF_V0_2.frontmatter.order
|
||||
|
||||
|
||||
def test_the_contract_fields_are_contiguous_and_in_section_10_2_order() -> None:
|
||||
"""One contract, emitted as one block. Scattering the five across the
|
||||
frontmatter would still parse, but the reader of a concept would have to
|
||||
reassemble by hand what §10.2 presents together."""
|
||||
order = OKF_V0_2.frontmatter.order
|
||||
positions = [order.index(key) for key in CONTRACT_FIELDS]
|
||||
assert positions == list(range(positions[0], positions[0] + len(CONTRACT_FIELDS)))
|
||||
|
||||
|
||||
def test_a_contract_field_no_longer_falls_into_the_alphabetical_tail() -> None:
|
||||
"""`emit` writes named keys in profile order, then everything else SORTED.
|
||||
While the five were unnamed that tail ordered them alphabetically, so
|
||||
`attester` came out ahead of `runtime` — presentation deciding what the
|
||||
contract says."""
|
||||
values = {
|
||||
"type": ATTESTED,
|
||||
"attester": "{ resource: attesters/revenue.py }",
|
||||
"runtime": "bigquery",
|
||||
}
|
||||
emitted = [line.partition(":")[0] for line in OKF_V0_2.frontmatter.emit(values).splitlines()]
|
||||
assert emitted == ["type", "runtime", "attester"]
|
||||
|
||||
|
||||
def test_the_v0_1_profiles_gain_no_v0_2_contract_field() -> None:
|
||||
"""V-A6: adding v0.2 support is behavior-neutral for the v0.1 profiles."""
|
||||
for profile in (DEFAULT, STRICT_V1):
|
||||
assert not set(CONTRACT_FIELDS) & set(profile.frontmatter.order)
|
||||
|
||||
|
||||
# --- §10.2's one type-conditional requirement -----------------------------
|
||||
|
||||
|
||||
def test_runtime_is_required_when_the_type_is_attested_computation() -> None:
|
||||
"""§10.2: "REQUIRED for this type". The single field that says how to run
|
||||
the computation, and so what `parameters` mean — an Attested Computation
|
||||
without it is a contract that cannot be interpreted."""
|
||||
violations = OKF_V0_2.frontmatter.violations({"type": ATTESTED})
|
||||
assert [(v.key, v.code) for v in violations] == [
|
||||
("runtime", "frontmatter_key_missing_for_type")
|
||||
]
|
||||
|
||||
|
||||
def test_the_requirement_is_satisfied_by_the_field_itself() -> None:
|
||||
assert OKF_V0_2.frontmatter.violations({"type": ATTESTED, "runtime": "bigquery"}) == ()
|
||||
|
||||
|
||||
def test_the_other_four_contract_fields_stay_optional() -> None:
|
||||
"""§10.2 makes exactly one of the five REQUIRED. `computation` absent means
|
||||
the body fence is the computation (§10.3) — a valid concept, not a gap."""
|
||||
assert OKF_V0_2.frontmatter.violations({"type": ATTESTED, "runtime": "python"}) == ()
|
||||
|
||||
|
||||
def test_another_known_type_does_not_carry_the_requirement() -> None:
|
||||
assert OKF_V0_2.frontmatter.violations({"type": "Concept"}) == ()
|
||||
|
||||
|
||||
def test_an_unknown_type_carries_no_requirement_either() -> None:
|
||||
"""§14: a consumer MUST NOT reject on unknown `type` values. A conditional
|
||||
keyed on a type we do not know must therefore stay silent rather than
|
||||
guess."""
|
||||
assert OKF_V0_2.frontmatter.violations({"type": "Dashboard"}) == ()
|
||||
|
||||
|
||||
def test_a_document_with_no_type_at_all_reports_only_the_missing_type() -> None:
|
||||
"""The conditional keys off a value that is not there. It must not fire on
|
||||
absence — that would report two violations for one defect."""
|
||||
violations = OKF_V0_2.frontmatter.violations({"title": "Untyped"})
|
||||
assert [(v.key, v.code) for v in violations] == [("type", "frontmatter_key_missing")]
|
||||
|
||||
|
||||
def test_the_v0_1_profiles_do_not_judge_a_v0_2_type(tmp_path: Path) -> None:
|
||||
"""V-A6 again, on the judging side: `Attested Computation` is a v0.2 type,
|
||||
so a v0.1 profile has no rule about it and must invent none."""
|
||||
assert DEFAULT.frontmatter.required_by_type == {}
|
||||
assert DEFAULT.frontmatter.violations({"type": ATTESTED}) == ()
|
||||
|
||||
|
||||
def test_a_type_conditional_key_must_be_inside_a_closed_allowlist() -> None:
|
||||
"""The same rule `required` already carries: a schema that demands a key it
|
||||
also forbids can never be satisfied. Checked at construction because a
|
||||
profile is configuration, and configuration that cannot be satisfied should
|
||||
fail where it is written, not where it is used."""
|
||||
with pytest.raises(ValueError, match="can never be satisfied"):
|
||||
FrontmatterSchema(
|
||||
order=("type",),
|
||||
allowed=frozenset({"type"}),
|
||||
required_by_type={ATTESTED: frozenset({"runtime"})},
|
||||
)
|
||||
|
||||
|
||||
# --- what the scalar parser can and cannot read ---------------------------
|
||||
|
||||
|
||||
def test_the_contract_fields_round_trip_in_their_flow_form(tmp_path: Path) -> None:
|
||||
"""The form this library can both write and read. §11 asks for a parseable
|
||||
YAML block, and a flow mapping is one — V-A8 measured upstream's own reader
|
||||
recovering our flow forms as structures."""
|
||||
values = {
|
||||
"type": ATTESTED,
|
||||
"runtime": "bigquery",
|
||||
"parameters": "[{ name: year, type: integer, required: true }]",
|
||||
"executor": "{ resource: skills/run-on-bq.md, receipt: [job_id, executed_sql] }",
|
||||
"attester": "{ resource: attesters/revenue.py }",
|
||||
}
|
||||
path = tmp_path / "revenue.md"
|
||||
path.write_text(f"---\n{OKF_V0_2.frontmatter.emit(values)}\n---\n\n# Computation\n")
|
||||
|
||||
assert parse_frontmatter(path) == values
|
||||
|
||||
|
||||
def test_two_nested_block_mappings_sharing_a_key_collide_in_the_scalar_parser(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
"""Measured 2026-07-31, and the reason D4 stops at the flow form.
|
||||
|
||||
§10.2 presents `executor` and `attester` as nested BLOCK mappings, and both
|
||||
carry a `resource`. The line-oriented parser has no indentation model, so it
|
||||
flattens them into one namespace where the second `resource` overwrites the
|
||||
first: `executor.resource` is lost and `attester.resource` surfaces as a
|
||||
top-level key. No error is raised.
|
||||
|
||||
Pinned rather than fixed. Reading this form needs the structured reader
|
||||
(D1b), and a half-reader that silently drops half a contract is worse than
|
||||
one that never claimed to read it.
|
||||
"""
|
||||
path = tmp_path / "block-form.md"
|
||||
path.write_text(
|
||||
"---\n"
|
||||
"type: Attested Computation\n"
|
||||
"runtime: bigquery\n"
|
||||
"executor:\n"
|
||||
" resource: skills/run-on-bq.md\n"
|
||||
" receipt: [job_id, executed_sql]\n"
|
||||
"attester:\n"
|
||||
" resource: attesters/revenue.py\n"
|
||||
"---\n\n# Computation\n"
|
||||
)
|
||||
|
||||
parsed = parse_frontmatter(path)
|
||||
|
||||
assert parsed["executor"] == ""
|
||||
assert parsed["attester"] == ""
|
||||
assert parsed["resource"] == "attesters/revenue.py"
|
||||
assert "skills/run-on-bq.md" not in parsed.values()
|
||||
Loading…
Add table
Add a link
Reference in a new issue