feat: docs reference generates a skill catalogue and a page-type reference under docs/reference/ from the shipped defaults, checked and written in the origin repo only (#189)
Files changed: - CHANGES.md - README.md - VERSION - docs/reference/README.md - docs/reference/commands/README.md - docs/reference/commands/types-instructions-and-docs.md - docs/reference/page-types.md - docs/reference/skills.md - instructions/dev/doc-pull-through.md - tools/CONTRACT.md - tools/README.md - tools/chemenu/commands/docs_verify.py - tools/chemenu/reference_doc.py - tools/chemenu/tests/test_reference_doc.py Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
d644a1d4f7
commit
070cae6aad
14 files changed
+606
-65
No files matched your search
@@ -1,11 +1,14 @@
|
||||
import json
|
||||
import re
|
||||
|
||||
import pytest
|
||||
from typer.testing import CliRunner
|
||||
|
||||
from chemenu import blocks, cli, cli_contract, config, reference_doc, toc
|
||||
from chemenu import version as version_mod
|
||||
from chemenu.cli import app
|
||||
from chemenu.commands import dist_cmd, docs_verify
|
||||
from chemenu.commands import dist_cmd, docs_verify, instructions_cmd
|
||||
from chemenu.type_resolver import resolver
|
||||
|
||||
runner = CliRunner()
|
||||
|
||||
@@ -94,6 +97,112 @@ def test_the_reference_pages_survive_dist_export_unchanged():
|
||||
assert plan[region.path].content == (config.ROOT / region.path).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def _type_sections(page: str) -> dict[str, str]:
|
||||
region = blocks.find(page, reference_doc.PAGE_TYPES_REGION)
|
||||
return {
|
||||
match.group(1): match.group(2)
|
||||
for match in re.finditer(r"^## `([^`]+)`\n(.*?)(?=^## |\Z)", region, re.M | re.S)
|
||||
}
|
||||
|
||||
|
||||
def test_the_skill_catalogue_has_one_row_per_shipped_skill():
|
||||
shipped = {
|
||||
d.name for d in instructions_cmd.skill_dirs() if not instructions_cmd.is_dev_only(d)
|
||||
}
|
||||
assert shipped
|
||||
region = blocks.find(
|
||||
(config.ROOT / reference_doc.SKILLS).read_text(encoding="utf-8"), reference_doc.SKILLS_REGION
|
||||
)
|
||||
rows = re.findall(r"^\| `([^`]+)` \|", region, re.M)
|
||||
assert sorted(rows) == sorted(shipped)
|
||||
for directory in instructions_cmd.skill_dirs():
|
||||
if instructions_cmd.is_dev_only(directory):
|
||||
assert f"`{directory.name}`" not in region
|
||||
|
||||
|
||||
def test_page_types_has_one_section_per_page_type_matching_types_describe():
|
||||
"""Required fields and subtypes are what `types describe --json` says, and
|
||||
each subtype's directory is where `new` would write it."""
|
||||
sections = _type_sections((config.ROOT / reference_doc.PAGE_TYPES).read_text(encoding="utf-8"))
|
||||
names = {
|
||||
fm["name"] for path, fm in resolver.list_type_specs()
|
||||
if path.rsplit("/", 1)[-1][:-3] in dist_cmd.instance_owned_type_stems()
|
||||
}
|
||||
assert names and set(sections) == names
|
||||
|
||||
for name, section in sections.items():
|
||||
result = runner.invoke(app, ["types", "describe", name, "--json"])
|
||||
assert result.exit_code == 0, result.output
|
||||
described = json.loads(result.output)
|
||||
required = [f["field"] for f in described["fields"] if f["required"]]
|
||||
listed = re.findall(r"^- `([^`]+)` - ", section.split("**Required fields**")[1], re.M)
|
||||
assert listed == required, name
|
||||
|
||||
field = described["subtype_field"]
|
||||
rows = re.findall(r"^\| `([^`]+)` \| `([^`]+)` \|", section, re.M)
|
||||
if not field:
|
||||
assert rows == [], name
|
||||
continue
|
||||
enum = next(f["enum"] for f in described["fields"] if f["field"] == field)
|
||||
assert [value for value, _ in rows] == enum, name
|
||||
for value, written_to in rows:
|
||||
target = resolver.compute_target_dir(described["type_path"], {field: value})
|
||||
assert written_to == target.relative_to(config.ROOT).as_posix() + "/", (name, value)
|
||||
|
||||
|
||||
def test_a_description_is_split_at_its_use_when_sentence():
|
||||
assert reference_doc.split_description("Does X. Use when Y happens.") == ("Does X.", "Y happens.")
|
||||
assert reference_doc.split_description("Does X.") == ("Does X.", "only when asked for by name")
|
||||
|
||||
|
||||
def test_a_table_cell_escapes_pipes_and_tags_but_not_inside_code():
|
||||
cell = reference_doc._cell('says "ingest <file>" or `a <b> | c` | d')
|
||||
assert cell == 'says "ingest <file>" or `a <b> \\| c` \\| d'
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def exported_instance(tmp_path, monkeypatch):
|
||||
"""A `dist export` of this tree, adopted and then changed the way an
|
||||
instance may: a type-spec of its own making, and a skill of its own."""
|
||||
target = tmp_path / "instance"
|
||||
dist_cmd.run_export(target)
|
||||
monkeypatch.setattr(config, "ROOT", target)
|
||||
monkeypatch.setattr(resolver, "_repo_root", target)
|
||||
for template in sorted((target / "types").glob("*.template")):
|
||||
template.rename(template.with_suffix(""))
|
||||
entity = target / "types" / "entity.md"
|
||||
entity.write_text(
|
||||
entity.read_text(encoding="utf-8").replace(
|
||||
"description: Base type for entity pages", "description: This instance's entities"
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
own = target / "instructions" / "own-skill"
|
||||
own.mkdir()
|
||||
(own / "SKILL.md").write_text(
|
||||
"---\nname: own-skill\ndescription: Does this instance's thing. Use when asked.\n---\n# Own\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
return target
|
||||
|
||||
|
||||
def test_an_adapted_instance_leaves_both_pages_alone(exported_instance):
|
||||
root = exported_instance
|
||||
assert version_mod.stamp_file().is_file()
|
||||
pages = {
|
||||
relative: (root / relative).read_text(encoding="utf-8")
|
||||
for relative in (reference_doc.SKILLS, reference_doc.PAGE_TYPES)
|
||||
}
|
||||
assert docs_verify.check_reference_regions() == []
|
||||
|
||||
result = runner.invoke(app, ["docs", "reference", "--apply"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "Left alone in this instance" in result.output
|
||||
for relative, before in pages.items():
|
||||
assert (root / relative).read_text(encoding="utf-8") == before, relative
|
||||
assert docs_verify.check_reference_regions() == []
|
||||
|
||||
|
||||
# --- a fixture tree -------------------------------------------------------
|
||||
|
||||
GROUPS = (("Fixture things", ("frobnicate",)), ("Other", ("gate keep",)))
|
||||
@@ -127,15 +236,34 @@ HEADS = {
|
||||
"docs/reference/commands/other.md": "# Other\n\nThe other one.\n",
|
||||
"docs/reference/commands/README.md": "# Command reference\n\nAll of it.\n",
|
||||
"docs/reference/exit-codes.md": "# Exit codes and gates\n\nWhere it stops.\n",
|
||||
"docs/reference/skills.md": "# Skill catalogue\n\nThe skills.\n",
|
||||
"docs/reference/page-types.md": "# Page types\n\nThe types.\n",
|
||||
}
|
||||
REGION_OF = {
|
||||
"docs/reference/commands/fixture-things.md": reference_doc.COMMANDS_REGION,
|
||||
"docs/reference/commands/other.md": reference_doc.COMMANDS_REGION,
|
||||
"docs/reference/commands/README.md": reference_doc.INDEX_REGION,
|
||||
"docs/reference/exit-codes.md": reference_doc.EXIT_CODES_REGION,
|
||||
"docs/reference/skills.md": reference_doc.SKILLS_REGION,
|
||||
"docs/reference/page-types.md": reference_doc.PAGE_TYPES_REGION,
|
||||
}
|
||||
|
||||
|
||||
def _page_type(description: str = "A fixture type.") -> reference_doc.PageType:
|
||||
return reference_doc.PageType(
|
||||
described={
|
||||
"name": "gadget", "description": description, "subtype_field": "gadget_type",
|
||||
"root": "kb", "base_dir": "gadgets", "title_prefix": None,
|
||||
"fields": [
|
||||
{"field": "type", "required": True, "type": "string", "enum": None},
|
||||
{"field": "gadget_type", "required": True, "type": "string", "enum": ["widget"]},
|
||||
{"field": "tags", "required": False, "type": "array", "enum": None},
|
||||
],
|
||||
},
|
||||
subtypes=(reference_doc.Subtype("widget", "widgets", "Widgets"),),
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tree(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||
@@ -143,6 +271,11 @@ def tree(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(cli_contract, "all_records", lambda: dict(records))
|
||||
monkeypatch.setattr(cli_contract, "GROUPS", GROUPS)
|
||||
monkeypatch.setattr(cli, "options_texts", lambda paths: {p: " --flag A flag." for p in paths})
|
||||
monkeypatch.setattr(
|
||||
reference_doc, "shipped_skills",
|
||||
lambda: [reference_doc.Skill("fixture-skill", "Does a thing. Use when asked.")],
|
||||
)
|
||||
monkeypatch.setattr(reference_doc, "page_types", lambda: [_page_type()])
|
||||
for relative, head in HEADS.items():
|
||||
path = tmp_path / relative
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
@@ -286,3 +419,44 @@ def test_the_index_links_every_command_to_its_section(tree):
|
||||
index = (root / "docs/reference/commands/README.md").read_text(encoding="utf-8")
|
||||
assert "- [`frobnicate`](fixture-things.md#frobnicate) - Does a thing." in index
|
||||
assert "- [`gate keep`](other.md#gate-keep) - Does a thing." in index
|
||||
|
||||
|
||||
def test_a_changed_skill_or_type_makes_verify_name_its_page(tree, monkeypatch):
|
||||
runner.invoke(app, ["docs", "reference", "--apply"])
|
||||
assert docs_verify.check_reference_regions() == []
|
||||
|
||||
monkeypatch.setattr(
|
||||
reference_doc, "shipped_skills",
|
||||
lambda: [reference_doc.Skill("fixture-skill", "Does another thing. Use when asked.")],
|
||||
)
|
||||
monkeypatch.setattr(reference_doc, "page_types", lambda: [_page_type("A changed type.")])
|
||||
issues = docs_verify.check_reference_regions()
|
||||
for page in ("docs/reference/skills.md", "docs/reference/page-types.md"):
|
||||
assert any(page in issue and "docs reference --apply" in issue for issue in issues), page
|
||||
|
||||
|
||||
def test_the_fixture_skill_and_type_render(tree):
|
||||
root, _ = tree
|
||||
runner.invoke(app, ["docs", "reference", "--apply"])
|
||||
skills = (root / "docs/reference/skills.md").read_text(encoding="utf-8")
|
||||
assert "| `fixture-skill` | Does a thing. | asked. |" in skills
|
||||
types = (root / "docs/reference/page-types.md").read_text(encoding="utf-8")
|
||||
assert "- Written to: `kb/gadgets/<subtype directory>/`" in types
|
||||
assert "| `widget` | `kb/gadgets/widgets/` | Widgets |" in types
|
||||
assert "- `gadget_type` - string, one of `widget`" in types
|
||||
assert "`tags`" not in types
|
||||
|
||||
|
||||
def test_an_instance_skips_a_missing_skill_or_type_page(tree):
|
||||
"""In an instance the two pages are not the tree's to check - not even
|
||||
whether they exist."""
|
||||
root, _ = tree
|
||||
runner.invoke(app, ["docs", "reference", "--apply"])
|
||||
(root / version_mod.RELEASE_STAMP_FILENAME).write_text("{}", encoding="utf-8")
|
||||
(root / "docs/reference/skills.md").unlink()
|
||||
(root / "docs/reference/page-types.md").write_text("# Page types\n", encoding="utf-8")
|
||||
assert docs_verify.check_reference_regions() == []
|
||||
result = runner.invoke(app, ["docs", "reference", "--apply"])
|
||||
assert result.exit_code == 0, result.output
|
||||
assert "Left alone in this instance: docs/reference/skills.md, docs/reference/page-types.md" in result.output
|
||||
assert not (root / "docs/reference/skills.md").exists()
|
||||
Reference in new issue
Block a user