feat(okf-v0.2): V-A7 -- a profile cannot name both timestamp and generated
Plan step 3's remaining half, and the one place steps 1-3 add behavior rather than only characterizing it. Operator decision this session: build the gate NOW rather than at D2, so the multi-file D2 work is developed with the invariant already in place. OKF section 13.1 reads `timestamp` as a legacy stand-in for `generated.at` only while `generated` is ABSENT. A schema able to name both can therefore describe a document with neither a valid `generated.at` nor an eligible fallback. Refused at construction, the same shape as the reserved `verdict` layer -- and one level below the sketch that was approved, on `FrontmatterSchema` rather than `BundleProfile`: the schema is the policy that names frontmatter keys, it is the exact C3 analog, and a profile must build its schema first, so the stricter placement subsumes the other. Stated as key names rather than as a judgement on values, deliberately: every `generated` a schema can express today is a scalar (`_is_legal_value`) and so malformed as a v0.2 mapping, which makes naming both exactly the hazard. It narrows when a value model can express a well-formed `generated` -- recorded at the constant rather than left for a reader to reconstruct. Behavior-neutral for everything shipped, and not by assertion: DEFAULT emits `generated` and no `timestamp`, STRICT_V1 the reverse (which is what puts the wiki on the legacy path rather than in a defect), and the byte-exact golden suite is green. 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
1215f985ae
commit
7bc366bc1a
2 changed files with 64 additions and 2 deletions
|
|
@ -11,7 +11,8 @@ 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.
|
||||
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).
|
||||
|
|
@ -27,6 +28,15 @@ from dataclasses import dataclass, field
|
|||
# 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.
|
||||
_TIMESTAMP_FALLBACK_PAIR = frozenset({"timestamp", "generated"})
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class TypeRejection:
|
||||
|
|
@ -134,6 +144,16 @@ class FrontmatterSchema:
|
|||
key_pattern: re.Pattern[str] | None = None
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
named = set(self.order) | set(self.required)
|
||||
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)):
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ from llm_ingestion_okf.materialize import (
|
|||
materialize_bundle,
|
||||
parse_frontmatter,
|
||||
)
|
||||
from llm_ingestion_okf.profiles import DEFAULT, STRICT_V1, BundleProfile
|
||||
from llm_ingestion_okf.profiles import DEFAULT, STRICT_V1, BundleProfile, FrontmatterSchema
|
||||
from test_import_flow import StubImportGate, place, run
|
||||
|
||||
INGESTED_AT = "2026-07-25T12:00:00Z"
|
||||
|
|
@ -277,6 +277,48 @@ def test_the_key_name_is_what_a_profile_pins(tmp_path: Path) -> None:
|
|||
assert "okf_version" in STRICT_V1.index.root_frontmatter
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"schema_kwargs",
|
||||
[
|
||||
pytest.param({"order": ("timestamp", "generated")}, id="both-emitted"),
|
||||
pytest.param(
|
||||
{"order": ("timestamp",), "allowed": frozenset({"timestamp", "generated"})},
|
||||
id="one-emitted-one-admitted",
|
||||
),
|
||||
pytest.param(
|
||||
{"order": (), "required": frozenset({"timestamp", "generated"})},
|
||||
id="both-required",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_a_schema_naming_both_timestamp_and_generated_cannot_be_built(
|
||||
schema_kwargs: dict[str, Any],
|
||||
) -> None:
|
||||
"""V-A7. OKF §13.1 grants the `timestamp` fallback only while `generated` is
|
||||
ABSENT, so a schema able to name both can describe a document with neither a
|
||||
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.
|
||||
"""
|
||||
with pytest.raises(ValueError, match="timestamp"):
|
||||
FrontmatterSchema(**schema_kwargs)
|
||||
|
||||
|
||||
def test_the_two_shipped_profiles_sit_on_opposite_sides_of_the_fallback() -> None:
|
||||
"""Why the gate above costs nothing: neither shipped profile is near the
|
||||
combination, and they are not near it in opposite directions. `DEFAULT`
|
||||
emits `generated` and no `timestamp`; `STRICT_V1` emits `timestamp` and no
|
||||
`generated`, which is precisely what puts the wiki on the §13.1 legacy path
|
||||
rather than in a defect.
|
||||
"""
|
||||
assert "generated" in DEFAULT.frontmatter.order
|
||||
assert "timestamp" not in DEFAULT.frontmatter.order
|
||||
assert "timestamp" in STRICT_V1.frontmatter.order
|
||||
assert "generated" not in STRICT_V1.frontmatter.order
|
||||
assert STRICT_V1.frontmatter.allowed is not None
|
||||
assert "generated" not in STRICT_V1.frontmatter.allowed
|
||||
|
||||
|
||||
@pytest.mark.parametrize("declared", ["0.1", "0.2", "1.0", "2026.7.3"])
|
||||
def test_the_root_frontmatter_policy_expresses_any_version_value(declared: str) -> None:
|
||||
"""V4. The policy judges presence and ORDER, never the value — so the day
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue