fix(okf-v0.2): V-A7 counted every naming field but nullable
The gate refuses a schema that names both `timestamp` and `generated`,
because OKF §13.1 grants the legacy fallback only while `generated` is
ABSENT. `__post_init__` built the named set from `order | required |
allowed` and left `nullable` out, so
FrontmatterSchema(order=("timestamp", "title"),
nullable=frozenset({"generated"}))
constructed, and `violations()` on a document carrying both returned ().
The namespace is open by default, so a key named only by `nullable` is
admitted exactly as surely as an emitted one — the omission was a hole in
the gate, not a narrower gate.
Found by the proving consumer the same day the gate was written; their
construction is the red test case.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A2aKJxLejT9S8jYwoZ9fut
This commit is contained in:
parent
d77224d4ed
commit
a00355533e
2 changed files with 14 additions and 1 deletions
|
|
@ -35,6 +35,10 @@ RESERVED_OKF_TYPE = "verdict"
|
|||
# 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"})
|
||||
|
||||
|
||||
|
|
@ -144,7 +148,7 @@ class FrontmatterSchema:
|
|||
key_pattern: re.Pattern[str] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
named = set(self.order) | set(self.required)
|
||||
named = set(self.order) | set(self.required) | set(self.nullable)
|
||||
if self.allowed is not None:
|
||||
named |= set(self.allowed)
|
||||
if _TIMESTAMP_FALLBACK_PAIR <= named:
|
||||
|
|
|
|||
|
|
@ -289,6 +289,10 @@ def test_the_key_name_is_what_a_profile_pins(tmp_path: Path) -> None:
|
|||
{"order": (), "required": frozenset({"timestamp", "generated"})},
|
||||
id="both-required",
|
||||
),
|
||||
pytest.param(
|
||||
{"order": ("timestamp", "title"), "nullable": frozenset({"generated"})},
|
||||
id="one-emitted-one-nullable",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_a_schema_naming_both_timestamp_and_generated_cannot_be_built(
|
||||
|
|
@ -299,6 +303,11 @@ def test_a_schema_naming_both_timestamp_and_generated_cannot_be_built(
|
|||
valid `generated.at` nor an eligible fallback. Refused at construction, the
|
||||
same shape as the reserved `verdict` layer: a profile that could reach the
|
||||
combination cannot be built, let alone handed to a door.
|
||||
|
||||
Every judging field names keys, `nullable` included. An open namespace
|
||||
already admits `generated`, so a schema that also gives it a null rule has
|
||||
named it exactly as surely as one that emits it — the last case is the
|
||||
construction the proving consumer found the day the gate was written.
|
||||
"""
|
||||
with pytest.raises(ValueError, match="timestamp"):
|
||||
FrontmatterSchema(**schema_kwargs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue