diff --git a/scripts/policy/resolve.py b/scripts/policy/resolve.py index cbf2b63..e311a47 100644 --- a/scripts/policy/resolve.py +++ b/scripts/policy/resolve.py @@ -363,13 +363,19 @@ def resolve_source(source: dict) -> dict: install_unresolved = [] if source["provider"] != "huggingface-model": install_unresolved.append("No supported robotctl install route exists for GitHub or Hugging Face Space sources.") + recipe = recipe_for_policy(source["repo"], None, source) + simulation = ( + {"status": "covered", "recipe": recipe, "scope": recipe["provenance"]["scope"]} + if recipe is not None + else {"status": "not-covered", "reason": "No machine-readable policy manifest is published with this artifact."} + ) diagnosis = { "resolution": "review", "install_route": "review", "unresolved": ["No machine-readable policy manifest is published with this artifact."], "install_unresolved": install_unresolved, "policy_set": False, - "simulation": {"status": "not-covered", "reason": "No machine-readable policy manifest is published with this artifact."}, + "simulation": simulation, } else: diagnosis = _manifest_diagnosis(manifest, source["repo"], source, policy_set=policy_set) diff --git a/simulation/execution.py b/simulation/execution.py index 49087a0..b095970 100644 --- a/simulation/execution.py +++ b/simulation/execution.py @@ -49,21 +49,33 @@ def execution_spec_from_policy(policy: dict[str, Any], resolved: dict[str, Any]) if manifest is not None and not isinstance(manifest, dict): return None - robot = manifest.get("robot") if isinstance(manifest, dict) else None + # Manifest facts are authoritative when present. Exact source-bound + # maintainer recipes may provide a deliberately narrow contract for older + # artifacts that have no machine-readable manifest; there is no general + # legacy/default fallback here. + contract_source = manifest if isinstance(manifest, dict) else recipe.get("contract") + if not isinstance(contract_source, dict): + return None + robot = contract_source.get("robot") if not isinstance(robot, dict): return None - if manifest.get("obs_len") != 61 or manifest.get("action_len") != 14 or robot.get("model") != "microduck" or robot.get("control_hz") != 50: + if contract_source.get("obs_len") != 61 or contract_source.get("action_len") != 14 or robot.get("model") != "microduck" or robot.get("control_hz") != 50: + return None + action_scale = contract_source.get("action_scale") + recipe_action_scale = recipe.get("action_scale") + if action_scale is None: + action_scale = recipe_action_scale + elif recipe_action_scale is not None and action_scale != recipe_action_scale: return None - action_scale = manifest.get("action_scale") if isinstance(action_scale, bool) or not isinstance(action_scale, (int, float)): return None contract = { - "observation_dim": manifest.get("obs_len"), - "action_dim": manifest.get("action_len"), + "observation_dim": contract_source.get("obs_len"), + "action_dim": contract_source.get("action_len"), "control_frequency_hz": robot.get("control_hz"), "action_scale": float(action_scale), - "decimation": manifest.get("decimation", 4), - "actuator_model": manifest.get("actuator_model", "Registry deterministic position-control diagnostic runtime"), + "decimation": contract_source.get("decimation", 4), + "actuator_model": contract_source.get("actuator_model", "Registry deterministic position-control diagnostic runtime"), } model = recipe.get("model") if not isinstance(model, str): diff --git a/simulation/execution_recipes.py b/simulation/execution_recipes.py index 0db5351..ce3e9bd 100644 --- a/simulation/execution_recipes.py +++ b/simulation/execution_recipes.py @@ -33,13 +33,460 @@ UPSTREAM_PIN = "bc41fb5c9a9b39894669c1e022e375cf83800382" UPSTREAM_MANIFEST_URL = f"https://github.com/pollen-robotics/microduck/blob/{UPSTREAM_PIN}/docs/policy-manifest.md" UPSTREAM_CHEATSHEET_URL = f"https://github.com/pollen-robotics/microduck/blob/{UPSTREAM_PIN}/docs/robot/cheatsheet.md" +UPSTREAM_CONTROL_URL = f"https://github.com/pollen-robotics/microduck/blob/{UPSTREAM_PIN}/robotd/src/control.rs" + +POLLEN_POLICY_REPO = "pollen-robotics/microduck-policies" +POLLEN_POLICY_REVISION = "088524a64e2557dc453256b6071dbb9d23888802" +POLLEN_MANIFEST_PATH = "manifest.json" +POLLEN_MANIFEST_SHA256 = "d0c36e7b71129dd617339c63bcb1d704eab282c8617ebf14c2013a01abfb2dda" +POLLEN_MANIFEST_URL = f"https://huggingface.co/{POLLEN_POLICY_REPO}/blob/{POLLEN_POLICY_REVISION}/{POLLEN_MANIFEST_PATH}" +POLLEN_ARTIFACT_SHA256 = { + "alpha_walking.onnx": "e36332d383997d51401897734cd3e79cf5038406feddb18b4d57ecfb141daa6c", + "alpha_ground_pick.onnx": "ffbf5109982ff999b0ba53afe86b9ae731bbec679d67fb7f8ab4c52152c88872", + "roller.onnx": "cf05651d2708a2f9364212e86b866c97a70ace8131c492500105e8f28bf99afd", + "roller_crouch.onnx": "a1a084be240469c76ac9d3fa44d4792f16d4b1da60398b3ecd3cfc5e2244d990", + "roulade.onnx": "3d60da08fc13f29c1b57f41977aa898132c0d60042100149d8e775affcbca32b", + "ball_kick_left.onnx": "d6928284dccd3dd61e08bf2f760effa74309fbefd97b2b31afb2a60f526d196a", + "ball_kick_right.onnx": "147a32c388c6b19111b3ac3b550a9a6dc8b8bf267118af4d8c3712522eedb5af", + "alpha_sitstand.onnx": "c6c40e35e726eabd803d633e090d112994f469921152448367953fbaf9799bc8", +} + +GENESIS_REPO = "Macmachi/microduck-rl-genesis" +GENESIS_REVISION = "9d1f213879650f2623e3bbd7bf06fe63dbf71a10" +GENESIS_ARTIFACT_SHA256 = { + "policies/backlash.onnx": "3f8db8bc2c11b2e41665633c1780af21bae3fda7db229eb5035e6c2d5698c075", + "policies/rough.onnx": "04261902d3651dc02303e3e9e5ab756062c4d93c45400f431a0ae68b5969185c", + "policies/velocity.onnx": "c315b9159a1b6f30976c90074ed6df2a33e7e1d14ef1505aed6c2c673f59061d", +} + +JUMP_SOURCE = { + "provider": "github", + "repo": "Liyucheng1997/318_lab-microduck-simulator", + "revision": "512d4bec6fc3ba321d29c93312be72856ad21268", + "artifact_path": "app/public/policies/jump.onnx", + "artifact_sha256": "0b10d7f50f2225467771c1fd11e027490e775b762c2e50c9e25f82c0f488e5c4", + "manifest_path": None, + "manifest_sha256": None, +} + +MAX_HEIGHT_JUMP_SOURCE = { + "provider": "github", + "repo": "ThomasBurgess2000/microduck-max-height-jump", + "revision": "7e5dc6028900f13d145e6710847378b007a675e9", + "artifact_path": "policy/max_height_jump.onnx", + "artifact_sha256": "046debd3eebd61a8c027d5595c1bca4fe32056fbb0ae63ac0b2f4e3798e1270f", + "manifest_path": None, + "manifest_sha256": None, +} def _duration(value: Any) -> float | None: if isinstance(value, bool) or not isinstance(value, (int, float)): return None value = float(value) - return value if isfinite(value) and 1.0 <= value <= 30.0 else None + return value if isfinite(value) and 0.0 < value <= 30.0 else None + + +def _source_matches(source: dict[str, Any] | None, expected: dict[str, Any]) -> bool: + return isinstance(source, dict) and all(source.get(key) == value for key, value in expected.items()) + + +def _pollen_source(artifact_path: str) -> dict[str, Any]: + return { + "provider": "huggingface-model", + "repo": POLLEN_POLICY_REPO, + "revision": POLLEN_POLICY_REVISION, + "artifact_path": artifact_path, + "artifact_sha256": POLLEN_ARTIFACT_SHA256[artifact_path], + "manifest_path": POLLEN_MANIFEST_PATH, + "manifest_sha256": POLLEN_MANIFEST_SHA256, + } + + +def _execution_contract(action_scale: float, model: str = MODEL) -> dict[str, Any]: + """Contract facts admitted only by an exact trusted source recipe.""" + + return { + "obs_len": 61, + "action_len": 14, + "action_scale": float(action_scale), + "robot": {"model": "microduck", "hw_rev": 1, "servos": "xl330", "control_hz": 50}, + "model": model, + } + + +def _provenance(source: str, source_url: str, scope: str, **facts: Any) -> dict[str, Any]: + return { + "owner": "uduck-registry-maintainers", + "source": source, + "source_url": source_url, + "scope": scope, + **facts, + } + + +def _velocity_recipe( + *, + model: str, + action_scale: float, + provenance: dict[str, Any], + yaw_rate: float = 0.5, + contract: dict[str, Any] | None = None, +) -> dict[str, Any]: + """A finite, source-bound locomotion diagnostic, not a publisher eval.""" + + recipe = { + "runner": RUNNER, + "model": model, + "scene": SCENE, + "start": deepcopy(START), + "scenario": "velocity", + "duration_s": 6.0, + "segments": [ + {"duration_s": 1.0, "vx": 0.0, "vy": 0.0, "wz": 0.0}, + {"duration_s": 3.0, "vx": 0.25, "vy": 0.0, "wz": 0.0}, + {"duration_s": 2.0, "vx": 0.25, "vy": 0.0, "wz": yaw_rate}, + ], + "checks": ["no_fall", "ends_upright", "velocity_tracking"], + "action_scale": float(action_scale), + "provenance": provenance, + } + if contract is not None: + recipe["contract"] = contract + return recipe + + +def _official_phase_recipe(manifest: dict[str, Any], source: dict[str, Any], *, roller: bool) -> dict[str, Any] | None: + command = manifest.get("command") + duration = _duration(manifest.get("duration_s")) + if ( + manifest.get("kind") != "episodic" + or not isinstance(command, dict) + or command.get("encoding") != "phase" + or not isinstance(command.get("period_s"), (int, float)) + or not isinstance(command.get("end_phase"), (int, float)) + or duration is None + or not isfinite(float(command["period_s"])) + or not isfinite(float(command["end_phase"])) + or not 0.0 < float(command["period_s"]) + or not 0.0 < float(command["end_phase"]) <= 1.0 + ): + return None + model = "microduck-rollers" if roller else MODEL + action_scale = manifest.get("action_scale", 0.8 if roller else 1.0) + if isinstance(action_scale, bool) or not isinstance(action_scale, (int, float)) or not isfinite(float(action_scale)): + return None + return { + "runner": RUNNER, + "model": model, + "scene": SCENE, + "start": deepcopy(START), + "scenario": "oneshot_phase", + "duration_s": duration, + "period_s": float(command["period_s"]), + "end_phase": float(command["end_phase"]), + "checks": ["no_fall", "ends_upright"], + "action_scale": float(action_scale), + "provenance": _provenance( + "Exact per-file Pollen schema-2 manifest plus the upstream phase-command contract", + POLLEN_MANIFEST_URL, + "Registry diagnostic rollout under flat-v1 with the manifest's phase timing; this does not establish intended-task success or hardware verification.", + policy_set_revision=POLLEN_POLICY_REVISION, + manifest_sha256=POLLEN_MANIFEST_SHA256, + artifact_path=source["artifact_path"], + command_encoding="phase", + command_slots=command.get("slots"), + period_s=float(command["period_s"]), + end_phase=float(command["end_phase"]), + action_scale=float(action_scale), + robot_model=model, + ), + } + + +def _official_recipe(manifest: dict[str, Any], source: dict[str, Any]) -> dict[str, Any] | None: + artifact_path = source.get("artifact_path") + if not isinstance(artifact_path, str) or artifact_path not in POLLEN_ARTIFACT_SHA256: + return None + if not _source_matches(source, _pollen_source(artifact_path)) or manifest.get("file") != artifact_path: + return None + + if artifact_path == "alpha_walking.onnx" and manifest.get("kind") == "perpetual": + return _velocity_recipe( + model=MODEL, + action_scale=0.9, + provenance=_provenance( + "Pollen policy manifest and the pinned Microduck alpha runtime command contract", + UPSTREAM_MANIFEST_URL, + "Finite flat-v1 velocity diagnostic for a perpetual gait; this is not publisher evaluation or hardware evidence.", + policy_set_revision=POLLEN_POLICY_REVISION, + manifest_sha256=POLLEN_MANIFEST_SHA256, + artifact_path=artifact_path, + command_semantics="twist = [vx, vy, wz] from the Microduck 61D command block", + command_schedule="idle 1s, forward 0.25m/s 3s, forward 0.25m/s plus yaw 0.5rad/s 2s", + action_scale=0.9, + action_scale_source=UPSTREAM_CONTROL_URL, + ), + ) + + if artifact_path == "roller.onnx" and manifest.get("kind") == "perpetual" and manifest.get("mode") == "roller" and manifest.get("action_scale") == 0.8: + return _velocity_recipe( + model="microduck-rollers", + action_scale=0.8, + yaw_rate=0.25, + provenance=_provenance( + "Exact Pollen policy-set manifest plus the pinned roller command contract", + POLLEN_MANIFEST_URL, + "Finite flat-v1 roller diagnostic using the pinned roller MJCF; this does not reproduce publisher roller evaluation or establish hardware evidence.", + policy_set_revision=POLLEN_POLICY_REVISION, + manifest_sha256=POLLEN_MANIFEST_SHA256, + artifact_path=artifact_path, + command_semantics="twist = [vx, vy, wz]; roller diagnostic stays within the documented 0.6m/s forward and 0.3rad/s yaw envelope", + command_schedule="idle 1s, forward 0.25m/s 3s, forward 0.25m/s plus yaw 0.25rad/s 2s", + action_scale=0.8, + ), + ) + + if artifact_path == "alpha_ground_pick.onnx": + return _official_phase_recipe(manifest, source, roller=False) + if artifact_path == "roller_crouch.onnx" and manifest.get("mode") == "roller" and manifest.get("action_scale") == 0.8: + return _official_phase_recipe(manifest, source, roller=True) + + if artifact_path in {"roulade.onnx", "ball_kick_left.onnx", "ball_kick_right.onnx"}: + if manifest.get("kind") != "episodic" or _duration(manifest.get("duration_s")) is None: + return None + if manifest.get("command") not in (None, {}): + return None + duration = float(manifest["duration_s"]) + checks = ["recover_upright"] if artifact_path == "roulade.onnx" else ["no_fall", "ends_upright"] + return { + "runner": RUNNER, + "model": MODEL, + "scene": SCENE, + "start": deepcopy(START), + "scenario": "oneshot_zero", + "duration_s": duration, + "checks": checks, + "action_scale": 1.0, + "chain": bool(manifest.get("chain", False)), + "provenance": _provenance( + "Exact per-file Pollen schema-2 manifest and the pinned robotd zero-command skill contract", + UPSTREAM_MANIFEST_URL, + "Registry diagnostic rollout of the exact policy window under flat-v1; this does not establish intended-task success or hardware verification.", + policy_set_revision=POLLEN_POLICY_REVISION, + manifest_sha256=POLLEN_MANIFEST_SHA256, + artifact_path=artifact_path, + command=[0.0, 0.0, 0.0], + command_semantics="Selecting an ordinary constant episodic skill is the trigger; the upstream runtime feeds the all-zero twist.", + action_scale=1.0, + action_scale_source=UPSTREAM_CONTROL_URL, + chain=bool(manifest.get("chain", False)), + ), + } + + if artifact_path == "alpha_sitstand.onnx": + command = manifest.get("command") + ramp_s = manifest.get("ramp_s") + unwind_s = manifest.get("unwind_s") + if ( + manifest.get("kind") != "scripted" + or not isinstance(command, dict) + or command.get("encoding") != "posture_flag" + or command.get("slot") != "twist.vx" + or command.get("sit") != 1.0 + or command.get("stand") != 0.0 + or not isinstance(ramp_s, (int, float)) + or not isinstance(unwind_s, (int, float)) + or not 0.0 < float(ramp_s) + or not 0.0 < float(unwind_s) + ): + return None + return { + "runner": RUNNER, + "model": MODEL, + "scene": SCENE, + "start": deepcopy(START), + "scenario": "sitstand", + "duration_s": float(ramp_s) + float(unwind_s), + "hold_s": float(ramp_s), + "checks": ["recover_upright"], + "action_scale": 1.0, + "provenance": _provenance( + "Exact per-file Pollen schema-2 posture-flag manifest and the pinned sitstand timing contract", + POLLEN_MANIFEST_URL, + "Registry diagnostic that holds the manifest's sit timing then drives its declared rise/unwind timing; it does not reproduce the full daemon handoff or establish hardware verification.", + policy_set_revision=POLLEN_POLICY_REVISION, + manifest_sha256=POLLEN_MANIFEST_SHA256, + artifact_path=artifact_path, + command=command, + ramp_s=float(ramp_s), + unwind_s=float(unwind_s), + action_scale=1.0, + action_scale_source=UPSTREAM_CONTROL_URL, + ), + } + return None + + +def _manifest_velocity_recipe(repo: str, manifest: dict[str, Any], source: dict[str, Any]) -> dict[str, Any] | None: + expected = { + "RemiFabre/microduck-rough-walk-e": { + "revision": "fa7b27eeb5610d3b351362f4bd71691ee8be3d7d", + "artifact_path": "policy.onnx", + "artifact_sha256": "5aa423bd693e431b19e2ead77f99cbae6184e40a529eb2f7c1b4f85bb7f57040", + "manifest_sha256": "f9b9cdbd7450de266ae1c7f6dd3ed1cc82fd5de01582bf073cc73d81cb2c0332", + }, + "RemiFabre/microduck-rough-walk-g": { + "revision": "242876a0aa8b40b702142fb0a5677fd43bc88a4c", + "artifact_path": "policy.onnx", + "artifact_sha256": "7a0d132f121d4bea3b713d3d7509500319389e0ac8de1ec9b390256471bbfc18", + "manifest_sha256": "a304b650a9fe558eb054695654d2b2a346a2c246c05dd56f4524a1fff42174c6", + }, + "HannesVonEssen/microduck-running": { + "revision": "d839a07cd2cb4bdc2850ca72bf00d9b549ec600a", + "artifact_path": "policy.onnx", + "artifact_sha256": "007707dd7779b2756ded67c58b2e9f94fe5071794a48c2b5a20d5f8d841efbeb", + "manifest_sha256": "7d70763e525e23d6c37b4f991be3732e2cd67be84f8c7c510ce4652cd63f1487", + }, + }.get(repo) + if expected is None: + return None + expected_source = { + "provider": "huggingface-model", + "repo": repo, + "revision": expected["revision"], + "artifact_path": expected["artifact_path"], + "artifact_sha256": expected["artifact_sha256"], + "manifest_path": "manifest.json", + "manifest_sha256": expected["manifest_sha256"], + } + if not _source_matches(source, expected_source): + return None + robot = manifest.get("robot") + command = manifest.get("command") + if ( + manifest.get("kind") != "perpetual" + or manifest.get("action_scale") != 1.0 + or manifest.get("entry_pose") != "standing" + or not isinstance(robot, dict) + or robot.get("model") != "microduck" + or robot.get("control_hz") != 50 + or not isinstance(command, dict) + or not isinstance(command.get("twist"), list) + ): + return None + yaw = 0.0 if repo == "HannesVonEssen/microduck-running" else 0.5 + readme_url = f"https://huggingface.co/{repo}/blob/{expected['revision']}/README.md" + return _velocity_recipe( + model=MODEL, + action_scale=1.0, + yaw_rate=yaw, + provenance=_provenance( + "Exact pinned Hugging Face manifest and publisher command contract", + readme_url, + "Finite flat-v1 velocity diagnostic using the registry runner; rough-terrain and high-speed publisher evaluations remain publisher claims.", + revision=expected["revision"], + manifest_sha256=expected["manifest_sha256"], + artifact_path=expected["artifact_path"], + command_semantics="twist = [vx, vy, wz] in the 61D command block; the registry uses a bounded diagnostic schedule rather than claiming the publisher's full envelope", + command_schedule="idle 1s, forward 0.25m/s 3s, then forward 0.25m/s with a bounded yaw probe 2s" if yaw else "idle 1s followed by two forward 0.25m/s segments", + action_scale=1.0, + ), + ) + + +def _genesis_recipe(source: dict[str, Any]) -> dict[str, Any] | None: + artifact_path = source.get("artifact_path") + if not isinstance(artifact_path, str) or artifact_path not in GENESIS_ARTIFACT_SHA256: + return None + expected = { + "provider": "github", + "repo": GENESIS_REPO, + "revision": GENESIS_REVISION, + "artifact_path": artifact_path, + "artifact_sha256": GENESIS_ARTIFACT_SHA256[artifact_path], + "manifest_path": None, + "manifest_sha256": None, + } + if not _source_matches(source, expected): + return None + contract = _execution_contract(1.0) + return _velocity_recipe( + model=MODEL, + action_scale=1.0, + contract=contract, + provenance=_provenance( + "Exact pinned Genesis export plus its published 61D/14D/50Hz velocity contract", + f"https://github.com/{GENESIS_REPO}/blob/{GENESIS_REVISION}/SIM2REAL.md", + "Finite flat-v1 registry diagnostic of a policy exported from the Genesis port; this does not reproduce its Genesis terrain/actuator evaluation or establish hardware evidence.", + revision=GENESIS_REVISION, + artifact_path=artifact_path, + command_semantics="twist = [vx, vy, wz] with the source's velocity-commanded walking contract", + command_schedule="idle 1s, forward 0.25m/s 3s, forward 0.25m/s plus yaw 0.5rad/s 2s", + action_scale=1.0, + contract_source=f"https://github.com/{GENESIS_REPO}/blob/{GENESIS_REVISION}/microduck/velocity_cfg.py", + ), + ) + + +def _jump_recipe(source: dict[str, Any]) -> dict[str, Any] | None: + if not _source_matches(source, JUMP_SOURCE): + return None + contract = _execution_contract(1.0) + return { + "runner": RUNNER, + "model": MODEL, + "scene": SCENE, + "start": deepcopy(START), + "scenario": "oneshot_zero", + "duration_s": 2.0, + "checks": ["no_fall", "ends_upright"], + "action_scale": 1.0, + "contract": contract, + "provenance": _provenance( + "Exact pinned jump artifact and the publisher's browser/runtime jump contract", + f"https://github.com/{JUMP_SOURCE['repo']}/blob/{JUMP_SOURCE['revision']}/app/src/game/constants.js", + "Registry diagnostic rollout of the primary jump ONNX under flat-v1; publisher fall recovery and browser hand-back are not simulated, and this establishes no hardware verification.", + revision=JUMP_SOURCE["revision"], + artifact_path=JUMP_SOURCE["artifact_path"], + command=[0.0, 0.0, 0.0], + command_semantics="Policy selection is the trigger; the publisher's jump controller receives zero-padded command slots.", + duration_s=2.0, + action_scale=1.0, + contract_source=f"https://github.com/{JUMP_SOURCE['repo']}/blob/{JUMP_SOURCE['revision']}/app/src/game/constants.js", + ), + } + + +def _max_height_jump_recipe(source: dict[str, Any]) -> dict[str, Any] | None: + if not _source_matches(source, MAX_HEIGHT_JUMP_SOURCE): + return None + contract = _execution_contract(1.0) + return { + "runner": RUNNER, + "model": MODEL, + "scene": SCENE, + "start": deepcopy(START), + "scenario": "oneshot_trigger", + "duration_s": 1.0, + "trigger_s": 0.75, + "checks": ["takeoff", "touchdown_after_takeoff"], + "action_scale": 1.0, + "contract": contract, + "provenance": _provenance( + "Exact pinned jump artifact, publisher README, and its handoff manifest", + f"https://github.com/{MAX_HEIGHT_JUMP_SOURCE['repo']}/blob/{MAX_HEIGHT_JUMP_SOURCE['revision']}/README.md", + "Primary jump-policy diagnostic only; the separately hashed standing handoff is intentionally not run as part of this entry, and fixed-timer clearing is an explicit unvalidated fallback rather than a claim of deployment equivalence.", + revision=MAX_HEIGHT_JUMP_SOURCE["revision"], + artifact_path=MAX_HEIGHT_JUMP_SOURCE["artifact_path"], + launch_command=[1.0, 0.0, 0.0], + settle_command=[0.0, 0.0, 0.0], + trigger_s=0.75, + action_scale=1.0, + contract_source=f"https://github.com/{MAX_HEIGHT_JUMP_SOURCE['repo']}/blob/{MAX_HEIGHT_JUMP_SOURCE['revision']}/README.md", + handoff_source=f"https://github.com/{MAX_HEIGHT_JUMP_SOURCE['repo']}/blob/{MAX_HEIGHT_JUMP_SOURCE['revision']}/handoff/manifest.json", + ), + } def _generic_zero_recipe(manifest: dict[str, Any]) -> dict[str, Any] | None: @@ -119,24 +566,43 @@ def _flamingo_recipe() -> dict[str, Any]: } -def recipe_for_policy(repo: str, manifest: dict[str, Any], source: dict[str, Any] | None = None) -> dict[str, Any] | None: +def recipe_for_policy(repo: str, manifest: dict[str, Any] | None, source: dict[str, Any] | None = None) -> dict[str, Any] | None: """Return a reviewed recipe only for an exact source/manifest match.""" - if not isinstance(repo, str) or not isinstance(manifest, dict): + if not isinstance(repo, str): return None - if repo.casefold() == FLAMINGO_REPO.casefold() and manifest.get("name") == FLAMINGO_NAME and source is not None and all(source.get(key) == value for key, value in FLAMINGO_SOURCE.items()): + if repo.casefold() == FLAMINGO_REPO.casefold() and isinstance(manifest, dict) and manifest.get("name") == FLAMINGO_NAME and source is not None and all(source.get(key) == value for key, value in FLAMINGO_SOURCE.items()): return _flamingo_recipe() - return _generic_zero_recipe(manifest) + if source is not None: + if manifest is not None: + official = _official_recipe(manifest, source) + if official is not None: + return official + community = _manifest_velocity_recipe(repo, manifest, source) + if community is not None: + return community + genesis = _genesis_recipe(source) + if genesis is not None: + return genesis + jump = _jump_recipe(source) + if jump is not None: + return jump + max_height_jump = _max_height_jump_recipe(source) + if max_height_jump is not None: + return max_height_jump + return _generic_zero_recipe(manifest) if isinstance(manifest, dict) else None -def recipe_reason(repo: str, manifest: dict[str, Any], source: dict[str, Any] | None = None) -> str: +def recipe_reason(repo: str, manifest: dict[str, Any] | None, source: dict[str, Any] | None = None) -> str: """Explain why no registry-owned execution recipe applies.""" if repo.casefold() == FLAMINGO_REPO.casefold(): - if manifest.get("name") != FLAMINGO_NAME: + if not isinstance(manifest, dict) or manifest.get("name") != FLAMINGO_NAME: return "The reviewed Flamingo recipe is bound to manifest name flamingo-cycle." if source is None or not all(source.get(key) == value for key, value in FLAMINGO_SOURCE.items()): return "The reviewed Flamingo recipe is bound to its pinned revision and manifest/artifact hashes." + if not isinstance(manifest, dict): + return "No machine-readable policy manifest is published with this artifact." if manifest.get("kind") == "perpetual": return "Perpetual policies require a maintainer-reviewed activation command and finite hold window." if manifest.get("kind") == "scripted": diff --git a/simulation/microduck_sim/preflight.py b/simulation/microduck_sim/preflight.py index 6fc8e8e..25ef33c 100644 --- a/simulation/microduck_sim/preflight.py +++ b/simulation/microduck_sim/preflight.py @@ -89,8 +89,8 @@ def preflight_execution(spec: "ExecutionSpec") -> PreflightResult: duration = recipe.get("duration_s") duration_value = float(duration) if _finite(duration) else None - if duration_value is None or not 1 <= duration_value <= 30: - errors.append("execution duration_s must be finite and between 1 and 30 seconds") + if duration_value is None or not 0 < duration_value <= 30: + errors.append("execution duration_s must be finite, positive, and at most 30 seconds") segments = recipe.get("segments") if scenario == "velocity": diff --git a/simulation/tests/test_evidence_identity.py b/simulation/tests/test_evidence_identity.py index ba0aaa8..54a0901 100644 --- a/simulation/tests/test_evidence_identity.py +++ b/simulation/tests/test_evidence_identity.py @@ -3,8 +3,10 @@ import sys import unittest from pathlib import Path +from unittest.mock import patch sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +import evidence # noqa: E402 from evidence import EVIDENCE_VERSION, IDENTITY_VERSION, execution_inputs, inputs_digest # noqa: E402 @@ -23,6 +25,28 @@ def test_identity_contains_execution_inputs_but_not_curation(self) -> None: self.assertIn("simulation", inputs) self.assertNotIn("curation", inputs) + def test_recipe_change_is_scoped_to_the_changed_entry(self) -> None: + original_alpha = inputs_digest("alpha-walking") + original_jump = inputs_digest("jump") + alpha_inputs = execution_inputs("alpha-walking") + changed_alpha = { + **alpha_inputs, + "simulation": { + **alpha_inputs["simulation"], + "recipe": { + **alpha_inputs["simulation"]["recipe"], + "duration_s": 7.0, + }, + }, + } + + def changed_inputs(entry_id: str) -> dict: + return changed_alpha if entry_id == "alpha-walking" else execution_inputs(entry_id) + + with patch.object(evidence, "execution_inputs", side_effect=changed_inputs): + self.assertNotEqual(inputs_digest("alpha-walking"), original_alpha) + self.assertEqual(inputs_digest("jump"), original_jump) + if __name__ == "__main__": unittest.main() diff --git a/simulation/tests/test_execution_recipes.py b/simulation/tests/test_execution_recipes.py index e0171be..130feaf 100644 --- a/simulation/tests/test_execution_recipes.py +++ b/simulation/tests/test_execution_recipes.py @@ -6,7 +6,14 @@ from pathlib import Path from execution import execution_spec_from_policy -from execution_recipes import recipe_for_policy, recipe_reason +from execution_recipes import ( + POLLEN_ARTIFACT_SHA256, + POLLEN_MANIFEST_SHA256, + POLLEN_POLICY_REPO, + POLLEN_POLICY_REVISION, + recipe_for_policy, + recipe_reason, +) from microduck_sim.preflight import preflight_execution from microduck_sim.scenarios import make_command_fn, scenario_from_recipe @@ -82,6 +89,101 @@ def test_generic_zero_recipe_requires_explicit_contract(self) -> None: command_prose["command"]["twist"] = "forward speed" self.assertIsNone(recipe_for_policy("someone/microduck-bow", command_prose)) + def test_official_recipes_bind_to_exact_artifact_and_manifest_identity(self) -> None: + common = { + "provider": "huggingface-model", + "repo": POLLEN_POLICY_REPO, + "revision": POLLEN_POLICY_REVISION, + "manifest_path": "manifest.json", + "manifest_sha256": POLLEN_MANIFEST_SHA256, + } + manifests = { + "alpha_walking.onnx": {"file": "alpha_walking.onnx", "kind": "perpetual"}, + "alpha_ground_pick.onnx": { + "file": "alpha_ground_pick.onnx", "kind": "episodic", "duration_s": 2.8, + "command": {"encoding": "phase", "period_s": 4.0, "end_phase": 0.7}, + }, + "roller.onnx": {"file": "roller.onnx", "kind": "perpetual", "mode": "roller", "action_scale": 0.8}, + "roller_crouch.onnx": { + "file": "roller_crouch.onnx", "kind": "episodic", "duration_s": 3.5, + "mode": "roller", "action_scale": 0.8, + "command": {"encoding": "phase", "period_s": 5.0, "end_phase": 0.7}, + }, + "roulade.onnx": {"file": "roulade.onnx", "kind": "episodic", "duration_s": 1.0, "chain": True}, + "ball_kick_left.onnx": {"file": "ball_kick_left.onnx", "kind": "episodic", "duration_s": 0.5}, + "ball_kick_right.onnx": {"file": "ball_kick_right.onnx", "kind": "episodic", "duration_s": 0.5}, + "alpha_sitstand.onnx": { + "file": "alpha_sitstand.onnx", "kind": "scripted", "ramp_s": 2.0, "unwind_s": 1.0, + "command": {"encoding": "posture_flag", "slot": "twist.vx", "sit": 1.0, "stand": 0.0}, + }, + } + for artifact_path, manifest in manifests.items(): + source = { + **common, + "artifact_path": artifact_path, + "artifact_sha256": POLLEN_ARTIFACT_SHA256[artifact_path], + } + recipe = recipe_for_policy(POLLEN_POLICY_REPO, manifest, source) + self.assertIsNotNone(recipe, artifact_path) + altered_revision = {**source, "revision": "0" * 40} + self.assertIsNone(recipe_for_policy(POLLEN_POLICY_REPO, manifest, altered_revision), artifact_path) + altered_hash = {**source, "artifact_sha256": "0" * 64} + self.assertIsNone(recipe_for_policy(POLLEN_POLICY_REPO, manifest, altered_hash), artifact_path) + + kick = recipe_for_policy( + POLLEN_POLICY_REPO, + manifests["ball_kick_left.onnx"], + {**common, "artifact_path": "ball_kick_left.onnx", "artifact_sha256": POLLEN_ARTIFACT_SHA256["ball_kick_left.onnx"]}, + ) + self.assertIsNotNone(kick) + assert kick is not None + self.assertEqual(kick["duration_s"], 0.5) + self.assertEqual(kick["scenario"], "oneshot_zero") + + def test_exact_no_manifest_recipes_supply_only_their_pinned_contract(self) -> None: + from execution_recipes import GENESIS_ARTIFACT_SHA256, GENESIS_REPO, GENESIS_REVISION + + source = { + "provider": "github", + "repo": GENESIS_REPO, + "revision": GENESIS_REVISION, + "artifact_path": "policies/velocity.onnx", + "artifact_sha256": GENESIS_ARTIFACT_SHA256["policies/velocity.onnx"], + "manifest_path": None, + "manifest_sha256": None, + } + recipe = recipe_for_policy(GENESIS_REPO, None, source) + self.assertIsNotNone(recipe) + assert recipe is not None + self.assertEqual(recipe["contract"]["obs_len"], 61) + self.assertEqual(recipe["contract"]["action_len"], 14) + self.assertEqual(recipe["contract"]["action_scale"], 1.0) + self.assertIsNone(recipe_for_policy(GENESIS_REPO, None, {**source, "artifact_sha256": "0" * 64})) + + def test_scenario_semantics_follow_recipe_fields(self) -> None: + phase = scenario_from_recipe({ + "runner": "microduck-standard-v1", "scenario": "oneshot_phase", "duration_s": 2.8, + "period_s": 4.0, "end_phase": 0.7, + }) + phase_fn = make_command_fn(phase, use_13d=False) + self.assertAlmostEqual(float(phase_fn(0.0)[0]), 1.0, places=6) + self.assertAlmostEqual(float(phase_fn(2.0)[0]), -1.0, places=5) + self.assertEqual(phase_fn(2.8).tolist(), [0.0, 0.0, 0.0]) + + posture = scenario_from_recipe({ + "runner": "microduck-standard-v1", "scenario": "sitstand", "duration_s": 3.0, "hold_s": 2.0, + }) + posture_fn = make_command_fn(posture, use_13d=False) + self.assertEqual(posture_fn(1.99).tolist(), [1.0, 0.0, 0.0]) + self.assertEqual(posture_fn(2.0).tolist(), [0.0, 0.0, 0.0]) + + trigger = scenario_from_recipe({ + "runner": "microduck-standard-v1", "scenario": "oneshot_trigger", "duration_s": 1.0, "trigger_s": 0.75, + }) + trigger_fn = make_command_fn(trigger, use_13d=False) + self.assertEqual(trigger_fn(0.74).tolist(), [1.0, 0.0, 0.0]) + self.assertEqual(trigger_fn(0.75).tolist(), [0.0, 0.0, 0.0]) + def test_uncovered_policy_has_no_execution_spec(self) -> None: policy = {"id": "mystery", "source": {**FLAMINGO_SOURCE, "repo": "someone/mystery"}} resolved = {"manifest": None, "simulation": {"status": "not-covered", "reason": "No recipe."}} diff --git a/simulation/tests/test_preflight.py b/simulation/tests/test_preflight.py index b1494a9..02e8c55 100644 --- a/simulation/tests/test_preflight.py +++ b/simulation/tests/test_preflight.py @@ -33,6 +33,22 @@ def test_accepts_a_complete_supported_recipe(self) -> None: result = preflight_execution(spec()) self.assertTrue(result.valid, result.errors) + def test_accepts_truthful_subsecond_policy_windows(self) -> None: + candidate = spec() + candidate.recipe["duration_s"] = 0.5 + candidate.recipe["segments"] = [{"duration_s": 0.5, "vx": 0.0, "vy": 0.0, "wz": 0.0}] + result = preflight_execution(candidate) + self.assertTrue(result.valid, result.errors) + + def test_rejects_nonpositive_policy_windows(self) -> None: + for duration in (0, -0.1): + candidate = spec() + candidate.recipe["duration_s"] = duration + candidate.recipe["segments"] = [{"duration_s": duration, "vx": 0.0, "vy": 0.0, "wz": 0.0}] + result = preflight_execution(candidate) + self.assertFalse(result.valid) + self.assertTrue(any("duration_s" in error for error in result.errors)) + def test_rejects_command_outside_the_runtime_range(self) -> None: candidate = spec() candidate.recipe["segments"][0]["vx"] = 2.2 diff --git a/tests/catalog.test.ts b/tests/catalog.test.ts index 43a15c1..bb27b3c 100644 --- a/tests/catalog.test.ts +++ b/tests/catalog.test.ts @@ -133,6 +133,29 @@ describe("policy catalog boundary", () => { expect(entry.coverage.registry_simulation.status).toBe("passed"); }); + it("uses complete registry evidence media as the primary catalog media", () => { + const entry = catalogEntryFromPolicy(flamingoPolicy(), { + status: "failed", + evidence_key: "a".repeat(64), + inputs_sha256: "b".repeat(64), + runner: "microduck-standard-v1", + scene: "flat-v1", + scenario: "command_schedule", + report_url: "/media/registry-sim/flamingo-cycle/report.json", + loop_url: "/media/registry-sim/flamingo-cycle/loop.mp4", + poster_url: "/media/registry-sim/flamingo-cycle/poster.png", + checks: [{ check: "no_fall", passed: false, detail: "measured" }], + reason: "The requested diagnostic check failed.", + }); + expect(entry.media.primary).toBe("registry"); + expect(entry.media.registry).toEqual({ + loop_url: "/media/registry-sim/flamingo-cycle/loop.mp4", + poster_url: "/media/registry-sim/flamingo-cycle/poster.png", + report_url: "/media/registry-sim/flamingo-cycle/report.json", + }); + expect(entry.coverage.registry_simulation.status).toBe("failed"); + }); + it("only synthesizes exact robotctl targets for single-artifact Hugging Face models", () => { const base = flamingoPolicy(); base.resolved = { diff --git a/tests/contributor-tools.test.ts b/tests/contributor-tools.test.ts index 60f0b8d..7adbb21 100644 --- a/tests/contributor-tools.test.ts +++ b/tests/contributor-tools.test.ts @@ -9,6 +9,8 @@ describe("contributor-facing catalog documentation", () => { expect(policyCount).toBe(18); expect(readme).toContain("https://uduckmoves.com"); expect(readme).toContain("https://uduckmoves.com/registry.json"); + expect(readme).toContain("The live catalog is generated from the authored policies"); + expect(readme).not.toMatch(/(?:empty|no) (?:policy|behavior|entry|catalog|registry)/i); expect(readme).not.toContain("BEGIN GENERATED CATALOG TABLE"); expect(readme).not.toContain("| --- | --- | --- | --- | --- | --- | --- |"); }); diff --git a/tests/test_policy_resolver.py b/tests/test_policy_resolver.py index 54a6922..5dafc24 100644 --- a/tests/test_policy_resolver.py +++ b/tests/test_policy_resolver.py @@ -9,6 +9,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / 'scripts/policy')) from ingest_issue import parse_issue from resolve import _discover_source, classify, digest, parse_artifact_url, parse_source_url, parse_url, register_policy, resolve, resolve_source, select_manifest_for_artifact, validate_policy +from simulation.execution_recipes import GENESIS_ARTIFACT_SHA256, GENESIS_REPO, GENESIS_REVISION, POLLEN_ARTIFACT_SHA256, POLLEN_MANIFEST_SHA256, POLLEN_POLICY_REPO, POLLEN_POLICY_REVISION MANIFEST = { @@ -158,6 +159,50 @@ def fetch(url, *args): with self.assertRaisesRegex(ValueError, 'no unique entry'): resolve_source({**source, 'artifact_path': 'missing.onnx'}) + def test_exact_official_manifest_member_admits_the_matching_registry_recipe(self): + source = { + 'provider': 'huggingface-model', + 'repo': POLLEN_POLICY_REPO, + 'revision': POLLEN_POLICY_REVISION, + 'artifact_path': 'alpha_ground_pick.onnx', + 'artifact_sha256': POLLEN_ARTIFACT_SHA256['alpha_ground_pick.onnx'], + 'manifest_path': 'manifest.json', + 'manifest_sha256': POLLEN_MANIFEST_SHA256, + } + raw_manifest = json.dumps(POLLEN_POLICY_SET).encode() + + def fetch(url, *args): + if url.endswith('/manifest.json'): + return raw_manifest + return b'{}' + + def fake_digest(data): + return POLLEN_MANIFEST_SHA256 if data == raw_manifest else source['artifact_sha256'] + + with patch('resolve.fetch', fetch), patch('resolve.digest', fake_digest), patch('resolve.inspect_onnx', return_value={'smoke': 'passed'}): + result = resolve_source(source) + self.assertEqual(result['manifest']['file'], 'alpha_ground_pick.onnx') + self.assertEqual(result['simulation']['status'], 'covered') + self.assertEqual(result['simulation']['recipe']['scenario'], 'oneshot_phase') + self.assertEqual(result['simulation']['recipe']['duration_s'], 2.8) + + def test_exact_no_manifest_source_can_be_covered_without_a_general_fallback(self): + source = { + 'provider': 'github', + 'repo': GENESIS_REPO, + 'revision': GENESIS_REVISION, + 'artifact_path': 'policies/velocity.onnx', + 'artifact_sha256': GENESIS_ARTIFACT_SHA256['policies/velocity.onnx'], + 'manifest_path': None, + 'manifest_sha256': None, + } + with patch('resolve.fetch', return_value=b'not-json'), patch('resolve.digest', return_value=source['artifact_sha256']), patch('resolve.inspect_onnx', return_value={'smoke': 'passed'}): + result = resolve_source(source) + self.assertIsNone(result['manifest']) + self.assertEqual(result['simulation']['status'], 'covered') + self.assertEqual(result['simulation']['recipe']['contract']['obs_len'], 61) + self.assertEqual(result['simulation']['recipe']['scenario'], 'velocity') + def test_official_pollen_entries_select_their_exact_policy_set_members(self): expected = { 'alpha-walking': ('alpha_walking.onnx', 'perpetual', None, 'constant'),