fix: types/type-spec.schema.yaml enforced against real type-spec frontmatter, doc-pull-through.md docs/-page count corrected (closes #105)
CI / verify (push) Failing after 45s
Release / release (push) Successful in 38s

Files changed:
- CHANGES.md
- VERSION
- instructions/dev/doc-pull-through.md
- tools/CONTRACT.md
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/tests/test_docs_verify.py
- types/type-spec.md
- types/type-spec.schema.yaml
This commit is contained in:
torben committed 2026-09-15 19:45:33 +02:00
1 parent 6eb3f84256
commit 55f65c1ab1
8 files changed
+161 -7

No files matched your search

+45 -1
View File
@@ -44,6 +44,15 @@ assume - is `instructions verify`'s job, not this one, since that module
already owns the Skill/Instruction split (`skill_dirs()` vs
`instruction_files()`).
An eighth checks the type layer against its own schema: every file under
`types/` declaring `type: types/type-spec.md` must validate against
`types/type-spec.schema.yaml`. Before this check existed the schema had
already drifted behind two fields real type-specs carry (`root:`,
`capture_fields:`) while `additionalProperties: false` sat there describing a
contract nothing enforced - the exact "checked or absent" failure this file's
opening paragraph names, just one level up, for the schema that describes the
type layer instead of a copy the type layer's code produces (Gitea #105).
Everything here is a hard oracle: a set comparison or a regex, no judgment.
Content quality of the contracts themselves stays with the LLM.
"""
@@ -422,6 +431,37 @@ def check_stack_required_types() -> list[str]:
return issues
def check_type_spec_frontmatter() -> list[str]:
"""Every type-spec's own frontmatter must validate against
`types/type-spec.schema.yaml` - the schema that describes the type layer
gets the same enforcement any other type's schema gets (Gitea #105).
Before this check nothing ever called `validate_frontmatter` against a
type-spec's own frontmatter, so the schema had quietly drifted behind two
fields real type-specs actually carry (`root:`, `capture_fields:`)
without anything failing - `additionalProperties: false` described a
contract that bound nothing. `resolver.list_type_specs()` already reads
every file's frontmatter once for `wikitool types list`; reusing it here
means this check costs no second parse pass.
"""
from chemenu.type_resolver import resolver
issues: list[str] = []
for type_path, frontmatter in resolver.list_type_specs():
try:
resolver.validate_frontmatter(
frontmatter, "types/type-spec.md", source_file=config.ROOT / type_path
)
except ValueError as exc:
# `validate_frontmatter`'s own message names the type path it
# validated *against* (always `types/type-spec.md` here, since
# every type-spec is validated against the same schema) rather
# than the specific file that failed - prefix that file's own
# path so two failures in one run stay distinguishable.
issues.append(f"{type_path}: {exc}")
return issues
def check_legacy_type_blocks() -> list[str]:
issues = []
guarded = [
@@ -857,11 +897,12 @@ def check_breaking_change_for_boundary() -> list[str]:
@app.command("verify")
def verify():
"""Check the CLI/README command tables, contract presence, type-form drift, ignore rules, version/changelog agreement, issue references, and link targets in shipped documents."""
"""Check the CLI/README command tables, contract presence, type-form drift, every type-spec's frontmatter against its own schema, ignore rules, version/changelog agreement, issue references, and link targets in shipped documents."""
issues = (
check_cli_readme()
+ check_readmes_have_no_command_table()
+ check_collection_contracts()
+ check_type_spec_frontmatter()
+ check_legacy_type_blocks()
+ check_ignored_content()
+ check_version_changelog()
@@ -875,10 +916,13 @@ def verify():
if issues:
fail("Documentation issues found:\n" + "\n".join(f"- {i}" for i in issues))
from chemenu.type_resolver import resolver
success(
f"Docs verified: {len(registered_commands())} command(s) documented, "
f"{len(kb_collections.iter_kb_collections())} collection(s) and "
f"{len(STAGE_CONTRACTS)} stage contract(s) present, no legacy type blocks, "
f"{len(resolver.list_type_specs())} type-spec(s) validating against their own schema, "
f"{len(IGNORE_CANARIES)} ignore canaries clear, "
f"no issue references in {len(shipped_prose())} shipped document(s), "
f"tables of contents current and every link resolving on "
+47 -1
View File
@@ -1,8 +1,9 @@
import pytest
import typer
from chemenu import config
from chemenu import config, type_resolver
from chemenu.commands import dist_cmd, docs_verify
from chemenu.type_resolver import TypeResolver
def test_every_registered_command_is_documented():
@@ -289,6 +290,51 @@ def test_an_absent_listed_doc_is_skipped_not_reported(tmp_path, monkeypatch):
assert docs_verify.check_readmes_have_no_command_table() == []
def test_this_repos_type_specs_validate_against_their_own_schema():
"""Regression guard for Gitea #105: types/type-spec.schema.yaml declared
`additionalProperties: false` while real type-specs already carried
`root:` and `capture_fields:`, and nothing ever validated a type-spec's
own frontmatter against it - so the mismatch shipped silently."""
assert docs_verify.check_type_spec_frontmatter() == []
def test_an_unknown_type_spec_field_is_reported(tmp_path, monkeypatch):
"""Once the schema is enforced, a type-spec frontmatter field its own
schema does not know about must fail loudly rather than validating
silently - the other direction of the #105 regression guard above."""
types_dir = tmp_path / "types"
types_dir.mkdir()
(types_dir / "type-spec.md").write_text(
"---\n"
"type: types/type-spec.md\n"
"name: type-spec\n"
"description: Authoring and validation contract for type specs\n"
"schema: types/type-spec.schema.yaml\n"
"---\n",
encoding="utf-8",
)
(types_dir / "type-spec.schema.yaml").write_text(
(config.ROOT / "types" / "type-spec.schema.yaml").read_text(encoding="utf-8"),
encoding="utf-8",
)
(types_dir / "widget.md").write_text(
"---\n"
"type: types/type-spec.md\n"
"name: widget\n"
"description: A type-spec with a field its own schema does not know\n"
"schema: null\n"
"not_a_real_field: true\n"
"---\n",
encoding="utf-8",
)
monkeypatch.setattr(config, "ROOT", tmp_path)
monkeypatch.setattr(config, "TYPES_DIR", types_dir)
monkeypatch.setattr(type_resolver, "resolver", TypeResolver(repo_root=tmp_path))
issues = docs_verify.check_type_spec_frontmatter()
assert any("widget.md" in issue and "not_a_real_field" in issue for issue in issues)
def test_legacy_type_blocks_are_absent():
assert docs_verify.check_legacy_type_blocks() == []