tools: command records - NOTES always bullets, examples held by tests (#142)
CI / verify (push) Successful in 1m12s
Release / release (push) Successful in 35s

Files changed:
- CHANGES.md
- VERSION
- tools/chemenu/cli_contract.py
- tools/chemenu/tests/test_cli.py
- tools/chemenu/tests/test_cli_contract.py
- tools/chemenu/tests/test_docs_verify.py
This commit is contained in:
torben committed 2026-09-26 09:33:46 +02:00
1 parent 5bfbb49d74
commit 5aae7fed1b
6 files changed
+54 -23

No files matched your search

+10 -1
View File
@@ -59,7 +59,7 @@ concern - readable here, never shipped as something to parse.
---
## 7.1.0-beta.20 - 2026-09-26 - Command records, Instance health group: one bullet per check, examples
## 7.1.0-beta.21 - 2026-09-26 - Command records: NOTES is always a tuple of bullets; every record's examples are tested
**Author:** Torben Nehmer
@@ -89,6 +89,7 @@ concern - readable here, never shipped as something to parse.
- Command records, Content migrations group: one line per cause, examples, prohibitions
- Command records, Private instances group: one line per cause, examples, prohibitions
- Command records, Instance health group: one bullet per check, examples
- Command records: NOTES is always a tuple of bullets; every record's examples are tested
<!-- /wikitool:bumps -->
### CalDAV task-tracker provider (Nextcloud Tasks, iOS Reminders); review reports unknown-value findings instead of skipping them
@@ -333,6 +334,14 @@ dropped rather than moved: the record justified the conventions `FAIL` by `xref`
out of the section headings, while the check's own docstring calls those headings cosmetic and
grounds the `FAIL` in the file binding every page.
### Command records: NOTES is always a tuple of bullets; every record's examples are tested
With every group rewritten, `CommandRecord.notes` no longer accepts the single-paragraph string
it carried over from the first pass; a record that passes one is refused at import. Two tests
over the real registry hold what the rewrite established: every command has at least one
example, and every command with a gate shows how its clearance is passed back in (`--confirm`,
`--confirm-rebase` or `--resume`).
---
## 7.0.0 - 2026-09-22 - Task-Tracker-Anbindung: Vorhaben als Seitenart, Verpflichtungsschicht, Weekly Review als Read-Time-Join
+1 -1
View File
@@ -1 +1 @@
7.1.0-beta.20
7.1.0-beta.21
+14 -16
View File
@@ -11,11 +11,12 @@ the `###`-level grouping and rendering order, unchanged from what
Phase 1 (Gitea #121) filled every record mechanically and word-for-word from
the two tables `tools/CONTRACT.md` used to carry. Phase 2 (Gitea #142)
rewrites them group by group: NOTES into present-tense bullets, one
`Failure` per cause, EXAMPLES/NEVER/SEE ALSO filled, and "why" moved out to
a code comment where the behaviour is implemented. A sentence several
records carry verbatim lives here once (`token_gate_reaction`, ...), so the
output repeats it and the source does not.
rewrote them: NOTES as present-tense bullets, one `Failure` per cause,
EXAMPLES/NEVER/SEE ALSO filled, and "why" moved out to a code comment where
the behaviour is implemented. How a record is written is the `CommandRecord`
docstring's job. A sentence several records carry verbatim lives here once
(`token_gate_reaction`, ...), so the output repeats it and the source does
not.
"""
from __future__ import annotations
@@ -120,22 +121,22 @@ class CommandRecord:
- `see_also`: related commands and the instruction that uses this one.
It is the only place another command may be named for context - a
behaviour this command shares with another is stated here in full,
not as "same as `X`".
A plain `str` for `notes` is still accepted and renders as one paragraph:
the phase-1 form, kept only until every group has been rewritten into
bullets (Gitea #142)."""
not as "same as `X`"."""
path: str
summary: str
synopsis: tuple[Variant, ...]
properties: Properties
notes: tuple[str, ...] | str
notes: tuple[str, ...]
failures: tuple[Failure, ...]
examples: tuple[str, ...] = ()
never: tuple[str, ...] = ()
see_also: tuple[str, ...] = ()
def __post_init__(self) -> None:
if isinstance(self.notes, str):
raise ValueError(
f"cli_contract: {self.path!r} notes must be a tuple of bullets, not one string"
)
if not self.properties.gates and any(f.code == 42 for f in self.failures):
raise ValueError(
f"cli_contract: {self.path!r} lists an exit-42 cause but declares no gate"
@@ -294,11 +295,8 @@ def _labelled(failure: Failure, text: str) -> str:
return f"{failure.label}: {text}" if failure.label else text
def _notes_lines(notes: tuple[str, ...] | str) -> list[str]:
"""NOTES as rendered lines: one `- ` bullet per entry, or the phase-1
paragraph unchanged."""
if isinstance(notes, str):
return [notes]
def _notes_lines(notes: tuple[str, ...]) -> list[str]:
"""NOTES as rendered lines: one `- ` bullet per entry."""
return [f"- {note}" for note in notes]
+19
View File
@@ -235,3 +235,22 @@ def test_a_usage_error_is_unframed_and_names_wikitool(monkeypatch):
text = out + err
assert "Usage: wikitool publish" in text
assert not FRAME_CHARS & set(text)
def test_every_record_shows_at_least_one_example():
"""Every real command record carries a copyable example (Gitea #142)."""
missing = [path for path, rec in cli_contract.all_records().items() if not rec.examples]
assert missing == []
def test_every_gated_record_shows_its_re_run_after_exit_42():
"""A command with a gate shows how the clearance is passed back in -
`--confirm`, `--confirm-rebase`, or `new project`'s `--resume`."""
clearing_flags = ("--confirm", "--confirm-rebase", "--resume")
missing = [
path
for path, rec in cli_contract.all_records().items()
if rec.properties.gates
and not any(flag in example for example in rec.examples for flag in clearing_flags)
]
assert missing == []
+7 -2
View File
@@ -23,7 +23,7 @@ def _fixture_record(**overrides) -> cc.CommandRecord:
atomic="Yes - single file write",
budget=cc.Budget.COUNTED,
),
notes="Frobnicates the named widget in place.",
notes=("Frobnicates the named widget in place.",),
failures=(cc.Failure(label="", cause="Widget not found", reaction="Fix the name and retry once"),),
)
defaults.update(overrides)
@@ -218,7 +218,7 @@ def test_render_markdown_section_has_no_options_heading():
section = cc.render_markdown_section(rec)
assert "OPTIONS" not in section
assert "#### `frobnicate`" in section
assert rec.notes in section
assert f"- {rec.notes[0]}" in section
def test_notes_tuple_renders_one_bullet_per_entry():
@@ -297,3 +297,8 @@ def test_exit_42_cause_without_a_gate_is_refused():
def test_failure_code_outside_0_1_42_is_refused():
with pytest.raises(ValueError):
cc.Failure(cause="Crash", reaction="Report", code=2)
def test_notes_as_one_string_is_refused():
with pytest.raises(ValueError, match="tuple of bullets"):
_fixture_record(notes="One paragraph, the phase-1 form.")
+3 -3
View File
@@ -52,7 +52,7 @@ def _fixture_record(path: str) -> cli_contract.CommandRecord:
atomic="Yes",
budget=cli_contract.Budget.COUNTED,
),
notes="Does a thing, mechanically.",
notes=("Does a thing, mechanically.",),
failures=(cli_contract.Failure(label="", cause="It broke", reaction="Fix and retry"),),
)
@@ -150,7 +150,7 @@ def test_a_phantom_synopsis_flag_is_reported(monkeypatch):
summary="Does a thing.",
synopsis=(cli_contract.Variant(usage="frobnicate --flag <v> --invented <v>"),),
properties=_fixture_record("frobnicate").properties,
notes="Does a thing.",
notes=("Does a thing.",),
failures=(),
)
param = click.Option(["--flag"])
@@ -188,7 +188,7 @@ def test_a_boolean_flag_pair_is_satisfied_by_either_spelling(monkeypatch):
summary="Does a thing.",
synopsis=(cli_contract.Variant(usage="frobnicate [--no-push]"),),
properties=_fixture_record("frobnicate").properties,
notes="Does a thing.",
notes=("Does a thing.",),
failures=(),
)
param = click.Option(["--push/--no-push"], default=True)