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
+3
-1
@@ -187,7 +187,6 @@ Scaffold a new wiki page of any type.
|
||||
- atomic: `new <type>`: Yes - single file write. `new project`: **No** for the tracker-configured case - a tracker-project write (or its human-clearance request) happens before the kb/ page write, so a failure between the two leaves a tracker project with no page (a state `review`'s check 3 already reports), never a page with no tracker project. Still a single file write when no tracker is configured
|
||||
- budget: counted
|
||||
- network: no
|
||||
- gates: human-intervention-required (`new project` only)
|
||||
|
||||
**EXAMPLES**
|
||||
|
||||
@@ -2520,6 +2519,7 @@ Check the docs that mirror the code.
|
||||
- 1 A page of the human reference is missing or lacks its marker pair, or a page under `docs/reference/commands/` belongs to no group
|
||||
- 1 A reference file's table-of-contents region is missing or stale
|
||||
- 1 A file sits directly in `docs/`, or in a directory there that is not one of its five areas
|
||||
- 1 `AGENTS.md` § Gates and `cli_contract.GATES` disagree, or a register gate that ends in exit 42 is declared by no command record
|
||||
- 1 A reference file's relative markdown link does not resolve to an existing file
|
||||
- 1 An `INSTALL.md` prerequisites region is stale
|
||||
- 1 An `INSTALL.md` prerequisites region is missing, or names a platform no tool has
|
||||
@@ -2538,6 +2538,7 @@ Check the docs that mirror the code.
|
||||
- A page of the human reference is missing or lacks its marker pair, or a page under `docs/reference/commands/` belongs to no group -> Create the page with a title, a one-sentence head and the marker pair the message names (or add the pair), then run `docs reference --apply`; `git mv` or `git rm` a page left over from a renamed or removed group
|
||||
- A reference file's table-of-contents region is missing or stale -> Run `docs toc --apply`, then re-run
|
||||
- A file sits directly in `docs/`, or in a directory there that is not one of its five areas -> Move it (`git mv`) into the area its kind of documentation belongs to, and fix the links that pointed at the old path
|
||||
- `AGENTS.md` § Gates and `cli_contract.GATES` disagree, or a register gate that ends in exit 42 is declared by no command record -> Make § Gates list exactly the register's display names, or register the gate in `cli_contract.GATES`; declare an exit-42 gate on the command that raises it
|
||||
- A reference file's relative markdown link does not resolve to an existing file -> Fix the `../` count or the target's name
|
||||
- An `INSTALL.md` prerequisites region is stale -> Run `docs prerequisites --apply`, then re-run
|
||||
- An `INSTALL.md` prerequisites region is missing, or names a platform no tool has -> Add the marker pair where that list belongs (or remove the orphaned region and its introducing prose), then run `docs prerequisites --apply`
|
||||
@@ -2559,6 +2560,7 @@ Check the docs that mirror the code.
|
||||
- Every reference file `docs toc` covers carries the current table-of-contents region for its own headings - missing and stale are one check.
|
||||
- Every relative markdown link in one of those reference files resolves to an existing file. A target's `#anchor` suffix is stripped first, and code fences and inline code spans are masked before scanning, so link syntax shown as an example is not mistaken for a real reference.
|
||||
- Every file under `docs/` sits in one of its areas - `tutorials/`, `how-to/`, `reference/`, `explanation/` or `dev/` - never directly in `docs/` or in another directory.
|
||||
- `AGENTS.md` § Gates names exactly the gates in `cli_contract.GATES` (the bold entry names, `- **<Name>.**`), and every gate in that register that ends in exit 42 is declared by at least one command record. A record naming a gate outside the register fails when `wikitool` starts, not here.
|
||||
- If `docs/` exists: every generated region of the human reference under `docs/reference/` matches what `docs reference` would write, every `cli_contract.GROUPS` entry has its page under `docs/reference/commands/`, and no page there belongs to no group. That includes the four configuration regions in `docs/reference/configuration.md`. The skill catalogue and the page types are checked only in the origin repo (no `.wikitool-release.json`): they describe the stack's shipped defaults, which an instance's own type-specs and instructions may differ from.
|
||||
- `INSTALL.md` carries one generated `<!-- wikitool:prerequisites -->` region per platform value of `tools/prerequisites.txt` (`prerequisites-<platform>` for a platform-specific one), each current; and the `<!-- setup-question: <key> -->` markers in `instructions/setup-instance.md` and `INSTALL.md` name the same set of keys, so a question the agent asks is never one the human guide leaves out, nor the reverse.
|
||||
- `VERSION` parses and the newest versioned `CHANGES.md` entry names it. An entry that crosses the compatibility boundary from the last release carries `**Breaking Change:**`, has a migration document or `**Migration:** none required`, and its `**Migration:**` line names every required migration document targeting it.
|
||||
|
||||
@@ -83,8 +83,8 @@ class Failure:
|
||||
EXIT STATUS renders one `<code> <cause>` line per entry, ON FAILURE one
|
||||
`<cause> -> <reaction>` line - the cause is repeated on purpose, so each
|
||||
ON FAILURE line reads on its own. `code` is 1 (validation error) or 42
|
||||
(a gate needs clearance; only on a command whose `Properties.gates` is
|
||||
non-empty), or 0 for an outcome a caller could mistake for a failure but
|
||||
(a human must see the output before anything proceeds - a declared gate, or
|
||||
a case like `new project`'s that is no gate), or 0 for an outcome a caller could mistake for a failure but
|
||||
that is not one (an unreachable remote reported and skipped). An empty
|
||||
`reaction` renders the EXIT STATUS line only.
|
||||
|
||||
@@ -107,6 +107,28 @@ class Failure:
|
||||
raise ValueError(f"cli_contract: Failure.code must be 0, 1 or 42, not {self.code}")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GateSpec:
|
||||
"""One gate as `AGENTS.md` § Gates names it: `display` is the bold entry name
|
||||
there, `exits_42` is whether it ends in a human clearance (the Iteration Budget
|
||||
Gate refuses with exit 1 instead)."""
|
||||
display: str
|
||||
exits_42: bool
|
||||
|
||||
|
||||
# The one list of gates, keyed by the name a record declares in `Properties.gates`.
|
||||
# `docs verify` holds AGENTS.md § Gates to exactly these display names.
|
||||
GATES: dict[str, GateSpec] = {
|
||||
"mass-update": GateSpec("Mass-Update Gate", True),
|
||||
"publish-remote": GateSpec("Publish-Remote Gate", True),
|
||||
"rebase-review": GateSpec("Rebase-Review Gate", True),
|
||||
"upload-review": GateSpec("Upload Review Gate", True),
|
||||
"guideline-push": GateSpec("Guideline Push Gate", True),
|
||||
# Applies to every command, so no record declares it.
|
||||
"iteration-budget": GateSpec("Iteration Budget Gate / Loop-Breaker", False),
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Properties:
|
||||
effect: Effect
|
||||
@@ -116,6 +138,14 @@ class Properties:
|
||||
network: Network = Network.NO
|
||||
gates: tuple[str, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
unknown = [gate for gate in self.gates if gate not in GATES]
|
||||
if unknown:
|
||||
raise ValueError(
|
||||
f"cli_contract: unknown gate {', '.join(repr(g) for g in unknown)} - "
|
||||
f"declare it in cli_contract.GATES first (known: {', '.join(sorted(GATES))})"
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class CommandRecord:
|
||||
@@ -154,10 +184,6 @@ class CommandRecord:
|
||||
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"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -62,6 +62,11 @@ documentation, plus the unshipped `docs/dev/`. A page dropped straight into
|
||||
`docs/` belongs to no kind, and its reader cannot tell from the path what it
|
||||
is for.
|
||||
|
||||
A tenth checks a list that three documents once each carried in their own form: the
|
||||
gates. `AGENTS.md` § Gates is the one list, `cli_contract.GATES` the register the
|
||||
command records are held to, and the check keeps the two equal and every exit-42 gate
|
||||
raised by some command.
|
||||
|
||||
Everything here is a hard oracle: a set comparison or a regex, no judgment.
|
||||
Content quality of the contracts themselves stays with the LLM.
|
||||
"""
|
||||
@@ -773,6 +778,55 @@ def check_docs_layout() -> list[str]:
|
||||
return issues
|
||||
|
||||
|
||||
GATES_SECTION_RE = re.compile(r"^## Gates[ \t]*\n(.*?)(?=^## |\Z)", re.MULTILINE | re.DOTALL)
|
||||
GATE_ENTRY_RE = re.compile(r"^- \*\*(.+?)\.\*\*", re.MULTILINE)
|
||||
|
||||
|
||||
def check_gates_list() -> list[str]:
|
||||
"""`AGENTS.md` § Gates names exactly the gates `cli_contract.GATES` knows, and
|
||||
every gate that ends in exit 42 is declared by at least one command record.
|
||||
|
||||
§ Gates is the one list of gates (invariant 8); the register is what the
|
||||
records are held to at import (`Properties` refuses a name outside it). This
|
||||
closes the loop in both directions: an entry the register lacks, a register
|
||||
entry § Gates never mentions, and a gate that clears with a human but that no
|
||||
command actually raises. An entry is a bullet whose bold run ends in a full
|
||||
stop (`- **Mass-Update Gate.**`).
|
||||
"""
|
||||
from chemenu import cli # noqa: F401 - importing it registers every record
|
||||
|
||||
agents = config.ROOT / "AGENTS.md"
|
||||
if not agents.is_file():
|
||||
return [f"{rel_path(agents)} is missing - it carries the list of gates"]
|
||||
section = GATES_SECTION_RE.search(agents.read_text(encoding="utf-8"))
|
||||
if section is None:
|
||||
return [f"{rel_path(agents)} has no `## Gates` section"]
|
||||
|
||||
listed = set(GATE_ENTRY_RE.findall(section.group(1)))
|
||||
known = {spec.display for spec in cli_contract.GATES.values()}
|
||||
issues = [
|
||||
f"{rel_path(agents)} § Gates does not list `{name}`, which cli_contract.GATES knows - "
|
||||
"add its `- **<Name>.**` entry"
|
||||
for name in sorted(known - listed)
|
||||
]
|
||||
issues += [
|
||||
f"{rel_path(agents)} § Gates lists `{name}`, which cli_contract.GATES does not know - "
|
||||
"register it there or drop the entry"
|
||||
for name in sorted(listed - known)
|
||||
]
|
||||
|
||||
declared = {
|
||||
gate for rec in cli_contract.all_records().values() for gate in rec.properties.gates
|
||||
}
|
||||
issues += [
|
||||
f"cli_contract.GATES lists `{key}` as ending in exit 42, but no command record declares "
|
||||
"it in `Properties.gates` - declare it on the command that raises it, or drop the entry"
|
||||
for key, spec in sorted(cli_contract.GATES.items())
|
||||
if spec.exits_42 and key not in declared
|
||||
]
|
||||
return issues
|
||||
|
||||
|
||||
def check_toc_regions() -> list[str]:
|
||||
"""Every reference file over the line threshold carries a current TOC.
|
||||
|
||||
@@ -1293,6 +1347,10 @@ def check_breaking_change_for_boundary() -> list[str]:
|
||||
"Every file under `docs/` sits in one of its areas - `tutorials/`, `how-to/`, "
|
||||
"`reference/`, `explanation/` or `dev/` - never directly in `docs/` or in another "
|
||||
"directory.",
|
||||
"`AGENTS.md` § Gates names exactly the gates in `cli_contract.GATES` (the bold entry "
|
||||
"names, `- **<Name>.**`), and every gate in that register that ends in exit 42 is "
|
||||
"declared by at least one command record. A record naming a gate outside the register "
|
||||
"fails when `wikitool` starts, not here.",
|
||||
"If `docs/` exists: every generated region of the human reference under "
|
||||
"`docs/reference/` matches what `docs reference` would write, every `cli_contract.GROUPS` "
|
||||
"entry has its page under `docs/reference/commands/`, and no page there belongs to no "
|
||||
@@ -1373,6 +1431,12 @@ def check_breaking_change_for_boundary() -> list[str]:
|
||||
reaction="Move it (`git mv`) into the area its kind of documentation belongs to, and "
|
||||
"fix the links that pointed at the old path",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="`AGENTS.md` § Gates and `cli_contract.GATES` disagree, or a register gate "
|
||||
"that ends in exit 42 is declared by no command record",
|
||||
reaction="Make § Gates list exactly the register's display names, or register the "
|
||||
"gate in `cli_contract.GATES`; declare an exit-42 gate on the command that raises it",
|
||||
),
|
||||
cli_contract.Failure(
|
||||
cause="A reference file's relative markdown link does not resolve to an existing "
|
||||
"file",
|
||||
@@ -1430,6 +1494,7 @@ def verify():
|
||||
+ check_changelog_budget()
|
||||
+ check_no_issue_references()
|
||||
+ check_docs_layout()
|
||||
+ check_gates_list()
|
||||
+ check_reference_regions()
|
||||
+ check_toc_regions()
|
||||
+ check_reference_targets()
|
||||
@@ -1454,6 +1519,7 @@ def verify():
|
||||
f"{len(IGNORE_CANARIES)} ignore canaries clear, "
|
||||
f"no issue references in {len(shipped_prose())} shipped document(s) or command help, "
|
||||
f"every docs/ file in one of its {len(DOCS_AREAS)} areas, "
|
||||
f"AGENTS.md § Gates naming the {len(cli_contract.GATES)} registered gates, "
|
||||
f"the reference under {reference_doc.REFERENCE_DIR}/ current{reference_scope}, "
|
||||
f"tables of contents current and every link resolving on "
|
||||
f"{len(toc.target_files())} reference file(s), "
|
||||
|
||||
@@ -416,7 +416,7 @@ def check_publish_remotes() -> Check:
|
||||
stops being a typo and starts being a disclosure.
|
||||
|
||||
Both absent states say **armed** or **not armed** rather than only naming
|
||||
the file. AGENTS.md lists this among the three limits enforced in code, so a
|
||||
the file. AGENTS.md lists this among the limits enforced in code, so a
|
||||
line that reports the file's absence and leaves the reader to infer what
|
||||
that means about the gate is how a checkout ends up trusting a safeguard
|
||||
that is not running - which is worse than having none.
|
||||
|
||||
@@ -103,7 +103,7 @@ def _run(args: list[str], input: Optional[str] = None,
|
||||
# public repository, where a force-push does not take it back: the objects stay
|
||||
# fetchable by SHA until someone expires the server's reflogs.
|
||||
#
|
||||
# Like the other two gates this refuses with exit 42 and has **no flag that
|
||||
# Like the other token-cleared gates this refuses with exit 42 and has **no flag that
|
||||
# opens it**. The way past it is to name the URL in the file, which is an edit
|
||||
# the user makes deliberately rather than something an agent can decide mid-run.
|
||||
|
||||
|
||||
@@ -379,7 +379,6 @@ def _ensure_tracker_project(page_title: str, *, resume: bool) -> Optional[str]:
|
||||
"project with no page (a state `review`'s check 3 already reports), never a page with "
|
||||
"no tracker project. Still a single file write when no tracker is configured",
|
||||
budget=cli_contract.Budget.COUNTED,
|
||||
gates=("human-intervention-required (`new project` only)",),
|
||||
),
|
||||
notes=(
|
||||
"A title becomes a file name, so it must be valid and unique on Windows and macOS as "
|
||||
|
||||
@@ -41,11 +41,11 @@ class HumanInterventionRequired(ChemenuError):
|
||||
one write `chemenu.tasks.protocol.TaskWriter` asks of it.
|
||||
|
||||
The CLI adapter (`wikitool new project`, Gitea #126) renders this the same
|
||||
way it renders the four named gates in AGENTS.md's Gates section:
|
||||
way it renders the named gates in AGENTS.md's Gates section:
|
||||
`commands._util.needs_clearance(str(exc))`, exit code 42 - "a human must
|
||||
see the command's output before anything proceeds" applies here for the
|
||||
same reason it applies to a mass update, just for a different cause. It is
|
||||
not a fifth *named* gate (no threshold, no `--confirm` token to compute),
|
||||
not a *named* gate (no threshold, no `--confirm` token to compute),
|
||||
but the same exit code and the same posture: show the message verbatim,
|
||||
stop, and do not improvise a workaround (AGENTS.md invariant 7).
|
||||
|
||||
|
||||
@@ -82,7 +82,7 @@ def check_trace_destination(root: Path) -> None:
|
||||
dev checkout - so this guard resolves the same policy rather than reading
|
||||
`WIKI_TRACE` a second time, which is how it used to drift from the writer
|
||||
(AGENTS.md invariant 8). Today nothing on this path emits - the writer is
|
||||
wired into `cli.main()` and the two gates, none of which run here - so
|
||||
wired into `cli.main()` and the gates, none of which run here - so
|
||||
this is a guard against the future rather than a fix for the present. It
|
||||
is worth having anyway: the sync that keeps this checkout current is
|
||||
`git reset --hard`, so a trace written into the tree is both a per-request
|
||||
|
||||
@@ -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