Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions keel/commands/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <id>",
),
Step(
Expand Down Expand Up @@ -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)

Expand Down
25 changes: 25 additions & 0 deletions tests/commands/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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