From 26100ba91e1758006177891ff7fba06c3efef9b2 Mon Sep 17 00:00:00 2001 From: Elmehdi Aitbrahim Date: Thu, 20 Aug 2026 18:47:37 -0400 Subject: [PATCH] docs(setup): a refused promotion is the gate working, and the checklist should say so Measured on a real deployment while verifying D4's acceptance. A freshly-seeded turtle_breakout, promoted from the browser against 74,714 real candles, was refused: n_trades 12 < min_trades 100 win_rate 0.5 < min_win_rate 0.55 overfitting check (G4 / PBO-CSCV) NOT RUN: no trial matrix was supplied, so the probability that this rule's parameters were selected by overfitting is UNKNOWN -- which is not the same as low, and is not a pass. status -> candidate That is exactly right, and it is also the single most likely thing a first-run user will see on this step. Left unexplained it reads as a broken button on a checklist that has gone green everywhere else -- so the step now says a refusal is the gate doing its job, that this item can stay outstanding for a long time, and that the deliberate bypass is `keel rules promote --force` at a terminal, on the record. `ready_for(PAPER)`'s docstring gains the same correction. "Set up" and "has a rule worth running" are different states; it reports the second, and a fresh install does not reach it quickly. No behaviour changes. The website already says this about `keel simulate` -- "it will very likely tell you TRAIN MORE and name the gates that fail; that is the engine working, not broken; the honesty is the feature" -- and the setup checklist was the one surface still missing it. 4094 passed, 3 skipped (2 new). ruff clean repo-wide. Refs #437, #18. Co-Authored-By: Claude Opus 5 (1M context) --- keel/commands/setup.py | 16 ++++++++++++++-- tests/commands/test_setup.py | 25 +++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 2 deletions(-) diff --git a/keel/commands/setup.py b/keel/commands/setup.py index 8913b52..c52fc82 100644 --- a/keel/commands/setup.py +++ b/keel/commands/setup.py @@ -209,7 +209,14 @@ def blocking(self) -> bool: title="At least one rule promoted to paper", kind=StepKind.JUDGEMENT, stage=Stage.PAPER, - why="Seeded rules are candidates and trade nothing. Which rule to run is your choice.", + why=( + "Seeded rules are candidates and trade nothing. Which rule to run is your choice -- " + "and on a fresh deployment the gate will very likely REFUSE it, naming too few " + "trades, a win rate under the floor, and an overfitting check that was never run. " + "That is the engine working, not a fault: a rule that has not earned paper status " + "does not get it, and this step can stay outstanding for a long time. Bypassing the " + "gate is `keel rules promote --force`, deliberately and on the record, at a terminal." + ), how="keel rules promote ", ), Step( @@ -296,7 +303,12 @@ def ready_for(self, stage: Stage) -> bool: `OFF_VENUE` steps can never be observed, so a deployment is never `ready_for(LIVE)` by this function's reckoning. That is the honest answer and it is deliberate: the last word on going live belongs to the operator who checked the venue dashboard, not to a function - that cannot see it.""" + that cannot see it. + + `ready_for(PAPER)` is a real answer, but not one a fresh install reaches quickly: + `rule_promoted` waits on a promotion gate that a newly-seeded rule will very likely fail, + by design. "Set up" and "has a rule worth running" are different states, and this reports + the second.""" wanted = (Stage.PAPER,) if stage is Stage.PAPER else (Stage.PAPER, Stage.LIVE) return all(state.done is True for state in self.states if state.step.stage in wanted) diff --git a/tests/commands/test_setup.py b/tests/commands/test_setup.py index 1fb3f62..9c85c4b 100644 --- a/tests/commands/test_setup.py +++ b/tests/commands/test_setup.py @@ -245,3 +245,28 @@ def test_the_rendered_checklist_names_every_step_and_the_next_action( for step in STEPS: assert step.title in text, step.key assert text.rstrip().splitlines()[-1].startswith("next: ") + + +def test_the_promotion_step_says_a_refusal_is_the_engine_working() -> None: + """Measured on a real deployment: a freshly-seeded turtle_breakout promoted from the browser + was refused with `n_trades 12 < 100`, `win_rate 0.5 < 0.55`, and the overfitting check never + run. That is correct, and it is also the single most likely thing a first-run user will see + on this step. + + Left unexplained it reads as a broken button on a checklist that has gone green everywhere + else. The step's own text has to say that a refusal is the gate doing its job, that this item + can stay outstanding for a long time, and where the deliberate bypass lives -- at a terminal, + on the record.""" + step = next(s for s in STEPS if s.key == "rule_promoted") + lowered = step.why.lower() + assert "refuse" in lowered + assert "not a fault" in lowered or "engine working" in lowered + assert "--force" in step.why + assert "terminal" in lowered + + +def test_ready_for_paper_does_not_claim_a_fresh_install_is_ready(fresh: tuple[Path, Path]) -> None: + """ "Set up" and "has a rule worth running" are different states, and a checklist that + conflated them would call a deployment ready on the strength of a rule the gate refused.""" + config_path, db_path = fresh + assert inspect(config_path, db_path).ready_for(Stage.PAPER) is False