feat(phase-3): the index policy becomes configurable, with the reader that judges it

`IndexPolicy` grew five judging fields and `IndexPolicy.violations`, closing the
gap a test has been pinning since `848e395`. `STRICT_V1.index` is now the proving
consumer's shape rather than DEFAULT's.

The design was settled by a conflict rather than by a preference. The convention
owner reported that an index is an authored count of a directory's children and
that a validator enumerating the directory has implemented the wrong contract.
Checked against the proving consumer before adopting it, the two turn out to be
directly opposed: gate BUNDLE_INDEX_COMPLETE (validate.py:1081-1120, ERROR) builds
its expected set by enumerating the directory and demands an exact bidirectional
match, and bundle.py:498-567 writes every index from a tree walk. Neither is
incoherent inside its own spec, so authored-versus-derived is a policy field in
both directions and a library invariant in neither.

Consequences encoded rather than documented: nothing here enumerates a directory
— the caller supplies the listing, `violations` refuses one when the profile's
index is authored and refuses to run without one when it is derived, so code
written to the wrong contract fails at the call instead of passing every test one
would think to write. Root and nested indexes are asymmetric (confirmed in both
consumers, different key sets). A per-entry description is template-level, so
`render_link` refuses both a missing description and an unwanted one.

DEFAULT keeps upstream's root-only index and judges nothing, for the same measured
reason it carries no required frontmatter key set: upstream binds `index.md` to
the bundle root alone, so a judging default would condemn conforming bundles.

25 new tests, 493 green. C1 re-proven: `git diff --stat examples/` empty.
This commit is contained in:
Kjell Tore Guttormsen 2026-07-25 20:30:54 +02:00
commit 9436427520
6 changed files with 626 additions and 29 deletions

View file

@ -307,19 +307,60 @@ def test_strict_v1_path_namespaces_are_the_spec_namespaces() -> None:
assert STRICT_V1.paths == DEFAULT.paths
def test_strict_v1_index_policy_is_still_the_default_shape() -> None:
"""A known gap, pinned so it cannot drift silently.
def test_strict_v1_index_policy_is_the_consumers_shape() -> None:
"""The gap this test used to pin is closed; it now states the shape.
The consumer's index shape — one index per directory, entries
`* [Title](link) - description`, exactly one `# ` heading, and a root index
carrying okf_version/bundle_profile/okf_spec_commit is not expressible by
`IndexPolicy` as it stands: neither per-directory indexes nor a description
on an entry has a field. It lands with the reader that judges it. Until
then, nothing consumes a profile's index policy, so this assertion is the
honest statement of where the port has reached.
Every value is read out of their code rather than recalled the entry
template and the root/nested frontmatter split from `bundle.py:498-567`,
the bidirectional match from gate `BUNDLE_INDEX_COMPLETE`
(`validate.py:1081-1120`, severity ERROR). The reader that judges all of
it lives on `IndexPolicy.violations`, so no field here is written without
something that reads it.
"""
assert STRICT_V1.index == DEFAULT.index
assert "description" not in STRICT_V1.index.link_template
index = STRICT_V1.index
assert index != DEFAULT.index
assert index.name == DEFAULT.index.name
assert index.render_link("Hooks", "hooks.md", "How hooks fire.") == (
"* [Hooks](hooks.md) - How hooks fire."
)
assert index.per_directory is True
assert index.heading_required is True
assert index.allows_prose is False
assert index.entries_match_directory is True
assert index.root_frontmatter == ("okf_version", "bundle_profile", "okf_spec_commit")
def test_a_conforming_wiki_root_index_passes_and_a_default_one_does_not() -> None:
"""The plan's second cross-profile test: a bundle valid under one profile
is refused by the other. It could not come from frontmatter DEFAULT has
no required key set, by measurement so it comes from index shape.
"""
conforming = (
"---\nokf_version: 0.1\nbundle_profile: strict-v1\n"
"okf_spec_commit: d44368c\n---\n\n# Wiki\n\n"
"* [Concepts](concepts/index.md) - Machine-generated index of concepts.\n"
)
assert (
STRICT_V1.index.violations(conforming, is_root=True, expected_targets={"concepts/index.md"})
== ()
)
default_shaped = "# Wiki\n\nCurated prose.\n\n- [Concepts](concepts/index.md)\n"
codes = {
violation.code
for violation in STRICT_V1.index.violations(
default_shaped, is_root=True, expected_targets={"concepts/index.md"}
)
}
assert codes == {
"index_root_key_missing",
"index_prose_not_allowed",
"index_entry_missing",
}
# ...and the reverse direction: DEFAULT judges neither of them, because a
# judging default would condemn the bundles this library itself writes.
assert DEFAULT.index.violations(conforming, is_root=True) == ()
assert DEFAULT.index.violations(default_shaped, is_root=True) == ()
def test_strict_v1_emission_is_the_ordered_prefix_then_the_sorted_tail() -> None: