From 7f2e0214dc9ae5a543cbfd63fa420b06a3f11771 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Wed, 2 Sep 2026 04:56:27 -0400 Subject: [PATCH] fix(docs): the rail count drifted to five different answers in one commit (#667 follow-up) `guards.check` implements TWENTY rails -- 1-14 and 16-21, rail 15 retired. At v0.13.2 the repository stated that number four different ways at once: guards.py:267 "all twenty rails" correct README.md:227 "the 18 rails" two behind executor.py:4 "eighteen ... rails" two behind executor.py:93 "eighteen rails" two behind executor.py:1558 "eighteen rails" two behind MY OWN SWEEP CAUSED IT. #667 added rail 21 and updated the count by grepping for "nineteen" -- which found `guards.py`, `glossary.md`, the runbook, `launch.md` and `go-live-runbook.md`, and never touched the four places that said "eighteen" or "18" because those had already been stale since rail 19. Fixing the spelling I happened to search for left the ones I did not, and nothing failed. TEST FIRST, and it earned its place immediately: it found FIVE claims where a manual grep had found three, including `executor.py`'s module docstring. The count is DERIVED from the numbered rails in `guards.check` and every English and numeric spelling is searched repository-wide, so rail 22 fails the suite instead of leaving documents wrong for three releases. DATED RECORDS ARE EXCLUDED, and this is a rule rather than a convenience. `docs/research/2026-08-20-quant-lab-note-cross-verification.md` says "eighteen rails" and "four rule families", and BOTH WERE TRUE when a third party's note was verified in August 2026. Updating it would silently rewrite a verification nobody re-ran. `docs/experiments`, `docs/presentations` and `docs/superpowers` are out for the same reason: they are statements about what was true when written. The contiguity assertion is about the SET, not about "sorted, unique, starts at 1". A mutation renumbering rail 21 to 22 passed all three of those -- the count held, the order held, 15 was still absent -- while a silent gap opened at the top. A gap is the shape this drifts in. 4 mutants, 4 killed: README going stale, a docstring going stale, a renumbering that opens a gap, and reuse of the retired 15. Tests: `tests/execution/test_rail_count.py` (2 tests). Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01NzuKAe2RVrPt9acVAWjRyL --- README.md | 2 +- keel/execution/executor.py | 6 +- tests/execution/test_rail_count.py | 115 +++++++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 4 deletions(-) create mode 100644 tests/execution/test_rail_count.py diff --git a/README.md b/README.md index 170b4fe..2ba5292 100644 --- a/README.md +++ b/README.md @@ -187,7 +187,7 @@ the stop holds, not what you spend. ``` keel/ the agent and CLI ├── agent.py the loop and RULE_REGISTRY (where rules live) -├── execution/guards.py the 18 rails (where enforcement lives) +├── execution/guards.py the 20 rails (where enforcement lives) ├── execution/sizing.py position sizing ├── compliance/screen.py attested allowlist admission (fails closed) └── commands/ CLI command implementations diff --git a/keel/execution/executor.py b/keel/execution/executor.py index 5e42043..0911957 100644 --- a/keel/execution/executor.py +++ b/keel/execution/executor.py @@ -1,7 +1,7 @@ """The order executor (P3 Task 4) -- turns a `Signal` into a guarded live order. `execute()` is the only path from a strategy `Signal` to a real order: it sizes the candidate -(`execution.sizing`), runs the eighteen un-overridable §14 hard rails (`execution.guards.check`) +(`execution.sizing`), runs the twenty un-overridable §14 hard rails (`execution.guards.check`) **before** anything reaches the broker, previews the order, honors the confirm/autonomous mode gate, places it, and writes a full audit trail to the `orders` table both before and after the broker call (so a crash mid-placement, or a broker-side rejection, still leaves a record). No path in @@ -90,7 +90,7 @@ `(best_ask - best_bid) / mid` at or beyond `execution.max_entry_spread_pct` (default 0.005, 50bp -- until #523 numerically #334's slippage cap; now an independent threshold that #523 deliberately left where it was) refuses the order, and a preview with no readable -bid/ask fails closed with a distinct reason. It sits BESIDE the eighteen rails, not among +bid/ask fails closed with a distinct reason. It sits BESIDE the twenty rails, not among them: `guards.check` is broker-less by design, and the book exists only in the preview this module just fetched -- the same preview #332's warning reads (`_preview_book`: one helper, two consumers). BUY-only (exits must execute, like rail 17 halting entries not exits) and @@ -1555,7 +1555,7 @@ def _entry_spread_gate( **Where it sits, and why.** AFTER `guards.check` and AFTER the preview: guards are broker-less by design (this module's docstring), so the book -- which only `broker.preview_order` returns -- cannot reach a `guards.check` rail. The gate is a - routing-time check BESIDE the eighteen rails, not a numbered rail, and it consumes the + routing-time check BESIDE the twenty rails, not a numbered rail, and it consumes the SAME preview #332's `_warn_if_market_routing_overrides_entry` reads (one helper, `_preview_book`, two consumers). It runs after that warning so the warning's position -- pinned by #332's tests -- is unchanged; on a wide book both facts are true at routing diff --git a/tests/execution/test_rail_count.py b/tests/execution/test_rail_count.py new file mode 100644 index 0000000..3475546 --- /dev/null +++ b/tests/execution/test_rail_count.py @@ -0,0 +1,115 @@ +"""The rail count is DERIVED from `guards.check`, and every claim about it must agree. + +It drifted three times before this file existed. Rails 19, 20 and 21 each arrived with a sweep +that updated the spelling the author happened to grep for -- "nineteen" when the stale text said +"eighteen", and never the digits -- so `guards.py` said twenty while `README.md` said 18 and +`executor.py` said eighteen, at the same commit. + +A comment cannot enforce this and a checklist did not. The count is read out of the source of +truth (the numbered rails in `guards.check`) and every English and numeric spelling of it is +searched for across the repository, so a rail added tomorrow fails here rather than leaving a +document quietly wrong for three releases. +""" + +from __future__ import annotations + +import re +from pathlib import Path + +_ROOT = Path(__file__).resolve().parents[2] +_GUARDS = _ROOT / "keel/execution/guards.py" + +#: Number words this project would plausibly write a rail count as. Deliberately wider than the +#: current count in both directions: the failure mode is a STALE claim, so the search has to see +#: the numbers nobody expects to find. +_WORDS = { + 15: "fifteen", 16: "sixteen", 17: "seventeen", 18: "eighteen", 19: "nineteen", + 20: "twenty", 21: "twenty-one", 22: "twenty-two", +} + +#: Where a count claim can hide. Experiment records, presentations, specs and research notes are +#: EXCLUDED: they are dated statements about what was true when they were written, and rewriting +#: one would be falsifying a record rather than fixing a document. +#: +#: `docs/research/2026-08-20-quant-lab-note-cross-verification.md` is the worked example. It +#: records what a third party's August-2026 note claimed about keel and what we verified -- +#: "eighteen rails" and "four rule families" were both TRUE when it was checked, and both are +#: stale now. Updating it would silently rewrite a verification nobody re-ran. +_SEARCHED = ("README.md", "README.ar.md", "docs", "keel") +_EXCLUDED = ( + "docs/experiments", + "docs/presentations", + "docs/superpowers", + "docs/research", +) + + +def rail_numbers() -> list[int]: + """The numbered rails `guards.check` actually implements -- THE source of truth.""" + return [ + int(m.group(1)) + for m in re.finditer(r"^ # (\d+)\. ", _GUARDS.read_text(encoding="utf-8"), re.M) + ] + + +def _files(): + for entry in _SEARCHED: + path = _ROOT / entry + if path.is_file(): + yield path + elif path.is_dir(): + for child in sorted(path.rglob("*")): + if child.suffix not in (".md", ".py") or not child.is_file(): + continue + rel = child.relative_to(_ROOT).as_posix() + if any(rel.startswith(skip) for skip in _EXCLUDED): + continue + yield child + + +def test_the_rails_are_numbered_contiguously_except_the_one_that_was_retired() -> None: + """1-14 and 16-21. Rail 15 does not exist and its absence is deliberate -- every document + that states the count says so, and a renumbering that quietly filled the gap would make + every historical reference to a rail number wrong.""" + numbers = rail_numbers() + assert numbers == sorted(numbers), f"rails are out of order: {numbers}" + assert len(set(numbers)) == len(numbers), f"a rail number is used twice: {numbers}" + assert 15 not in numbers, "rail 15 is retired; reusing the number would rewrite history" + + # EXACTLY 1..max minus the retired 15 -- not merely "sorted, unique and starting at 1". + # A mutation renumbering rail 21 to 22 passed all three of those: the count was unchanged, + # the order held, and 15 was still absent, while a silent gap opened at the top. A gap is + # the shape this drifts in, so the assertion has to be about the SET. + expected = [n for n in range(1, max(numbers) + 1) if n != 15] + assert numbers == expected, ( + f"the rail numbers have a gap: {numbers}. Rail 15 is the ONE deliberate absence; any " + "other missing number means a rail was renumbered or removed without renumbering the " + "rest, and every document citing a rail by number is now ambiguous." + ) + + +def test_every_rail_count_claim_in_the_repository_is_current() -> None: + """The pin. A stale count is not cosmetic: `README.md` is where a stranger learns what keel + enforces, and a number two behind understates the machinery by exactly the rails most + recently added -- the compliance ones.""" + expected = len(rail_numbers()) + current = _WORDS[expected] + wrong = {n: w for n, w in _WORDS.items() if n != expected} + stale: list[str] = [] + + for path in _files(): + text = path.read_text(encoding="utf-8") + for line_no, line in enumerate(text.splitlines(), start=1): + lowered = line.lower() + for number, word in wrong.items(): + # ` rails` and `the rails` -- the two shapes this repository writes. + if re.search(rf"\b{word}\b[^.]{{0,40}}\brails?\b", lowered) or re.search( + rf"\bthe {number}\b[^.]{{0,20}}\brails?\b", lowered + ): + rel = path.relative_to(_ROOT).as_posix() + stale.append(f"{rel}:{line_no}: {line.strip()[:88]}") + + assert not stale, ( + f"{len(stale)} rail-count claim(s) disagree with `guards.check`, which implements " + f"{expected} rails ({current}):\n " + "\n ".join(stale) + )