diff --git a/CHANGES.md b/CHANGES.md index 4b7b83e..063eef8 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 ### 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 diff --git a/VERSION b/VERSION index 77167cc..1af214d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -7.1.0-beta.20 +7.1.0-beta.21 diff --git a/tools/chemenu/cli_contract.py b/tools/chemenu/cli_contract.py index 91a7276..c9c2361 100644 --- a/tools/chemenu/cli_contract.py +++ b/tools/chemenu/cli_contract.py @@ -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] diff --git a/tools/chemenu/tests/test_cli.py b/tools/chemenu/tests/test_cli.py index dab4d76..db7f856 100644 --- a/tools/chemenu/tests/test_cli.py +++ b/tools/chemenu/tests/test_cli.py @@ -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 == [] diff --git a/tools/chemenu/tests/test_cli_contract.py b/tools/chemenu/tests/test_cli_contract.py index b028c58..9935791 100644 --- a/tools/chemenu/tests/test_cli_contract.py +++ b/tools/chemenu/tests/test_cli_contract.py @@ -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.") diff --git a/tools/chemenu/tests/test_docs_verify.py b/tools/chemenu/tests/test_docs_verify.py index d1971c4..767dbcd 100644 --- a/tools/chemenu/tests/test_docs_verify.py +++ b/tools/chemenu/tests/test_docs_verify.py @@ -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 --invented "),), 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)