docs(architecture): the refusal has a page, and the page has a guard - #295
Merged
evkir merged 5 commits intoSep 23, 2026
Merged
Conversation
The tree defines ten exceptions and six environment readers, and not one of them was named in docs/ or in the README. Ten ways the product says no, documented nowhere -- including RateLimitError, which is declared in utils/backoff.py and raised by nothing at all. A grep for any of the ten across docs/ and README.md returned zero lines before this commit. docs/architecture/refusal.md states the four shapes a refusal takes, keyed on who is standing at the screen: raised and left to travel, raised and turned into one CLI line, raised and turned into a value, or not raised at all. The last of those is the environment readers, which narrow to a default rather than aborting a scan: from_env has no raise in it by design, nine call sites reach it, and one is inside an MCP tool where an exception is not a message anybody reads. The guard is the inversion the manifest page already uses, with the set collected by AST instead of by dataclasses.fields. Both directions: a refusal the page omits is a mechanism no reader can find, and a name the page keeps after a rename is the same rot wearing the other face. Collection is structural, never by suffix. Every exception in the tree today ends in one of five words, so a name rule passes every assertion here -- mutation confirmed it. The control therefore runs the same collector over a sample the package does not contain: three generations of subclass and a helper called ReportsError that inherits nothing. Depth three rather than two, because at depth two a flat check still answers correctly and the mutant that removed the recursion survived. One collector, called twice. An earlier revision had two copies and the package copy could be swapped for a name test with the suite staying green.
…eads A page nobody links is a page nobody opens. The Documentation table is where a reviewer goes, and the new page sits beside the two that share its folder. Reachability is not guarded, deliberately for now. test_readme_links holds that a path in the table resolves; nothing holds that a file in docs/ is in the table, and mutation confirmed it -- deleting this row leaves every test green. Measured on 2026-09-23: six of twenty-five pages under docs/ are linked from nowhere in the README, so the guard that would close this is a day of work and six decisions, not a line in this commit.
The page written in the first commit generates its table from the tree, and the first thing that table found was a refusal that did not exist. RateLimitError declared "raised when API returns 429" with no code raising it, no code catching it and no code importing it: one occurrence in the whole tree, its own class statement. The 429 it described is real and is already answered -- both NVD request paths raise httpx.HTTPStatusError on 429 and 503, and exponential_backoff retries on exactly that type. nvd_backoff goes with it, for the same measured reason: one occurrence, its own def. It also caught (Exception,) indiscriminately, so had anything called it, a typo in a URL would have cost five identical attempts. The tests come first in the same commit because exponential_backoff had none at all, and deleting two objects from an untested module cannot be told apart from breaking it. Five now hold the properties the caller depends on: a late success is not a failure, the ceiling counts attempts rather than sleeps, the exception the caller sees is the last real one, a failure outside the declared set travels at once, and no delay is paid after the final attempt. Wide mypy errors 272 -> 270, both on the deleted def. The module is outside [tool.mypy] files, so the boundary does not move and drift stays none.
exponential_backoff(fn, max_retries=0) walked past an empty loop to its own final raise with nothing caught, and Python answered `raise None` with "exceptions must derive from BaseException". A TypeError from the retry helper, naming neither the call that failed nor the reason -- logged one line after an error claiming the call had failed after zero attempts. Zero attempts is a caller's mistake, so it is answered at once and before any request goes out: ValueError naming the value received. Reading zero as one attempt would invent a choice nobody made, which is the distinction the refusal page spends half its length on. The None branch after the loop is unreachable while the guard stands, and it says so rather than carrying a type: ignore. Mutation shows why the branch is there and not silenced: replacing its condition with False leaves every test green and brings [misc] straight back on the final raise -- the checker is the only channel that sees it, exactly as it was the only channel that saw the original defect. The module is outside [tool.mypy] files, so mypy had been reporting this line all along while the gate could not. Three tests: zero refused before the first call, a negative refused the same way, and one attempt allowed and made. The third is the control -- a guard written as `< 2` passes the first two. Wide mypy errors 270 -> 269. Boundary unchanged, drift none.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…rage too codecov failed the patch at 75%: one line, the RuntimeError inside the None branch after the loop. It is not a line anybody forgot -- it cannot be reached while the argument guard stands, which is what its own comment says and what the guard makes true. Marked with the form already used in cyberai/mcp/client_probe.py: pragma with the reason spelled out, not a bare directive. Coverage reads the pragma and the checker does not, so the branch stays where it is and goes on holding the type. Re-ran the mutation that matters: replacing the condition with False still brings [misc] back on the final raise, so the pragma silenced a report and not a rule.
evkir
deleted the
docs/the-refusal-has-a-page-and-the-page-has-a-guard
branch
September 23, 2026 15:02
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The tree defines ten exceptions and six environment readers. Before this
branch, not one of them was named anywhere in docs/ or the README -- a grep
across both returned zero lines. docs/architecture/refusal.md now states the
four shapes a refusal takes, keyed on who is standing at the screen, and
tests/architecture/test_every_refusal_is_on_the_page.py holds the page
against the tree in both directions.
The guard collects structurally rather than by suffix, and its control runs
the same collector over three generations of subclass -- at depth two a flat
check still answers correctly, which a shorter sample proved by letting a
mutant survive.
Two findings came from the table itself, not from the plan:
by nothing, imported by nothing. Removed with nvd_backoff, which had the
same single occurrence. The 429 both described is already answered by
httpx.HTTPStatusError in the two NVD paths.
raise Noneand answeredwith "exceptions must derive from BaseException" -- a TypeError from the
retry helper naming neither the call nor the reason. Now a ValueError
before the first request. mypy had reported the line as [misc] all along;
the module is outside [tool.mypy] files, so the gate never saw it.
The helper had no tests at all before this branch. Eight now hold it.
Badge 2988 -> 3001. Wide mypy errors 272 -> 269, boundary unchanged, drift
none. Version 1.7.0 untouched.
What this changes
How it was measured
Checklist
ruff format --check cyberai/ tests/andruff check cyberai/ tests/passpytest -W ignore::DeprecationWarning -m "not slow and not smoke"passes