diff --git a/CHANGELOG.md b/CHANGELOG.md index 46360c36..82ba3f98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,17 @@ trajectory so breaking changes can land in any minor release until the 1.0 stabi ### Changed +- **Internal restructuring** (no behaviour change): the play-mode + shortcut-retargeting key list moved off the private + `cli/__init__.py:_PLAY_RETARGETED_KEYS` constant onto + `SimulationCfg.play_retargeted_keys()` (a static method on the domain + config in `genelab.configs`). The CLI's `env.` → `play_env.` retarget + loop now calls the method; the set of play-retargetable simulation + override paths (`env.simulation.{vis,gpu,steps,dt}`) lives next to the + `SimulationCfg` fields the `--vis` / `--gpu` / `--steps` / `--dt` + shortcuts target. `genelab play --help` is unchanged (R0.1 snapshot + gate green); `configs.py` stays torch-free at import (invariant #5). + Lands as ROADMAP §9 PR R3.2 — completes ADR-0005 (R3). - **Internal restructuring** (no behaviour change): `--eval-*` runner-arg parsing for in-training eval moved from `cli/__init__.py:_build_eval_callback` onto the domain config as `EvalCallbackCfg.from_args(runner_args) -> diff --git a/src/genelab/cli/__init__.py b/src/genelab/cli/__init__.py index 8bb93365..9990aa17 100644 --- a/src/genelab/cli/__init__.py +++ b/src/genelab/cli/__init__.py @@ -38,7 +38,7 @@ render_registry, ) from genelab.cli._scaffold import create_project_skeleton -from genelab.configs import apply_overrides +from genelab.configs import SimulationCfg, apply_overrides from genelab.registry import ( TASKS, load_bundled_asset_zoo, @@ -80,14 +80,6 @@ class _RegistryKindArg(str, Enum): _AGENT_KINDS: Final[frozenset[str]] = frozenset({"zero", "random", "trained"}) -_PLAY_RETARGETED_KEYS: Final[tuple[str, ...]] = ( - "env.simulation.vis", - "env.simulation.gpu", - "env.simulation.steps", - "env.simulation.dt", -) - - _RUN_FLAGS_HELP: Final[str] = """\ Shorthand flags rewritten into env overrides: @@ -556,7 +548,7 @@ def _configured_task( # task's play_env when one is configured. Keeps `genelab play TASK --vis` working # without forcing users to spell `play_env.simulation.vis`. if command == "play" and getattr(task.cfg, "play_env", None) is not None: - for short_key in _PLAY_RETARGETED_KEYS: + for short_key in SimulationCfg.play_retargeted_keys(): if short_key in overrides: overrides[short_key.replace("env.", "play_env.", 1)] = overrides.pop(short_key) diff --git a/src/genelab/configs.py b/src/genelab/configs.py index 09522684..5f876243 100644 --- a/src/genelab/configs.py +++ b/src/genelab/configs.py @@ -37,6 +37,25 @@ class SimulationCfg: # raise ``decimation`` if that's not what you want. render_fps: int | None = 60 + @staticmethod + def play_retargeted_keys() -> tuple[str, ...]: + """Override paths the CLI rewrites ``env.`` → ``play_env.`` in play mode. + + The ``--vis`` / ``--gpu`` / ``--steps`` / ``--dt`` shorthand flags expand + to ``env.simulation.`` overrides. When a task defines a separate + ``play_env``, ``genelab play TASK --vis`` should target *that* env, so the + CLI retargets these keys onto ``play_env.simulation.``. Owning the + list here (rather than as a private constant in ``cli/__init__.py``) keeps + the set of play-retargetable simulation overrides next to the fields + themselves (ADR-0005 / R3.2). + """ + return ( + "env.simulation.vis", + "env.simulation.gpu", + "env.simulation.steps", + "env.simulation.dt", + ) + @dataclass class InteractiveSceneCfg: diff --git a/tests/test_configs.py b/tests/test_configs.py new file mode 100644 index 00000000..800eb4d0 --- /dev/null +++ b/tests/test_configs.py @@ -0,0 +1,36 @@ +"""Tests for ``genelab.configs`` domain-config helpers. + +Currently covers ``SimulationCfg.play_retargeted_keys`` (ROADMAP §9 R3.2 / +ADR-0005) — the set of override paths the CLI rewrites ``env.`` → +``play_env.`` in play mode. The list moved off a private constant in +``cli/__init__.py`` onto the domain config so it lives next to the +``SimulationCfg`` fields the shortcut flags target. +""" + +from __future__ import annotations + +from genelab.configs import SimulationCfg + + +def test_play_retargeted_keys_exact_set() -> None: + """The four ``env.simulation.*`` shortcut-override paths, verbatim and ordered.""" + assert SimulationCfg.play_retargeted_keys() == ( + "env.simulation.vis", + "env.simulation.gpu", + "env.simulation.steps", + "env.simulation.dt", + ) + + +def test_play_retargeted_keys_target_real_simulation_fields() -> None: + """Every retargeted key names an actual ``SimulationCfg`` field (no stale paths).""" + fields = SimulationCfg.__dataclass_fields__ + for key in SimulationCfg.play_retargeted_keys(): + assert key.startswith("env.simulation."), key + field_name = key.rsplit(".", 1)[1] + assert field_name in fields, f"{field_name!r} is not a SimulationCfg field" + + +def test_play_retargeted_keys_callable_on_class_and_instance() -> None: + """Static method — same result whether called on the class or an instance.""" + assert SimulationCfg.play_retargeted_keys() == SimulationCfg().play_retargeted_keys()