feat(tui): the strategy console and the research readers (#390 C4) - #404
Merged
Conversation
The console's fourth slice: the Rules menu becomes the full try/evaluate/
retry loop of PRD O11, and the Research menu lands the O5 evidence readers.
Everything dispatches to the C1 service layer; no sizing, gating or
reporting math is born in the TUI.
The tried-vs-used ledger's SOURCING (O11.2): every rules-table row rendered
with its lifecycle status AND the machine's recorded reason it sits there --
`can_promote`'s own `reasons` (the exact lines `rules promote` prints on a
refusal: which floor failed, and the G4/PBO axis honestly NOT RUN) from a
fresh backtest over the repo's cached candles at the config's own fee; the
`insights` service's promotion-gate distance for `paper` rules; the rows'
own `promoted_at`/`demoted_at` stamps for live/disabled. A rule with no
cached candles renders "no backtest on record" -- the honest absent case,
never a TUI-authored narrative. The ledger computes ONCE per entry (every
row's backtest is real work) and is held while open.
The simulate UX DECISION (O11.1): an ARMED view showing the TARGET REPORT
PATH first -- Enter IS the confirm step -- then `run_simulation` on the
active profile's config/db with the CLI's own defaults (5y, $500/month,
allowlist products, history fetched when the cache does not cover the
window). The run BLOCKS the loop exactly like `f` fetch does, mirroring the
CLI's own UX honestly (the mission's note-then-render pattern); the
progress lines the CLI streams are collected and shown at the head of the
results, the verdict/report render VERBATIM under a pinned verdict+path
footer, and closing re-arms. The report lands in the directory the
Research/promotion-reports reader lists, newest-first.
The EXTRACTION done first (C1 style, byte-compatible): the validation and
write logic of `rules add`/`backtest`/`promote`/`demote`/`disable`/`enable`
moved out of the click bodies into `keel.commands.rules` services
(`add_rule_row`, `run_rule_backtest`, `attempt_promotion`,
`apply_rule_*`) that echo through injected stdout/stderr sinks; the CLI
wrappers dispatch and are byte-identical (verified against origin/main
across 17 command scenarios, and by the untouched `test_cli.py`/
`test_rules_add.py` suites). The G4 loader stays injectable so the
`_load_pbo` test seam keeps working unchanged. O3 held: promote asks an
explicit y/N in-console, `--force` runs only behind the CLI's OWN
`_require_interactive_confirmation` typed gate (exact phrase, never
pre-filled, failing closed), and there is no auto-promotion anywhere.
`describe_params` DESIGN: one introspection helper in the service layer
deriving {param: doc, default, type, declared choices, quotable} from the
class itself -- the doc from per-parameter `PARAM_DOCS` dictionaries ADDED
AT THE CLASS (all four rule kinds; Dca's docstring Params section moved
into its table), the default/type from the constructor signature and
annotations, the choices from the param's own `Literal`, and the
"quoted is correct" answer from `agent.coerced_param_keys` (the coercion
boundary). The add form prompts ONE FIELD PER PARAM with its doc and
default (empty keeps the default, exactly as omitting the --params key);
the ledger's detail view renders every param through the same source. No
hand-maintained table anywhere.
The research readers (O5): the experiments/research-docs/promotion-reports
corpora (directories single-sourced from the writers -- the trials ledger's
own parent, `run_simulation`'s report directory -- resolved at call time)
plus the trials ledger view (`read_trials`/`trial_counts`/`verify_chain`,
read-only). Documents read through a BOUNDED reader (1 MiB, loud truncation
note) cached per (path, mtime) -- the compliance console's scout-browser
lessons, kept.
Fixes #390
… bounded, per-rule
CI caught what local paths hid: the 80-col wrap can break any multi-word phrase across lines (a long runner tmp path shifts every wrap point), so phrase-level asserts read whitespace-collapsed text. 'truncated', 'no documents', 'chain intact', 'no trials' normalized alongside 'newest first'; single-token asserts stay as-is.
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 console's fourth slice: the Rules menu becomes the full try/evaluate/retry loop of PRD O11, and the Research menu lands the O5 evidence readers. Everything dispatches to the C1 service layer; no sizing, gating or reporting math is born in the TUI.
The tried-vs-used ledger's sourcing (O11.2)
Every rules-table row rendered with its lifecycle status AND the machine's recorded reason it sits there:
can_promote's ownreasons— the exact linesrules promoteprints on a refusal: which floor failed (n_trades N < min_trades 100, expectancy/rr/win_rate), and the G4/PBO axis honestly NOT RUN — computed from a fresh backtest over the repo's cached candles at the config's own fee (_backtest_fee+_describe_feeprovenance).insightsservice's promotion-gate distance forpaperrules (build_rule_track_record/track_record, the trades_remaining wording).promoted_at/demoted_atstamps for live/disabled rows.The ledger computes once per entry (every row's backtest is real work) and is held while open. Its detail view renders every param through
describe_params.The simulate UX decision (O11.1)
An ARMED view showing the target report path first — Enter IS the confirm step — then
run_simulationon the active profile's config/db with the CLI's own defaults (5y, $500/month, allowlist products, history fetched when the cache does not cover the window, mirroring the CLI). The run blocks the loop exactly likeffetch does — the CLI's own UX, mirrored honestly. The progress lines the CLI streams are collected and shown at the head of the results; the verdict/report render verbatim under a pinned verdict+path footer (pinned_frame); closing re-arms. The report lands in the directory the Research/promotion-reports reader lists, newest-first.The extraction done first (C1 style, byte-compatible)
The validation and write logic of
rules add/backtest/promote/demote/disable/enablemoved out of the click bodies intokeel.commands.rulesservices (add_rule_row,run_rule_backtest,attempt_promotion,apply_rule_*) that echo through injected stdout/stderr sinks. The CLI wrappers dispatch and are byte-identical — verified against origin/main across 17 command scenarios (diffed outputs, exit codes), and by the untouchedtest_cli.py/test_rules_add.pysuites. The G4 loader stays injectable (load_pbo) so the_load_pbotest seam keeps working unchanged.--forceon the gated path loads config only where the old body did.O3 held: promote asks an explicit y/N in-console;
--forceruns only behind the CLI's OWN_require_interactive_confirmationtyped gate (clis_typed_promote_force_gate, exact phrase, never pre-filled, failing closed); no auto-promotion anywhere.describe_paramsdesignOne introspection helper in the service layer deriving
{param: doc, default, type, declared choices, quotable}from the class itself:PARAM_DOCSdictionaries added AT THE CLASS — all four rule kinds (turtle_breakout, pullback_continuation, rsi_meanrev, dca; Dca's docstring Params section moved into its table), two pinned verbatim by test.Literal; quotable fromagent.coerced_param_keys(the coercion boundary itself).The add form prompts one field per param with its doc and default (empty keeps the default, exactly as omitting the
--paramskey); the ledger's detail view renders from the same source. No hand-maintained table anywhere.The research readers (O5)
The experiments / research-docs / promotion-reports corpora (directories single-sourced from the writers — the trials ledger's own parent,
run_simulation's report directory — resolved at call time) plus the trials ledger view (read_trials/trial_counts/verify_chain, read-only, the chain verdict fail-loud). Documents read through a bounded reader (1 MiB, loud truncation note) cached per (path, mtime) — the compliance console's scout-browser lessons, kept.Gates
uv run pytest -q— 3458 passed, 3 skippeduv run ruff check keel tests packages— cleanuv run mypy— cleanFixes #390