AGENTS.md § Gates is the one gate list; cli_contract.GATES and docs verify hold it
The gate list contradicted itself across AGENTS.md, instructions/gates.md and the command records' gate names. The Rebase-Review Gate is now listed, `new project`'s exit 42 is no longer a pseudo-gate, gates.md and the explanation page link to the one list instead of counting, and a register plus a docs verify check keep the three in step. Files changed: - AGENTS.md - CHANGES.md - VERSION - docs/explanation/why-gates-are-code.md - docs/reference/commands/pages.md - docs/reference/commands/types-instructions-and-docs.md - docs/reference/exit-codes.md - instructions/gates.md - tools/CONTRACT.md - tools/chemenu/cli_contract.py - tools/chemenu/commands/docs_verify.py - tools/chemenu/commands/doctor.py - tools/chemenu/commands/git_publish.py - tools/chemenu/commands/new_page.py - tools/chemenu/errors.py - tools/chemenu/mcp/server.py - tools/chemenu/tests/test_cli_contract.py - tools/chemenu/tests/test_docs_verify.py - tools/chemenu/tests/test_git_publish.py - tools/chemenu/tests/test_reference_doc.py Co-Authored-By: Claude Code <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
1 parent
1559f4b405
commit
e9f4759727
20 files changed
+269
-52
No files matched your search
@@ -289,9 +289,37 @@ def test_on_failure_omitted_when_no_cause_has_a_reaction():
|
||||
assert "ON FAILURE" not in cc.render_markdown_section(rec)
|
||||
|
||||
|
||||
def test_exit_42_cause_without_a_gate_is_refused():
|
||||
with pytest.raises(ValueError, match="no gate"):
|
||||
_fixture_record(failures=(cc.Failure(cause="Gate", reaction="Stop", code=42),))
|
||||
def test_exit_42_cause_without_a_gate_is_allowed():
|
||||
"""`new project`'s exit 42 is a human-intervention stop, not a gate: it needs no
|
||||
register entry, and it is still listed as an exit 42 cause."""
|
||||
rec = _fixture_record(
|
||||
failures=(cc.Failure(cause="A human must act", reaction="Show it and stop", code=42),)
|
||||
)
|
||||
assert rec.properties.gates == ()
|
||||
assert cc._exit_codes(rec) == [0, 42]
|
||||
assert cc.exit_status_entries(rec)[-1] == (42, "A human must act")
|
||||
|
||||
|
||||
def test_a_gate_name_outside_the_register_is_refused():
|
||||
with pytest.raises(ValueError, match="unknown gate 'made-up'"):
|
||||
cc.Properties(
|
||||
effect=cc.Effect.WRITE,
|
||||
idempotent=cc.Idempotent.NO,
|
||||
atomic="No",
|
||||
budget=cc.Budget.COUNTED,
|
||||
gates=("mass-update", "made-up"),
|
||||
)
|
||||
|
||||
|
||||
def test_every_registered_gate_is_a_usable_name():
|
||||
for name in cc.GATES:
|
||||
cc.Properties(
|
||||
effect=cc.Effect.READ,
|
||||
idempotent=cc.Idempotent.YES,
|
||||
atomic="Read-only",
|
||||
budget=cc.Budget.COUNTED,
|
||||
gates=(name,),
|
||||
)
|
||||
|
||||
|
||||
def test_failure_code_outside_0_1_42_is_refused():
|
||||
|
||||
@@ -958,6 +958,97 @@ def test_verify_raises_when_a_docs_file_is_outside_every_area(monkeypatch):
|
||||
docs_verify.verify()
|
||||
|
||||
|
||||
# --- AGENTS.md § Gates against cli_contract.GATES ----------------------------
|
||||
|
||||
|
||||
def _agents_with_gates(tmp_path, monkeypatch, names: list[str], section: str = "## Gates") -> None:
|
||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||
entries = "".join(f"- **{name}.** Does something.\n" for name in names)
|
||||
(tmp_path / "AGENTS.md").write_text(
|
||||
f"# AGENTS\n\n{section}\n\nThese are enforced in code.\n\n{entries}\n"
|
||||
"## Next\n\n- **Not A Gate.** Outside the section.\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
|
||||
def _all_display_names() -> list[str]:
|
||||
return [spec.display for spec in cli_contract.GATES.values()]
|
||||
|
||||
|
||||
def test_this_repos_gates_section_matches_the_register():
|
||||
assert docs_verify.check_gates_list() == []
|
||||
|
||||
|
||||
def test_a_gates_section_naming_exactly_the_register_passes(tmp_path, monkeypatch):
|
||||
_agents_with_gates(tmp_path, monkeypatch, _all_display_names())
|
||||
assert docs_verify.check_gates_list() == []
|
||||
|
||||
|
||||
def test_a_register_gate_missing_from_the_section_is_reported(tmp_path, monkeypatch):
|
||||
names = [n for n in _all_display_names() if n != "Rebase-Review Gate"]
|
||||
_agents_with_gates(tmp_path, monkeypatch, names)
|
||||
issues = docs_verify.check_gates_list()
|
||||
assert len(issues) == 1
|
||||
assert "does not list `Rebase-Review Gate`" in issues[0]
|
||||
|
||||
|
||||
def test_a_section_entry_the_register_does_not_know_is_reported(tmp_path, monkeypatch):
|
||||
_agents_with_gates(tmp_path, monkeypatch, [*_all_display_names(), "Invented Gate"])
|
||||
issues = docs_verify.check_gates_list()
|
||||
assert len(issues) == 1
|
||||
assert "lists `Invented Gate`" in issues[0]
|
||||
|
||||
|
||||
def test_a_bullet_outside_the_gates_section_is_not_an_entry(tmp_path, monkeypatch):
|
||||
_agents_with_gates(tmp_path, monkeypatch, _all_display_names())
|
||||
assert "Not A Gate" not in " ".join(docs_verify.check_gates_list())
|
||||
|
||||
|
||||
def test_a_tree_with_no_gates_section_is_reported(tmp_path, monkeypatch):
|
||||
_agents_with_gates(tmp_path, monkeypatch, [], section="## Elsewhere")
|
||||
issues = docs_verify.check_gates_list()
|
||||
assert len(issues) == 1 and "no `## Gates` section" in issues[0]
|
||||
|
||||
|
||||
def test_a_tree_with_no_agents_md_is_reported(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||
issues = docs_verify.check_gates_list()
|
||||
assert len(issues) == 1 and "AGENTS.md is missing" in issues[0]
|
||||
|
||||
|
||||
def test_an_exit_42_gate_no_record_declares_is_reported(tmp_path, monkeypatch):
|
||||
_agents_with_gates(tmp_path, monkeypatch, _all_display_names())
|
||||
records = {
|
||||
path: rec
|
||||
for path, rec in cli_contract.all_records().items()
|
||||
if "upload-review" not in rec.properties.gates
|
||||
}
|
||||
monkeypatch.setattr(cli_contract, "all_records", lambda: records)
|
||||
issues = docs_verify.check_gates_list()
|
||||
assert len(issues) == 1
|
||||
assert "`upload-review`" in issues[0] and "no command record declares it" in issues[0]
|
||||
|
||||
|
||||
def test_the_budget_gate_needs_no_declaring_record(tmp_path, monkeypatch):
|
||||
"""It refuses with exit 1 and applies to every command, so no record names it."""
|
||||
_agents_with_gates(tmp_path, monkeypatch, _all_display_names())
|
||||
declared = {g for r in cli_contract.all_records().values() for g in r.properties.gates}
|
||||
assert "iteration-budget" not in declared
|
||||
assert docs_verify.check_gates_list() == []
|
||||
|
||||
|
||||
def test_new_declares_no_gate_but_still_lists_its_exit_42():
|
||||
rec = cli_contract.get("new")
|
||||
assert rec.properties.gates == ()
|
||||
assert any(f.code == 42 and f.label == "new project" for f in rec.failures)
|
||||
|
||||
|
||||
def test_verify_raises_when_the_gates_list_disagrees(monkeypatch):
|
||||
monkeypatch.setattr(docs_verify, "check_gates_list", lambda: ["disagree"])
|
||||
with pytest.raises(typer.Exit):
|
||||
docs_verify.verify()
|
||||
|
||||
|
||||
# The rewrite instructions/migrations/9.0.0-docs-diataxis.md tells an instance
|
||||
# to run, as the regex it spells out for `sed -E`.
|
||||
_DIATAXIS_NAMES = (
|
||||
|
||||
@@ -1406,7 +1406,7 @@ def test_empty_allowlist_refuses_everything(repo):
|
||||
|
||||
|
||||
def test_gate_has_no_flag_that_opens_it(repo):
|
||||
"""The other two gates clear with a token; this one deliberately does not,
|
||||
"""The other gates clear with a token; this one deliberately does not,
|
||||
because the right fix is a deliberate edit by the user."""
|
||||
import inspect
|
||||
|
||||
|
||||
@@ -268,7 +268,7 @@ def _page_type(description: str = "A fixture type.") -> reference_doc.PageType:
|
||||
def tree(tmp_path, monkeypatch):
|
||||
real_root = config.ROOT
|
||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
||||
records = {"frobnicate": _record("frobnicate"), "gate keep": _record("gate keep", ("keep",))}
|
||||
records = {"frobnicate": _record("frobnicate"), "gate keep": _record("gate keep", ("mass-update",))}
|
||||
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})
|
||||
@@ -416,7 +416,7 @@ def test_exit_codes_lists_each_gate_and_its_exit_42_causes(tree):
|
||||
root, _ = tree
|
||||
runner.invoke(app, ["docs", "reference", "--apply"])
|
||||
page = (root / "docs/reference/exit-codes.md").read_text(encoding="utf-8")
|
||||
assert "### keep\n\n- [`gate keep`](commands/other.md#gate-keep)" in page
|
||||
assert "### mass-update\n\n- [`gate keep`](commands/other.md#gate-keep)" in page
|
||||
assert "- [`gate keep`](commands/other.md#gate-keep): Needs a person" in page
|
||||
assert "frobnicate" not in page
|
||||
|
||||
|
||||
Reference in new issue
Block a user