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)):
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue