fix(profiles): P1-F1 — a permitted root key is not a required one

`IndexPolicy.root_frontmatter` carried two claims at once: which keys the
root index may carry and in what order, and which it must carry. The judge
read it as the second, so `OKF_V0_2` — which names `okf_version` only to fix
its position, upstream granting it as a MAY (§8:509-510, §12:773-775) —
reported every bundle exercising that MAY as violating. Measured in P1 over
17 bundles: 14 failed with exactly this one violation, upstream's own four
reference bundles among them, while D5's emitter treated the same key as
optional. We emitted a MAY correctly and graded it a MUST.

`root_frontmatter` now permits and orders; `root_frontmatter_required`
demands. A required key outside the ordered set fails at construction, the
same contradiction `FrontmatterSchema` already refuses when `required`
strays outside `allowed`.

- `OKF_V0_2` requires none — upstream's MAY, stated on the judge side too.
- `STRICT_V1` requires all three: the proving consumer's root index carries
  exactly those keys in that order on every bundle measured (`c5141f8`), so
  separating the meanings costs them nothing.
- `DEFAULT` names no root keys and is untouched.

Re-measured over the eight bundle-root indexes reachable locally (our four
goldens, upstream's four): 7 of 8 failing under the old semantics, 0 of 8
after. Emit path byte-identical — the golden suite fails otherwise.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JqCmfJ2ukpFXeFjfab8wvy
This commit is contained in:
Kjell Tore Guttormsen 2026-07-31 18:16:13 +02:00
commit 2c01e58259
2 changed files with 85 additions and 5 deletions

View file

@ -375,9 +375,18 @@ class IndexPolicy:
caller supplies the listing, and a policy that does not judge it refuses
to be handed one at all so code written to the wrong contract fails at
the call rather than passing every test one would think to write.
- `root_frontmatter` the ordered keys the ROOT index carries, where
- `root_frontmatter` the ordered keys the ROOT index may carry, where
nested indexes carry none. Confirmed independently in both consumers, so
the asymmetry is shape rather than one repo's preference.
the asymmetry is shape rather than one repo's preference. Naming a key
here PERMITS it and fixes its position; it does not demand it.
- `root_frontmatter_required` the subset that must actually be present.
Separate from the tuple above because the two are different claims, and
collapsing them was P1-F1: `OKF_V0_2` names `okf_version` to fix its
position, but upstream §8/§12 grant it as a MAY, and judging the pair as
one field failed 14 of 17 real bundles upstream's own four included —
each with exactly this one violation, while our emitter treated the same
key as optional. A profile whose consumer really does demand the keys
says so here, which is what `STRICT_V1` does.
"""
name: str
@ -388,8 +397,16 @@ class IndexPolicy:
allows_prose: bool = True
entries_match_directory: bool = False
root_frontmatter: tuple[str, ...] = ()
root_frontmatter_required: frozenset[str] = field(default_factory=frozenset)
def __post_init__(self) -> None:
stray = sorted(self.root_frontmatter_required - set(self.root_frontmatter))
if stray:
raise ValueError(
f"required root keys must be named in the ordered set "
f"(got {', '.join(repr(key) for key in stray)}) — a key demanded "
"but never named could not be judged for position"
)
if self.requires_description and "description" not in self.link_pattern.groupindex:
raise ValueError(
"link_template names {description} but link_pattern has no "
@ -526,7 +543,7 @@ class IndexPolicy:
found = [
IndexViolation(key, "is pinned on the root index and absent", "index_root_key_missing")
for key in self.root_frontmatter
if key not in head
if key in self.root_frontmatter_required and key not in head
]
present = [key for key in head if key in self.root_frontmatter]
declared = [key for key in self.root_frontmatter if key in head]
@ -646,6 +663,11 @@ STRICT_V1 = BundleProfile(
allows_prose=False,
entries_match_directory=True,
root_frontmatter=("okf_version", "bundle_profile", "okf_spec_commit"),
# The proving consumer demands all three, not merely permits them: their
# root index carries exactly these keys in exactly this order on every
# bundle measured (`c5141f8`). Stated explicitly so that separating
# "permitted" from "required" costs them nothing.
root_frontmatter_required=frozenset({"okf_version", "bundle_profile", "okf_spec_commit"}),
),
)
@ -700,7 +722,11 @@ _OKF_V0_2_KEY_ORDER = (
#
# Declaring it stays a MAY: none of upstream's four reference bundles carries
# the key at all (catalog grepped `okf/bundles` and `okf/samples`: zero hits),
# so omitting `root_frontmatter_values` emits no block.
# so omitting `root_frontmatter_values` emits no block. The JUDGE says the same
# thing since P1-F1 — the key is named here and left out of
# `root_frontmatter_required` — because for one release it did not, and a
# bundle exercising the MAY was reported as violating by the very profile that
# had emitted it correctly.
#
# **Measured limitation (guard 0.2.0, 2026-07-26):** a bundle emitted under this
# profile cannot be read back through a guard-gated import. The guard's T2