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
@@ -162,7 +162,7 @@ instance comes only from a release),
|
|||||||
[docs/explanation/language-boundaries.md](docs/explanation/language-boundaries.md) (why the
|
[docs/explanation/language-boundaries.md](docs/explanation/language-boundaries.md) (why the
|
||||||
control plane is English everywhere and the KB language is a value, and why the axis is the
|
control plane is English everywhere and the KB language is a value, and why the axis is the
|
||||||
reader rather than the owner),
|
reader rather than the owner),
|
||||||
[docs/explanation/why-gates-are-code.md](docs/explanation/why-gates-are-code.md) (why the five
|
[docs/explanation/why-gates-are-code.md](docs/explanation/why-gates-are-code.md) (why the
|
||||||
gates in [Gates](#gates) are code rather than instruction),
|
gates in [Gates](#gates) are code rather than instruction),
|
||||||
[docs/explanation/version-model.md](docs/explanation/version-model.md) (why a version number
|
[docs/explanation/version-model.md](docs/explanation/version-model.md) (why a version number
|
||||||
answers a compatibility question and a migration question separately), and
|
answers a compatibility question and a migration question separately), and
|
||||||
@@ -278,8 +278,8 @@ that take a title. A result cut short by `--limit` says so and names the total.
|
|||||||
|
|
||||||
## Gates
|
## Gates
|
||||||
|
|
||||||
Five limits are enforced in code rather than by instruction, because a prompt-level limit is
|
These limits are enforced in code rather than by instruction, because a prompt-level limit is
|
||||||
one an agent can talk itself past.
|
one an agent can talk itself past. This section is the one list of them.
|
||||||
|
|
||||||
- **Mass-Update Gate.** `publish` exits **42** on a change touching too many files, printing
|
- **Mass-Update Gate.** `publish` exits **42** on a change touching too many files, printing
|
||||||
the file list and the `--confirm <token>` line that publishes it once the user approves. The
|
the file list and the `--confirm <token>` line that publishes it once the user approves. The
|
||||||
@@ -287,6 +287,10 @@ one an agent can talk itself past.
|
|||||||
- **Publish-Remote Gate.** `publish` exits **42** on a push to a URL this checkout has not
|
- **Publish-Remote Gate.** `publish` exits **42** on a push to a URL this checkout has not
|
||||||
declared in `.wikitool-remotes.json`. It has no token and no flag: the way past it is a
|
declared in `.wikitool-remotes.json`. It has no token and no flag: the way past it is a
|
||||||
deliberate edit by the user, never by an agent.
|
deliberate edit by the user, never by an agent.
|
||||||
|
- **Rebase-Review Gate.** `sync` and `publish` exit **42** on a rebase whose incoming commits
|
||||||
|
touch a file this session is also changing, printing the incoming commits, the overlapping
|
||||||
|
files, their diff and the `--confirm-rebase <token>` line that rebases once the user has
|
||||||
|
judged that the two changes do not collide logically.
|
||||||
- **Upload Review Gate.** `upload accept` exits **42** on an MCP `submit` tool submission
|
- **Upload Review Gate.** `upload accept` exits **42** on an MCP `submit` tool submission
|
||||||
nobody has cleared yet, printing its manifest and the `--confirm <token>` line that promotes
|
nobody has cleared yet, printing its manifest and the `--confirm <token>` line that promotes
|
||||||
it once the user approves - same shape as the Mass-Update Gate, one submission at a time.
|
it once the user approves - same shape as the Mass-Update Gate, one submission at a time.
|
||||||
|
|||||||
+2
-1
@@ -14,7 +14,7 @@ place only: `instructions/dev/version-parts.md` § The candidate model.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 9.0.0-beta.8 - 2026-10-07 - Die Formen der vier handgeschriebenen .wikitool-*.json stehen je in einem Datensatz (config_shapes), aus dem Lader, doctor und die Konfigurationsreferenz lesen
|
## 9.0.0-beta.9 - 2026-10-07 - Gate-Liste: AGENTS.md § Gates ist die eine Liste (mit Rebase-Review Gate), cli_contract.GATES hält sie, docs verify vergleicht beide
|
||||||
|
|
||||||
**Author:** Torben Nehmer
|
**Author:** Torben Nehmer
|
||||||
|
|
||||||
@@ -33,6 +33,7 @@ place only: `instructions/dev/version-parts.md` § The candidate model.
|
|||||||
- docs/: how-to guides, configuration reference and two explanation pages carved out of README.md, INSTALL.md, INSTALL-MCP.md and EVALS.md; the last two retired
|
- docs/: how-to guides, configuration reference and two explanation pages carved out of README.md, INSTALL.md, INSTALL-MCP.md and EVALS.md; the last two retired
|
||||||
- INSTALL.md is an English installation tutorial; new tutorial from a first source to a first answer with a shipped sample; README.md a landing page
|
- INSTALL.md is an English installation tutorial; new tutorial from a first source to a first answer with a shipped sample; README.md a landing page
|
||||||
- Die Formen der vier handgeschriebenen .wikitool-*.json stehen je in einem Datensatz (config_shapes), aus dem Lader, doctor und die Konfigurationsreferenz lesen
|
- Die Formen der vier handgeschriebenen .wikitool-*.json stehen je in einem Datensatz (config_shapes), aus dem Lader, doctor und die Konfigurationsreferenz lesen
|
||||||
|
- Gate-Liste: AGENTS.md § Gates ist die eine Liste (mit Rebase-Review Gate), cli_contract.GATES hält sie, docs verify vergleicht beide
|
||||||
|
|
||||||
**Low impact**
|
**Low impact**
|
||||||
- version bump: Abschlussmeldung nennt Stichpunkt oder Themenabsatz statt pauschal Prosa
|
- version bump: Abschlussmeldung nennt Stichpunkt oder Themenabsatz statt pauschal Prosa
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
# Why gates are code
|
# Why gates are code
|
||||||
|
|
||||||
Chemenu has five hard limits - the Mass-Update Gate, the Publish-Remote Gate, the Upload Review
|
Chemenu's hard limits - the gates [AGENTS.md § Gates](../../AGENTS.md#gates) lists - live inside
|
||||||
Gate, the Guideline Push Gate, and the Iteration Budget Gate - and all five live inside
|
`tools/wikitool`, not in a paragraph of instructions an agent reads and follows. The rules
|
||||||
`tools/wikitool`, not in a
|
themselves, and what to do when one trips, are in [AGENTS.md § Gates](../../AGENTS.md#gates) and
|
||||||
paragraph of instructions an agent reads and follows. The rules themselves, and what to do when
|
|
||||||
one trips, are in [AGENTS.md § Gates](../../AGENTS.md#gates) and
|
|
||||||
[instructions/gates.md](../../instructions/gates.md). This page is only about the design choice
|
[instructions/gates.md](../../instructions/gates.md). This page is only about the design choice
|
||||||
underneath them: why code, and why these mechanisms in particular.
|
underneath them: why code, and why these mechanisms in particular.
|
||||||
|
|
||||||
@@ -34,8 +32,8 @@ well-written instruction - it's that code doesn't get talked into anything.
|
|||||||
|
|
||||||
## Why different mechanisms, not one
|
## Why different mechanisms, not one
|
||||||
|
|
||||||
The five gates ask four different kinds of question, and each one's shape follows from what kind
|
The gates ask different kinds of question, and each one's shape follows from what kind of
|
||||||
of question it is.
|
question it is.
|
||||||
|
|
||||||
The Mass-Update Gate asks *is this change too large to publish unreviewed* - a judgment that
|
The Mass-Update Gate asks *is this change too large to publish unreviewed* - a judgment that
|
||||||
varies changeset by changeset, so it clears with a `--confirm` token tied to the specific
|
varies changeset by changeset, so it clears with a `--confirm` token tied to the specific
|
||||||
@@ -51,7 +49,7 @@ editing `.wikitool-remotes.json` directly, outside the gate's own flow.
|
|||||||
The Upload Review Gate asks the Mass-Update Gate's own question - *is this change right?* - at
|
The Upload Review Gate asks the Mass-Update Gate's own question - *is this change right?* - at
|
||||||
the opposite end of its size range: one file from a stranger instead of a changeset from the
|
the opposite end of its size range: one file from a stranger instead of a changeset from the
|
||||||
session's own work. That similarity is exactly why it reuses the same shape (a `--confirm` token
|
session's own work. That similarity is exactly why it reuses the same shape (a `--confirm` token
|
||||||
digesting the thing being approved) rather than inventing a fourth one: the two gates differ in
|
digesting the thing being approved) rather than inventing a new one: the two differ in
|
||||||
*who* produced the change and *how much* of it there is, not in what kind of question either one
|
*who* produced the change and *how much* of it there is, not in what kind of question either one
|
||||||
is answering, so nothing about the mechanism needed to change - only the boundary it sits behind
|
is answering, so nothing about the mechanism needed to change - only the boundary it sits behind
|
||||||
did, since the submission lives in a quarantine the ordinary pipeline never reads at all rather
|
did, since the submission lives in a quarantine the ordinary pipeline never reads at all rather
|
||||||
@@ -68,14 +66,24 @@ standing, committed declaration - the capture manifests under `raw/` - and askin
|
|||||||
URLs in an allowlist as well would be a second declaration of one fact. What is left to ask is
|
URLs in an allowlist as well would be a second declaration of one fact. What is left to ask is
|
||||||
the per-run question, and that is the one a token answers.
|
the per-run question, and that is the one a token answers.
|
||||||
|
|
||||||
The Iteration Budget Gate asks a fourth kind of question - not "is this instance correct" but
|
The Rebase-Review Gate asks the one question the others cannot: *does what is arriving collide
|
||||||
|
logically with what I am about to publish?* `sync` and `publish` reconcile with the remote by
|
||||||
|
rebasing, and when the incoming commits touch a file this session is also changing, git can
|
||||||
|
produce a clean textual result out of two changes that contradict each other. Whether they do is
|
||||||
|
a question of meaning, not of text, so the tool cannot answer it - it can only refuse, and show
|
||||||
|
the incoming commits, the overlapping files and their diff to whoever can. The answer is different
|
||||||
|
for every rebase, which is why it takes the token shape: the token digests the exact upstream
|
||||||
|
state and the overlapping files, so a remote that moved again before the re-run is a new
|
||||||
|
question, asked again.
|
||||||
|
|
||||||
|
The Iteration Budget Gate asks a different kind of question - not "is this instance correct" but
|
||||||
"has this session stopped making progress." That's read from the shape of the call history
|
"has this session stopped making progress." That's read from the shape of the call history
|
||||||
itself (call count, repeated identical calls), not from anything about the content of any one
|
itself (call count, repeated identical calls), not from anything about the content of any one
|
||||||
call.
|
call.
|
||||||
|
|
||||||
## Exit 42 is a posture, and it outgrew the gates
|
## Exit 42 is a posture, and it outgrew the gates
|
||||||
|
|
||||||
Those five are the named gates, and they are not the only thing that exits 42 any more. When the
|
Those are the named gates, and they are not the only thing that exits 42 any more. When the
|
||||||
task-tracker provider layer arrived, it brought a case that looks like a gate from the outside and
|
task-tracker provider layer arrived, it brought a case that looks like a gate from the outside and
|
||||||
is not one: a provider whose API cannot create a project (Super Productivity's local REST API
|
is not one: a provider whose API cannot create a project (Super Productivity's local REST API
|
||||||
reads projects but does not write them) raises `HumanInterventionRequired`, and the command prints
|
reads projects but does not write them) raises `HumanInterventionRequired`, and the command prints
|
||||||
|
|||||||
@@ -85,7 +85,6 @@ tools/wikitool new project --name "Homelab migration" --set responsibility=infra
|
|||||||
- 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
|
- 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
|
||||||
- Counts toward the session call budget: yes
|
- Counts toward the session call budget: yes
|
||||||
- Can reach the network: no
|
- Can reach the network: no
|
||||||
- Gates: human-intervention-required (`new project` only) - see [exit codes and gates](../exit-codes.md)
|
|
||||||
|
|
||||||
**Exit codes**
|
**Exit codes**
|
||||||
|
|
||||||
|
|||||||
@@ -288,6 +288,7 @@ tools/wikitool docs verify
|
|||||||
- Every reference file `docs toc` covers carries the current table-of-contents region for its own headings - missing and stale are one check.
|
- 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 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.
|
- 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.
|
- 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.
|
- `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.
|
- `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.
|
||||||
@@ -316,6 +317,7 @@ tools/wikitool docs verify
|
|||||||
- `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 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 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` - 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` - 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 stale
|
||||||
- `1` - An `INSTALL.md` prerequisites region is missing, or names a platform no tool has
|
- `1` - An `INSTALL.md` prerequisites region is missing, or names a platform no tool has
|
||||||
|
|||||||
@@ -12,10 +12,6 @@ What each exit code means, and how a caller reacts to it, is [AGENTS.md § Tool
|
|||||||
|
|
||||||
- [`export guidelines`](commands/guideline-export.md#export-guidelines)
|
- [`export guidelines`](commands/guideline-export.md#export-guidelines)
|
||||||
|
|
||||||
### human-intervention-required (`new project` only)
|
|
||||||
|
|
||||||
- [`new`](commands/pages.md#new)
|
|
||||||
|
|
||||||
### mass-update
|
### mass-update
|
||||||
|
|
||||||
- [`publish`](commands/git.md#publish)
|
- [`publish`](commands/git.md#publish)
|
||||||
|
|||||||
+8
-13
@@ -1,13 +1,14 @@
|
|||||||
---
|
---
|
||||||
type: types/instruction.md
|
type: types/instruction.md
|
||||||
name: gates
|
name: gates
|
||||||
description: What to do when wikitool refuses a call - exit 42 (user clearance required) on publish, upload accept and export guidelines --push, and the Iteration Budget Gate and loop-breaker on every command.
|
description: What to do when wikitool refuses a call - exit 42 (user clearance required) on publish, sync, upload accept and export guidelines --push, and the Iteration Budget Gate and loop-breaker on every command.
|
||||||
---
|
---
|
||||||
|
|
||||||
# When a gate refuses a call
|
# When a gate refuses a call
|
||||||
|
|
||||||
Two limits are enforced in code rather than by instruction, because a prompt-level limit is one
|
The limits listed in [AGENTS.md § Gates](../AGENTS.md#gates) are enforced in code rather than by
|
||||||
an agent can talk itself past.
|
instruction, because a prompt-level limit is one an agent can talk itself past. This file holds
|
||||||
|
the procedure for each; it does not repeat the list.
|
||||||
|
|
||||||
**Never open a gate on your own initiative.** Not `--override-budget`, not `budget reset`, not a
|
**Never open a gate on your own initiative.** Not `--override-budget`, not `budget reset`, not a
|
||||||
`--confirm` token the user has not actually seen and approved.
|
`--confirm` token the user has not actually seen and approved.
|
||||||
@@ -34,24 +35,18 @@ Read the exit code first - it says which of these applies:
|
|||||||
## Exit 42: user clearance required
|
## Exit 42: user clearance required
|
||||||
|
|
||||||
A `wikitool` command that exits **42** is not reporting an error. It is refusing to act until a
|
A `wikitool` command that exits **42** is not reporting an error. It is refusing to act until a
|
||||||
human has *read its output*. Five gates use it today - the Mass-Update Gate (`publish`, on a
|
human has *read its output*. Which gates use it is [AGENTS.md § Gates](../AGENTS.md#gates); the
|
||||||
change touching 10 or more counted files), the rebase-review gate (`sync` and `publish`, on
|
rule is about the exit code, not the command:
|
||||||
a rebase whose incoming commits touch a file this session is also changing, generated files
|
|
||||||
aside), the
|
|
||||||
Publish-Remote Gate (`publish`, on a push to a target this checkout has not declared), the
|
|
||||||
Upload Review Gate (`upload accept`, on a submission nobody has cleared yet), and the Guideline
|
|
||||||
Push Gate (`export guidelines --push`, before a generated `GUIDELINES.md` goes into any captured
|
|
||||||
repository) - but the rule is about the exit code, not the command:
|
|
||||||
|
|
||||||
> **Copy the command's output into your reply - the substance of it, not a description of it -
|
> **Copy the command's output into your reply - the substance of it, not a description of it -
|
||||||
> and stop.** Run no further commands in that turn.
|
> and stop.** Run no further commands in that turn.
|
||||||
|
|
||||||
For the Mass-Update Gate that substance is the grouped file breakdown: the area headings, every
|
For the Mass-Update Gate (`publish`, on a change touching 10 or more counted files) that substance is the grouped file breakdown: the area headings, every
|
||||||
path, and the sizes. It is already ordered for a reader - what a bad publish damages most comes
|
path, and the sizes. It is already ordered for a reader - what a bad publish damages most comes
|
||||||
first, and the mechanically-regenerated files come last - so reproducing it in order is both the
|
first, and the mechanically-regenerated files come last - so reproducing it in order is both the
|
||||||
cheapest and the most useful thing to do with it.
|
cheapest and the most useful thing to do with it.
|
||||||
|
|
||||||
For the rebase-review gate the substance is different: the commits arriving from the remote,
|
For the Rebase-Review Gate (`sync` and `publish`) the substance is different: the commits arriving from the remote,
|
||||||
the files they touch that this session is also touching, and a diff of those files. Read it -
|
the files they touch that this session is also touching, and a diff of those files. Read it -
|
||||||
this is the check `sync`/`publish` cannot perform themselves, since a rebase between two commit
|
this is the check `sync`/`publish` cannot perform themselves, since a rebase between two commit
|
||||||
ranges that touch disjoint files never reaches this gate at all (no content collision is
|
ranges that touch disjoint files never reaches this gate at all (no content collision is
|
||||||
|
|||||||
+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
|
- 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
|
- budget: counted
|
||||||
- network: no
|
- network: no
|
||||||
- gates: human-intervention-required (`new project` only)
|
|
||||||
|
|
||||||
**EXAMPLES**
|
**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 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 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 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 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 stale
|
||||||
- 1 An `INSTALL.md` prerequisites region is missing, or names a platform no tool has
|
- 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 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 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
|
- 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
|
- 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 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`
|
- 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 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 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.
|
- 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.
|
- 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.
|
- `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.
|
- `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
|
EXIT STATUS renders one `<code> <cause>` line per entry, ON FAILURE one
|
||||||
`<cause> -> <reaction>` line - the cause is repeated on purpose, so each
|
`<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
|
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
|
(a human must see the output before anything proceeds - a declared gate, or
|
||||||
non-empty), or 0 for an outcome a caller could mistake for a failure but
|
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
|
that is not one (an unreachable remote reported and skipped). An empty
|
||||||
`reaction` renders the EXIT STATUS line only.
|
`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}")
|
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)
|
@dataclass(frozen=True)
|
||||||
class Properties:
|
class Properties:
|
||||||
effect: Effect
|
effect: Effect
|
||||||
@@ -116,6 +138,14 @@ class Properties:
|
|||||||
network: Network = Network.NO
|
network: Network = Network.NO
|
||||||
gates: tuple[str, ...] = ()
|
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)
|
@dataclass(frozen=True)
|
||||||
class CommandRecord:
|
class CommandRecord:
|
||||||
@@ -154,10 +184,6 @@ class CommandRecord:
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"cli_contract: {self.path!r} notes must be a tuple of bullets, not one string"
|
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
|
`docs/` belongs to no kind, and its reader cannot tell from the path what it
|
||||||
is for.
|
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.
|
Everything here is a hard oracle: a set comparison or a regex, no judgment.
|
||||||
Content quality of the contracts themselves stays with the LLM.
|
Content quality of the contracts themselves stays with the LLM.
|
||||||
"""
|
"""
|
||||||
@@ -773,6 +778,55 @@ def check_docs_layout() -> list[str]:
|
|||||||
return issues
|
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]:
|
def check_toc_regions() -> list[str]:
|
||||||
"""Every reference file over the line threshold carries a current TOC.
|
"""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/`, "
|
"Every file under `docs/` sits in one of its areas - `tutorials/`, `how-to/`, "
|
||||||
"`reference/`, `explanation/` or `dev/` - never directly in `docs/` or in another "
|
"`reference/`, `explanation/` or `dev/` - never directly in `docs/` or in another "
|
||||||
"directory.",
|
"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 "
|
"If `docs/` exists: every generated region of the human reference under "
|
||||||
"`docs/reference/` matches what `docs reference` would write, every `cli_contract.GROUPS` "
|
"`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 "
|
"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 "
|
reaction="Move it (`git mv`) into the area its kind of documentation belongs to, and "
|
||||||
"fix the links that pointed at the old path",
|
"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(
|
cli_contract.Failure(
|
||||||
cause="A reference file's relative markdown link does not resolve to an existing "
|
cause="A reference file's relative markdown link does not resolve to an existing "
|
||||||
"file",
|
"file",
|
||||||
@@ -1430,6 +1494,7 @@ def verify():
|
|||||||
+ check_changelog_budget()
|
+ check_changelog_budget()
|
||||||
+ check_no_issue_references()
|
+ check_no_issue_references()
|
||||||
+ check_docs_layout()
|
+ check_docs_layout()
|
||||||
|
+ check_gates_list()
|
||||||
+ check_reference_regions()
|
+ check_reference_regions()
|
||||||
+ check_toc_regions()
|
+ check_toc_regions()
|
||||||
+ check_reference_targets()
|
+ check_reference_targets()
|
||||||
@@ -1454,6 +1519,7 @@ def verify():
|
|||||||
f"{len(IGNORE_CANARIES)} ignore canaries clear, "
|
f"{len(IGNORE_CANARIES)} ignore canaries clear, "
|
||||||
f"no issue references in {len(shipped_prose())} shipped document(s) or command help, "
|
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"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"the reference under {reference_doc.REFERENCE_DIR}/ current{reference_scope}, "
|
||||||
f"tables of contents current and every link resolving on "
|
f"tables of contents current and every link resolving on "
|
||||||
f"{len(toc.target_files())} reference file(s), "
|
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.
|
stops being a typo and starts being a disclosure.
|
||||||
|
|
||||||
Both absent states say **armed** or **not armed** rather than only naming
|
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
|
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 means about the gate is how a checkout ends up trusting a safeguard
|
||||||
that is not running - which is worse than having none.
|
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
|
# public repository, where a force-push does not take it back: the objects stay
|
||||||
# fetchable by SHA until someone expires the server's reflogs.
|
# 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
|
# 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.
|
# 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 "
|
"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",
|
"no tracker project. Still a single file write when no tracker is configured",
|
||||||
budget=cli_contract.Budget.COUNTED,
|
budget=cli_contract.Budget.COUNTED,
|
||||||
gates=("human-intervention-required (`new project` only)",),
|
|
||||||
),
|
),
|
||||||
notes=(
|
notes=(
|
||||||
"A title becomes a file name, so it must be valid and unique on Windows and macOS as "
|
"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.
|
one write `chemenu.tasks.protocol.TaskWriter` asks of it.
|
||||||
|
|
||||||
The CLI adapter (`wikitool new project`, Gitea #126) renders this the same
|
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
|
`commands._util.needs_clearance(str(exc))`, exit code 42 - "a human must
|
||||||
see the command's output before anything proceeds" applies here for the
|
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
|
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,
|
but the same exit code and the same posture: show the message verbatim,
|
||||||
stop, and do not improvise a workaround (AGENTS.md invariant 7).
|
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
|
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
|
`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
|
(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
|
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
|
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
|
`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)
|
assert "ON FAILURE" not in cc.render_markdown_section(rec)
|
||||||
|
|
||||||
|
|
||||||
def test_exit_42_cause_without_a_gate_is_refused():
|
def test_exit_42_cause_without_a_gate_is_allowed():
|
||||||
with pytest.raises(ValueError, match="no gate"):
|
"""`new project`'s exit 42 is a human-intervention stop, not a gate: it needs no
|
||||||
_fixture_record(failures=(cc.Failure(cause="Gate", reaction="Stop", code=42),))
|
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():
|
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()
|
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
|
# The rewrite instructions/migrations/9.0.0-docs-diataxis.md tells an instance
|
||||||
# to run, as the regex it spells out for `sed -E`.
|
# to run, as the regex it spells out for `sed -E`.
|
||||||
_DIATAXIS_NAMES = (
|
_DIATAXIS_NAMES = (
|
||||||
|
|||||||
@@ -1406,7 +1406,7 @@ def test_empty_allowlist_refuses_everything(repo):
|
|||||||
|
|
||||||
|
|
||||||
def test_gate_has_no_flag_that_opens_it(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."""
|
because the right fix is a deliberate edit by the user."""
|
||||||
import inspect
|
import inspect
|
||||||
|
|
||||||
|
|||||||
@@ -268,7 +268,7 @@ def _page_type(description: str = "A fixture type.") -> reference_doc.PageType:
|
|||||||
def tree(tmp_path, monkeypatch):
|
def tree(tmp_path, monkeypatch):
|
||||||
real_root = config.ROOT
|
real_root = config.ROOT
|
||||||
monkeypatch.setattr(config, "ROOT", tmp_path)
|
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, "all_records", lambda: dict(records))
|
||||||
monkeypatch.setattr(cli_contract, "GROUPS", GROUPS)
|
monkeypatch.setattr(cli_contract, "GROUPS", GROUPS)
|
||||||
monkeypatch.setattr(cli, "options_texts", lambda paths: {p: " --flag A flag." for p in paths})
|
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
|
root, _ = tree
|
||||||
runner.invoke(app, ["docs", "reference", "--apply"])
|
runner.invoke(app, ["docs", "reference", "--apply"])
|
||||||
page = (root / "docs/reference/exit-codes.md").read_text(encoding="utf-8")
|
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 "- [`gate keep`](commands/other.md#gate-keep): Needs a person" in page
|
||||||
assert "frobnicate" not in page
|
assert "frobnicate" not in page
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user