Browse Source

recipe contract gate: sm121 validate + make-check test; archive 22 superseded lanes

- New WARN  glm53-flash-nvfp4-r18 [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/
WARN  deepseek-v4-flash-dspark38 [no-parent-model]: model set without parent_model (report tree grouping)
WARN  glm53-flash-nvfp4-r18-4x-nvfp4kv-spark [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/
WARN  dsv4-vision-exp-vavrusa [no-parent-model]: model set without parent_model (report tree grouping)
WARN  gemma4-12b [no-parent-model]: model set without parent_model (report tree grouping)
WARN  glm53-flash-intel-w4a16 [image-dir-missing]: docker_image_dir 'glm53-jovian-r12' not found under docker-images/
WARN  glm53-flash-nvfp4-r17abi [image-dir-missing]: docker_image_dir 'glm53-jovian-r12' not found under docker-images/
WARN  dsv4-flash-vision-eugr [no-parent-model]: model set without parent_model (report tree grouping)
WARN  dsv4-flash-vision [no-parent-model]: model set without parent_model (report tree grouping)
WARN  glm53-flash-nvfp4-r18-4x-nvfp4kv [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/
WARN  glm53-flash-nvfp4-r18-2x-nvfp4kv-spark [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/
WARN  test-sleep [no-parent-model]: model set without parent_model (report tree grouping)
WARN  glm53-flash-nvfp4-r18-2x-nvfp4kv [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/
WARN  dsv41-flash-4x-b12x [no-parent-model]: model set without parent_model (report tree grouping)
WARN  glm53-flash-aiden-4x [no-parent-model]: model set without parent_model (report tree grouping)
WARN  dsv4-vision-aiden-2x [no-parent-model]: model set without parent_model (report tree grouping)
WARN  deepseek-v4-flash [no-parent-model]: model set without parent_model (report tree grouping)
WARN  dsv41-flash-4x-aiden11 [no-parent-model]: model set without parent_model (report tree grouping)
WARN  qwen-agentworld-35b [no-parent-model]: model set without parent_model (report tree grouping)
WARN  dsv41-flash-4x [no-parent-model]: model set without parent_model (report tree grouping)
WARN  glm53-flash-r18-2x-nvfp4kv-spark-eager [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/
WARN  test-mods [no-parent-model]: model set without parent_model (report tree grouping)
WARN  deepseek-v4-flash-src [no-parent-model]: model set without parent_model (report tree grouping)
WARN  deepseek-v4-flash-eugr [no-parent-model]: model set without parent_model (report tree grouping)
WARN  laguna-m1-nvfp4 [no-parent-model]: model set without parent_model (report tree grouping)
WARN  deepseek-v4-flash-eugr-agent [no-parent-model]: model set without parent_model (report tree grouping)
WARN  glm53-flash-nvfp4-r18-4x-dcp4 [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/
WARN  glm53-flash-nvfp4-r18-4x [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/
WARN  qwen38-27b [no-parent-model]: model set without parent_model (report tree grouping)
WARN  dsv4-vision-aiden-4x [no-parent-model]: model set without parent_model (report tree grouping)
WARN  dsv41-flash-8x [no-parent-model]: model set without parent_model (report tree grouping)
WARN  dsv41-flash-4x-dcp2 [no-parent-model]: model set without parent_model (report tree grouping)
WARN  test-composed [no-parent-model]: model set without parent_model (report tree grouping)
WARN  laguna-s2.1-nvfp4 [no-parent-model]: model set without parent_model (report tree grouping)
WARN  glm53-flash-nvfp4-r18-dflash2 [image-dir-missing]: docker_image_dir 'glm53-jovian-r18' not found under docker-images/ (src/validate.rs): offline contract check over all
  recipe sources — docker_image_dir exists, port 30000, no floating
  :latest on engine images, floating model revisions, required_nodes
  1..=8. Active recipes: errors (exit 1). Archived lanes grandfathered
  (errors -> warnings). A cargo test runs the validator over the repo's
  own recipes/, so
running 344 tests
test ansi::tests::strip_ansi_survives_split_chunks ... ok
test ansi::tests::strip_ansi_removes_codes_keeps_text ... ok
test before_serve::tests::combined_empty_when_unconfigured ... ok
test after_stop::tests::empty_after_stop_does_nothing ... ok
test before_serve::tests::combined_global_applies_to_unlisted_cluster ... ok
test before_serve::tests::combined_orders_cluster_then_recipe ... ok
test before_serve::tests::combined_runs_global_then_cluster_then_recipe ... ok
test before_serve::tests::parses_recipe_level_before_serve ... ok
test before_serve::tests::truncate_keeps_short_string_unchanged ... ok
test after_stop::tests::deserialize_before_and_after_stop_coexist ... ok
test after_stop::tests::deserialize_after_stop_script_entry ... ok
test before_serve::tests::truncate_cjk_chars ... ok
test before_serve::tests::truncate_shortens_to_char_boundary ... ok
test before_serve::tests::truncate_does_not_panic_on_multibyte ... ok
test before_serve::tests::parses_cluster_level_before_serve ... ok
test bench::tests::sanitize_replaces_bad_chars ... ok
test bench_artifacts::tests::old_manifest_without_models_deserializes ... ok
test bench_artifacts::tests::short12_truncates ... ok
test bench::tests::served_model_parsed_from_args ... ok
test bench_artifacts::tests::sha256_hex_known_value ... ok
test bench_artifacts::tests::utc_now_iso_is_realistic ... ok
test bench_depth::tests::depths_exclude_the_covered_points ... ok
test bench_depth::tests::tokenizer_forwarded_when_given ... ok
test bench_depth::tests::depth_args_are_perf_only_with_campaign_pp_tg ... ok
test builtin_scripts::tests::known_aliases_lists_all ... ok
test builtin_scripts::tests::resolves_known_aliases ... ok
test builtin_scripts::tests::scripts_reference_each_other_correctly ... ok
test builtin_scripts::tests::unknown_alias_returns_none ... ok
test bench_report::tests::no_rows_is_an_error ... ok
test bench_report::tests::appends_section_from_log_box ... ok
^D^Dtest after_stop::tests::dry_run_emits_for_each_command ... ok
test cli::tests::health_format_rejects_invalid_value ... ok
test bench_artifacts::tests::run_package_writes_copy_and_manifest ... ok
test cli::tests::health_format_defaults_to_plain ... ok
test cluster_state::tests::free_count_missing_node_not_free ... ok
test cluster_state::tests::free_count_all_reachable ... ok
test cluster_state::tests::free_count_occupied_excluded ... ok
test cluster_state::tests::free_count_unreachable_excluded ... ok
test cli::tests::report_flag_defaults_and_overrides ... ok
test cli::tests::health_format_combines_with_cluster_and_fingerprint ... ok
test cli::tests::health_format_parses_each_variant ... ok
test cluster_state::tests::single_node_allows_cluster_without_nccl ... ok
test cluster_state::tests::verdict_all_up ... ok
test cluster_state::tests::verdict_one_offline_node_takes_cluster_down ... ok
test cluster_state::tests::verdict_switch_off_takes_cluster_down ... ok
test config::mikrotik_tests::test_config_deserialize_mikrotik ... ok
test config::mikrotik_tests::test_config_mikrotik_absent ... ok
test cluster_state::tests::select_smallest_with_enough_free ... ok
test config::staging::tests::defaults_round_trip ... ok
test config::staging::tests::export_parses ... ok
test config::staging::tests::parses_full_staging_block ... ok
test config::staging::tests::presence_verify_parses ... ok
test config::staging::tests::sparse_rows_defaults_layers ... ok
test config::tests::default_config_dir_appends_sm121 ... ok
test config::tests::effective_master_ip_falls_back_to_rank0_ib_ip ... ok
test config::tests::effective_master_ip_falls_back_to_ssh_ip_when_ib_missing ... ok
test config::tests::effective_master_ip_prefers_explicit_config ... ok
test config::tests::effective_master_ip_uses_first_node_without_rank0 ... ok
test config::tests::registry_section_parses_and_defaults_to_none ... ok
test config::tests::test_composed_local_files_deserialize ... ok
test config::tests::test_composed_source_exclude_deserialize ... ok
test config::tests::test_config_deserialize ... ok
test config::tests::test_config_deserialize_with_host_key ... ok
test config::tests::test_default_distributed_backend_is_none ... ok
test config::tests::test_default_engine_is_vllm ... ok
test config::tests::test_deploy_defaults_to_docker ... ok
test config::tests::test_deploy_explicit_docker ... ok
test config::tests::test_deploy_explicit_tmux ... ok
test config::mikrotik_tests::test_merge_cluster_sources_merges_mikrotik ... ok
test config::tests::test_merge_cluster_sources_adds_new_keeps_main ... ok
test config::tests::test_recipe_deserialize_full ... ok
test config::tests::test_load_recipe_from_recipe_sources ... ok
test config::tests::test_recipe_deserialize_minimal ... ok
test config::tests::test_recipe_deserialize_no_required_nodes ... ok
test config::tests::test_select_cluster_auto ... ok
test config::tests::test_select_cluster_by_name ... ok
test config::tests::test_select_cluster_auto_no_nccl_allows_single_node ... ok
test config::tests::test_select_cluster_by_name_found ... ok
test config::tests::test_select_cluster_by_prefix ... ok
test cluster_state::tests::no_cluster_has_enough_free_errors ... ok
test cluster_state::tests::select_cluster_not_found_errors ... ok
test config::tests::test_select_cluster_by_name_not_found ... ok
test cluster_state::tests::multi_node_requires_nccl ... ok
test config::tests::test_select_cluster_multi_node_requires_nccl ... ok
test config::tests::test_venv_defaults_to_none ... ok
test config::tests::test_venv_field ... ok
TTY=0 NO=1
main
test config::tests::test_venv_setup_defaults_to_empty ... ok
test config::tests::test_select_cluster_not_found ... ok
test config::tests::test_venv_setup_list_deserializes ... ok
test config::types::tests::expand_tilde_user_and_root ... ok
test config::tests::test_venv_setup_inline_skips_comments_and_blanks ... ok
test config::tests::test_venv_setup_inline_deserializes ... ok
test config::types::tests::resolve_mount_falls_back_to_host_with_tilde ... ok
test config::types::tests::resolve_mount_host_only ... ok
test config::types::tests::resolve_mount_neither_errors ... ok
test config::types::tests::resolve_mount_alias_wins_over_host ... ok
test detect::tests::detect_cmd_scans_roce_v2_ipv4 ... ok
test detect::tests::resolve_returns_pinned_config_unchanged ... ok
test bench::tests::run_tool_passes_non_interactive_env ... ok
test config::types::tests::model_cache_dir_name_maps_repo_id ... ok
test docker::build::tests::format_elapsed_seconds_and_minutes ... ok
test docker::build::tests::parse_versions_env_quotes_are_normalized ... ok
test docker::build::tests::parse_versions_env_skips_comments_and_blanks ... ok
test docker::build::tests::serialize_versions_env_sorted_one_per_line ... ok
test docker::build::tests::registry_refs_version_and_latest ... ok
test docker::build_dir::tests::parse_versions_env_quotes_are_normalized ... ok
test docker::build_dir::tests::parse_versions_env_skips_comments_and_blanks ... ok
test docker::build_dir::tests::glob_match_basics ... ok
test docker::command::tests::clear_paths_dedups_and_sorts ... ok
test docker::command::tests::devices_pass_through_as_docker_flags ... ok
test docker::command::tests::entrypoint_flag_overrides_image_entrypoint ... ok
test docker::command::tests::env_ref_renders_in_docker_env ... ok
test docker::build::tests::build_checksum_no_profile_no_overrides_returns_none_and_dir_checksum ... ok
test docker::command::tests::env_ref_resolves_to_recipe_port ... ok
test docker::command::tests::env_ref_unknown_target_errors ... ok
test docker::build_dir::tests::resolve_falls_back_to_bundle_layout ... ok
test docker::command::tests::env_toml_parses_mixed_plain_and_ref ... ok
test docker::command::tests::extra_mounts_fallback_to_host_when_alias_absent ... ok
test docker::command::tests::extra_mounts_are_mounted ... ok
test docker::build::tests::build_checksum_overrides_change_checksum_and_produce_merged ... ok
test docker::command::tests::malicious_mount_container_cannot_inject ... ok
test docker::build::tests::build_checksum_missing_profile_lists_available ... ok
test docker::command::tests::malicious_env_value_cannot_inject ... ok
test docker::command::tests::model_path_ref_is_partitioned_not_mounted ... ok
test docker::command::tests::no_devices_no_flag ... ok
test docker::command::tests::substitute_path_refs_replaces_placeholders ... ok
test docker::command::tests::substitute_path_refs_in_env ... ok
test docker::command::tests::memory_and_cpus_cgroup_flags ... ok
test docker::health::tests::net_check_script_validates_interface_name ... ok
test docker::health::tests::parse_ps_output_splits_containers_and_net ... ok
test docker::command::tests::no_memory_limit_no_flags ... ok
test docker::health::tests::parse_ps_output_down_missing_error_flip_net_up ... ok
test docker::health::tests::render::html_escapes_hostile_names ... ok
test docker::health::tests::render::md_neutralizes_pipes_in_cells ... ok
test docker::build::tests::build_checksum_profile_merges_over_base_and_under_overrides ... ok
test docker::health::tests::render::down_cluster_suppresses_container_data ... ok
test docker::health::tests::render::recipes_summary_is_sorted_and_deterministic ... ok
test docker::health::tests::render::md_renders_headings_and_tables ... ok
test docker::health::tests::render::plain_layout_is_aligned_and_suppressed ... ok
test docker::health::tests::render::render_fleet_handles_each_format ... ok
test docker::health::tests::render::tmux_all_stopped_shows_summary_line ... ok
test docker::health::tests::verdict_all_online_counts_free ... ok
test docker::health::tests::verdict_from_entity_states_switch_off_downs_cluster ... ok
test docker::health::tests::verdict_offline_node_downs_cluster ... ok
test docker::health::tests::render::recipes_none_renders_no_recipes_section ... ok
test docker::logs::tests::follow_cmd_shell_quotes_recipe_name_with_single_quote ... ok
test docker::health::tests::render::tmux_running_entry_rendered_stopped_skipped ... ok
test docker::mirror::tests::copy_command_branches_and_quoting ... ok
test docker::mirror::tests::mirror_dir_default_tilde_and_explicit ... ok
test docker::mirror::tests::repos_gather_model_pathrefs_and_composed ... ok
test docker::logs::tests::tail_cmd_shell_quotes_recipe_name ... ok
test docker::mirror::tests::resolve_returns_mirror_and_falls_back_to_shared ... ok
test docker::mods::tests::mods_with_entrypoint_sh_c ... ok
test docker::mods::tests::empty_mods_preserves_tail ... ok
test docker::mods::tests::escaping_special_chars_in_commands ... ok
test docker::mods::tests::mods_wrapping_uses_entrypoint_sh ... ok
test docker::mods::tests::deserialize_mods_toml ... ok
test docker::mods::tests::on_failure_abort_exits_on_error ... ok
test docker::mods::tests::on_failure_continue_is_silent ... ok
test docker::mods::tests::on_failure_warn_continues ... ok
test docker::mods::tests::simple_mods_wraps_tail ... ok
test docker::stale::tests::stale_when_label_differs ... ok
test docker::stale::tests::fresh_when_label_matches ... ok
test docker::stale::tests::stale_when_label_missing ... ok
test docker::stop::tests::no_wait_omits_verification_loop ... ok
test docker::stop::tests::default_includes_verification_loop ... ok
test docker::sync::tests::safe_image_name_is_filename_safe ... ok
test doctor::tests::test_generate_repair_dockerfile_ray ... ok
test doctor::tests::test_generate_repair_dockerfile_vllm ... ok
test doctor::tests::test_generate_repair_dockerfile_sglang ... ok
test engine_flags::tests::multi_node_sglang_flags ... ok
test engine_flags::tests::multi_node_vllm_mp_flags ... ok
test engine_flags::tests::ray_backend_adds_executor ... ok
test engine_flags::tests::single_node_no_distributed_flags ... ok
test exec::tests::resolve_command_alias_and_args_errors ... ok
test exec::tests::resolve_command_literal_join ... ok
test exec::tests::resolve_command_alias ... ok
test exec::tests::resolve_command_no_alias_no_args_errors ... ok
test docker::health::tests::build_summary_excludes_down_clusters_and_sorts_nodes ... ok
test gconfig::tests::explicit_config_path_wins ... ok
test gconfig::tests::default_config_dir_is_some ... ok
test exec::tests::resolve_command_unknown_alias_no_aliases ... ok
test home_assistant::tests::endpoint_extracts_domain ... ok
test exec::tests::resolve_command_unknown_alias_lists_known ... ok
test home_assistant::tests::endpoint_rejects_no_domain ... ok
test home_assistant::tests::split_entity_basic ... ok
test home_assistant::tests::split_entity_empty_domain_errors ... ok
test home_assistant::tests::split_entity_empty_entity_errors ... ok
test home_assistant::tests::split_entity_no_domain_errors ... ok
test home_assistant::tests::mikrotik_device_without_backend_not_reported_off ... ok
test docker::health::tests::render::renders_in_active_locale ... ok
test gconfig::tests::missing_config_error_names_the_paths_searched ... ok
test home_assistant::tests::mikrotik_entity_unreachable_is_blocking_off ... ok
test host_keys::tests::preserves_trailing_newline ... ok
test host_keys::tests::no_host_key_for_absent_update ... ok
test host_keys::tests::adds_host_key_when_missing ... ok
test host_keys::tests::only_updates_targeted_nodes ... ok
test host_keys::tests::replaces_host_key_value ... ok
test host_keys::tests::updates_multiple_clusters ... ok
test mikrotik::tests::hex_roundtrip ... ok
test locale::tests::spawn_with_locale_inherits_locale ... ok
test mikrotik::tests::login_modern_ok ... ok
test mikrotik::tests::login_trap_fails ... ok
test mikrotik::tests::sentence_terminates_on_empty_word ... ok
test mikrotik::tests::shutdown_done ... ok
test mikrotik::tests::shutdown_trap_fails ... ok
test mikrotik::tests::login_legacy_challenge ... ok
test orchestrate::tests::dry_run_resolves_without_probing_when_all_nodes_unreachable ... ok
test orchestrate::tests::dry_run_solo_node_skips_probing ... ok
test mikrotik::tests::framing_band5_roundtrip ... ok
test orchestrate::tests::cluster_already_on_selects_directly ... ok
test orchestrate::tests::force_occupied_keeps_occupied_nodes ... ok
test orchestrate::tests::occupied_nodes_excluded ... ok
test orchestrate::tests::cluster_off_auto_power_on_powers_and_selects ... ok
test power::tests::cluster_without_extra_entities_is_empty ... ok
test power::tests::decide_on_cycles_only_when_plug_on_and_node_down ... ok
test power::tests::cluster_extra_entities_parsed ... ok
test power::tests::extra_entity_forms_blocking_and_soft ... ok
test power::tests::power_on_requires_ha_config ... ok
^Dtest power::tests::select_targets_all ... ok
test power::tests::select_targets_filters_by_node ... ok
test power::tests::mikrotik_dispatch_lookup ... ok
test power::tests::select_targets_multiple_nodes ... ok
test mikrotik::tests::framing_roundtrip_all_bands ... ok
test power::tests::select_targets_partial_missing_errors ... ok
test power::tests::select_targets_unknown_node_errors ... ok
test publish::tests::run_script_sglang_solo_omits_dist ... ok
test publish::tests::run_script_vllm_has_dist_flags_and_json_arg ... ok
test publish::tests::versions_env_parses_quoted_and_commented ... ok
test registry::tests::flat_bundle_splits_into_launcher_layout ... ok
test registry::tests::names_are_slugs ... ok
test orchestrate::tests::retry_tests::flaky_probe_recovers_on_retry ... ok
test orchestrate::tests::cluster_off_no_auto_power_fails ... ok
test report::perf_box_tests::parse_perf_box_ignores_progress_spinner_soup ... ok
test report::perf_box_tests::parse_perf_box_reads_both_table_shapes ... ok
test orchestrate::tests::retry_tests::insufficient_nodes_error_names_the_reason ... ok
test report::provenance::tests::bad_manifest_yields_empty_provenance ... ok
test report::repo::tests::quoted_value_picks_the_value_not_the_key_mention ... ok
test report::stale::tests::live_container_anywhere_keeps_it_running ... ok
test report::stale::tests::unknown_cluster_and_tmux_rows_are_left_alone ... ok
test report::stale::tests::open_run_without_live_container_is_stale ... ok
test report::stale::tests::unreachable_node_is_inconclusive ... ok
test publish::tests::flatten_overlay_glob_collision_keeps_dir_nested ... ok
test report::tests::bench_complete_requires_full_pool ... ok
test registry::tests::tar_gz_roundtrips_bundle ... ok
test report::tests::detect_quant_prefers_longest_token ... ok
test report::tests::extract_time_reads_both_timestamp_shapes ... ok
test publish::tests::flatten_overlay_rewrites_copy_lines_and_collision_renames ... ok
test report::tests::fmt_pct_whole_and_fractional ... ok
test report::tests::html_page_keeps_view_state_in_the_url ... ok
test report::tests::incomplete_flags_pool_not_finished ... ok
test report::tests::medals_rank_family_model_and_tp_tiers ... ok
test report::tests::parse_bench_json_reads_scores_block ... ok
test report::tests::parse_score_handles_both_header_styles ... ok
test report::tests::parse_score_ignores_transcript_noise ... ok
test report::tests::parse_score_reads_log_summary_box ... ok
test report::tests::load_report_resolves_and_rejects_traversal ... ok
depth
test bench::tests::run_tool_append_keeps_prior_output ... ok
test report::tests::slash_pair_tolerates_bold_and_spaces ... ok
test report::ui_tests::clean_log_collapses_spinner_soup_and_dups ... ok
test report::ui_tests::html_page_at_rewrites_own_paths ... ok
test report::ui_tests::html_page_embeds_data_escaped ... ok
test report::ui_tests::markdown_lists_rows_and_scores ... ok
test report::ui_tests::page_has_bell_and_notif_panel ... ok
test report::tests::report_keys_fall_back_outside_en ... ok
test report::ui_tests::page_renders_stale_state ... ok
test report::ui_tests::page_notif_diff_js ... ok
test runlock::tests::archived_flag_flips_round_trip ... ok
test runlock::tests::history_is_capped_but_total_grows ... ok
test runlock::tests::open_runs_lists_only_open_ledgers ... ok
test runlock::tests::start_over_stale_run_closes_it ... ok
test runlock::tests::start_stop_roundtrip_and_counts ... ok
test ssh::tests::shq_escapes_interior_quote ... ok
test ssh::tests::shq_passes_through_already_quoted_token ... ok
test ssh::tests::shq_quotes_injectable_value_whole ... ok
test ssh::tests::shq_quotes_plain_word ... ok
test ssh::tests::shq_quotes_unbalanced_leading_quote ... ok
test staging::mirror::tests::command_assembles_all_flags ... ok
test staging::mirror::tests::command_minimal_no_force_no_skip ... ok
test staging::mirror::tests::script_path_is_id_scoped ... ok
test staging::rows::tests::command_assembles_args ... ok
test staging::rows::tests::command_defaults_layers_no_minfree ... ok
test staging::rows::tests::command_sparse_copy_flag ... ok
test staging::rows::tests::hf_placeholder_in_src ... ok
test staging::tests::hub_mirror_dst_tilde_and_detection ... ok
test staging::tests::sparse_rows_dry_run_dispatches ... ok
test staging::tests::stage_mode_parse ... ok
test stamp::tests::civil_date_known_values ... ok
test stamp::tests::fmt_ts_renders_date_and_minute ... ok
test stamp::tests::stamp_format ... ok
test tmux::commands::tests::entrypoint_fallback_when_empty ... ok
test tmux::commands::tests::entrypoint_used_when_present ... ok
test tmux::commands::tests::sglang_entrypoint_fallback ... ok
test tmux::commands::tests::single_node_no_distributed_flags ... ok
test tmux::commands::tests::venv_prefix_absent_when_unset ... ok
test tmux::commands::tests::venv_prefix_prepended ... ok
test bench::tests::pump_keeps_pipe_open_until_child_exits ... ok
test tmux::discover::tests::list_recipe_dirs_finds_directories ... ok
test validate::tests::archived_lane_downgrades_errors ... ok
test validate::tests::good_recipe_is_clean ... ok
test tmux::discover::tests::find_tmux_recipes_finds_tmux_deployed ... ok
test validate::tests::rules_fire_on_bad_recipe ... ok
test validate::tests::repo_recipes_pass_the_gate ... ok
test web::api_tests::log_event_serializes_with_type_tag ... ok
test report::tests::collect_reads_recipe_reports_and_dedups ... ok
test report::tests::collect_carries_file_and_time ... ok
test report::tests::bench_triplet_groups_into_one_entry_with_json_authority ... ok
test web::api_tests::single_flight_rejects_duplicate_then_allows_after_finish ... ok
test web::events::tests::down_cluster_hides_containers_on_live_nodes ... ok
test web::events::tests::job_event_serializes_with_type_tag ... ok
test web::events::tests::snapshot_reflects_probe_state ... ok
test web::events::tests::soft_off_does_not_take_cluster_down ... ok
test web::events::tests::switch_off_takes_cluster_down ... ok
test web::logs::tests::attach_is_idempotent_and_bounded ... ok
test web::logs::tests::attach_replaces_finished_stream_for_same_key ... ok
test web::logs::tests::attach_reuses_live_stream_for_same_key ... ok
test web::logs::tests::history_last_returns_oldest_first ... ok
test web::logs::tests::push_line_trims_to_capacity_keeping_newest ... ok
test web::logs::tests::stream_cmd_carries_recipe_rank_and_tail ... ok
test web::ssh_stream::tests::splits_complete_lines_and_buffers_partials ... ok
test web::ssh_stream::tests::strips_cr_and_handles_blank_lines ... ok
test web::api_tests::log_attach_get_detach_roundtrip ... ok
test web::tests::bus_delivers_state_events ... ok
test web::tests::jobs_persist_and_mark_interrupted ... ok
test web::api_tests::raw_endpoints_serve_config_recipe_effective ... ok
test web::tests::snapshot_serializes_with_type_tag ... ok
test report::provenance::tests::manifest_parses_and_drifts ... ok
test web::tests::stale_from_snapshot_picks_unreachable_nodes ... ok
test web::api_tests::serve_endpoint_registers_job_and_rejects_concurrent ... ok
test web::api_tests::recipes_endpoint_lists_repo_recipes ... ok
test web::tests::api_state_reports_unreachable_node ... ok
test web::topo::tests::derive_topo_fabric_dac_lan ... ok
test web::tests::recipes_panel_is_mounted_under_prefix ... ok
test web::ui_js_tests::ui_js_ansi_log_rendering ... ok
test web::ui_js_tests::ui_js_running_jobs_grouping ... ok
test web::ui_js_tests::ui_js_serve_body_logic ... ok
test web::tests::sse_replays_cached_snapshot_on_connect ... ok
test web::ui_js_tests::ui_js_sse_recovery_logic ... ok
test web::ui_js_tests::ui_js_syntax_and_snapshot_logic ... ok
test report::ui_tests::report_playwright_checks ... ok
test report::tests::refresher_publishes_on_change_only ... ok
test web::tests::token_gate_covers_recipes_panel ... ok
test web::tests::token_gate_enforces_when_set ... ok
test web::tests::ui_playwright_checks ... ok

test result: ok. 344 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 51.70s

All gates passed. gates every recipe change.
- Archive 22 superseded lanes (sm121 report --archive): the r12/r17abi/r18
  family (dead build contexts), r27/r28/r31 (superseded by r35/r39/karmic),
  and the 2 MiMo RL lanes (operator retired RL 2026-09-29). test-* lanes
  stay active (base sanity proof).
- Pin mimo26-flash-mopd-4x-vllm's floating eugr/spark-vllm-b12x:latest to
  the docker.io manifest digest (resolved via a node's docker creds; the
  repo is private on ghcr).
- AGENTS.md: document the validate command + gate semantics.
Alexander Korolev 2 weeks ago
parent
commit
84074f6272

+ 11 - 1
AGENTS.md

@@ -17,9 +17,19 @@ See [`DEVELOPMENT.md`](DEVELOPMENT.md) for architecture, module layout, and dev
 ## Commands
 
 ```
-serve/stop/bench/logs/health/doctor/exec/power/tmux/init-host-keys/config/web/report/runlock
+serve/stop/bench/logs/health/doctor/exec/power/tmux/init-host-keys/config/web/report/runlock/validate
 ```
 
+`validate` — offline recipe-contract gate (no cluster, no SSH): checks every
+`recipes/<name>/recipe.toml` against the launcher contract — `docker_image_dir`
+must exist under `docker-images/`, `port` must be the cluster default 30000,
+no floating `:latest` on engine images (`alpine` test images exempt), models
+must float (no `@revision`), `required_nodes` within 1..=8. Active recipes:
+violations are **errors** (exit 1). Archived lanes are grandfathered — their
+errors downgrade to **warnings** (advisory, always exit 0). Also runs as a
+`cargo test` over the repo's own `recipes/`, so `make check` fails if a new
+recipe breaks the contract. Rules live in `src/validate.rs`.
+
 `serve` records the launch in the recipe's run ledger (`recipes/<name>/.lock`,
 gitignored) and then **waits for the engine to actually come up** — polls
 `GET /v1/models` on the head node up to 30 min (`--no-wait` skips); a run that

+ 1 - 0
recipes/glm53-flash-intel-w4a16/recipe.toml

@@ -102,6 +102,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r17abi/recipe.toml

@@ -157,6 +157,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r18-2x-nvfp4kv-spark/recipe.toml

@@ -158,6 +158,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r18-2x-nvfp4kv/recipe.toml

@@ -158,6 +158,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r18-4x-dcp4/recipe.toml

@@ -158,6 +158,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r18-4x-nvfp4kv-spark/recipe.toml

@@ -162,6 +162,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r18-4x-nvfp4kv/recipe.toml

@@ -157,6 +157,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r18-4x/recipe.toml

@@ -156,6 +156,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r18-dflash2/recipe.toml

@@ -164,6 +164,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-nvfp4-r18/recipe.toml

@@ -157,6 +157,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-r18-2x-nvfp4kv-spark-eager/recipe.toml

@@ -160,6 +160,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-r27-4x-dcp4/recipe.toml

@@ -172,6 +172,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-r27-spark/recipe.toml

@@ -165,6 +165,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-r28-4x-dcp4-b12xload/recipe.toml

@@ -194,6 +194,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-r28-4x-dcp4-split2048/recipe.toml

@@ -195,6 +195,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 VLLM_GLM53_SPLIT_TARGET_BLOCK_SIZE = "2048"

+ 1 - 0
recipes/glm53-flash-r28-4x-dcp4/recipe.toml

@@ -194,6 +194,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-r28-dflash2-2x/recipe.toml

@@ -107,6 +107,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-r28-spark/recipe.toml

@@ -166,6 +166,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 HF_HUB_OFFLINE = "1"

+ 1 - 0
recipes/glm53-flash-r31-2x/recipe.toml

@@ -169,6 +169,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 

+ 1 - 0
recipes/glm53-flash-r31-4x-dcp4/recipe.toml

@@ -197,6 +197,7 @@ args = [
     "--enable-auto-tool-choice",
     "--reasoning-parser", "glm45",
 ]
+archived = true
 
 [env]
 

+ 4 - 1
recipes/mimo26-flash-mopd-4x-vllm/recipe.toml

@@ -43,7 +43,10 @@
 
 engine = "vllm"
 name = "MiMo-V2.6-Flash-MOPD (vLLM B12X, 4x Spark, DFlash-7, b12x-nightly)"
-docker_image = "eugr/spark-vllm-b12x:latest"
+# Pinned 2026-10-03 (was floating :latest — validator gate): the docker.io
+# manifest digest resolved via a node's docker creds (ghcr is private;
+# this image is published on docker.io). Re-resolve if the nightly moves.
+docker_image = "docker.io/eugr/spark-vllm-b12x@sha256:ecf04ea976f41053046f3b7db82b5424670b51929767e00bfa7a8eb499e3d6e5"
 entrypoint = ["vllm", "serve"]
 model = "XiaomiMiMo/MiMo-V2.6-Flash-MOPD"
 parent_model = "XiaomiMiMo/MiMo-V2.6-Flash-MOPD"

+ 1 - 0
recipes/mimo26-flash-rl-2x/recipe.toml

@@ -79,6 +79,7 @@ args = [
     "--enable-cache-report",
     "--watchdog-timeout", "1800",
 ]
+archived = true
 
 # DFlash draft weights: a subfolder of the main model repo, resolved per node
 # (table header — all bare keys above).

+ 1 - 0
recipes/mimo26-pro-rl-8x/recipe.toml

@@ -78,6 +78,7 @@ args = [
     "--enable-cache-report",
     "--watchdog-timeout", "1800",
 ]
+archived = true
 
 # DFlash draft weights: a subfolder of the main model repo, resolved per node
 # (later in the file — bare keys must come before table headers).

+ 7 - 0
src/cli.rs

@@ -336,6 +336,13 @@ pub enum Commands {
         #[command(subcommand)]
         action: PowerAction,
     },
+
+    /// Validate recipes against the launcher contract (build dirs exist,
+    /// port convention, no floating :latest, floating model revisions,
+    /// node counts). Errors fail the run (exit 1); archived lanes are
+    /// grandfathered (their errors downgrade to warnings). Also runs as a
+    /// `cargo test` gate over the repo's own `recipes/`.
+    Validate,
 }
 
 #[cfg(test)]

+ 23 - 0
src/main.rs

@@ -44,6 +44,7 @@ mod ssh;
 mod staging;
 mod stamp;
 mod tmux;
+mod validate;
 mod web;
 use crate::cli::{Cli, Commands};
 use crate::config::{Config, DeployTarget, load_recipe};
@@ -286,6 +287,28 @@ async fn main() -> Result<()> {
             .await?;
         }
         Commands::Runlock(cmd) => crate::runlock::run(&config, &cmd)?,
+        Commands::Validate => {
+            let v = crate::validate::validate_all(&config);
+            let errs = v
+                .iter()
+                .filter(|x| x.level == crate::validate::Level::Error)
+                .count();
+            if v.is_empty() {
+                println!("validate: all recipes pass");
+            } else {
+                for x in &v {
+                    let tag = match x.level {
+                        crate::validate::Level::Error => "ERROR",
+                        crate::validate::Level::Warning => "WARN ",
+                    };
+                    println!("{tag} {} [{}]: {}", x.recipe, x.rule, x.message);
+                }
+                eprintln!("validate: {errs} error(s), {} warning(s)", v.len() - errs);
+                if errs > 0 {
+                    std::process::exit(1);
+                }
+            }
+        }
     }
 
     Ok(())

+ 259 - 0
src/validate.rs

@@ -0,0 +1,259 @@
+//! Recipe contract validation — `sm121 validate` + the `make check` gate
+//! (the repo-self test at the bottom of this file runs the validator over
+//! `recipes/` on every `cargo test`, so new recipes cannot break the
+//! contract silently).
+//!
+//! Rules (errors for active recipes; `archived = true` lanes are
+//! grandfathered — their errors downgrade to warnings):
+//!
+//!   image-dir-missing  `docker_image_dir` must exist under `docker-images/`
+//!   port-convention    `port` must be the cluster default 30000
+//!   floating-tag       `docker_image` must not use a floating `:latest`
+//!                      (engine images; `alpine` test images are exempt)
+//!   pinned-revision    `model` must float (`org/name`), no `@revision`
+//!   node-count         `required_nodes` within 1..=8 (fleet size)
+//!
+//! Warnings (always advisory):
+//!
+//!   parse              recipe.toml failed to parse
+//!   no-parent-model    `model` set without `parent_model` (report tree)
+//!   multi-node-tp1     `required_nodes > 1` with TP=1/PP=1 (replicated)
+
+use std::path::Path;
+
+use crate::config::{Config, Recipe, recipe_dirs};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum Level {
+    Error,
+    Warning,
+}
+
+#[derive(Debug)]
+pub struct Violation {
+    pub recipe: String,
+    pub level: Level,
+    pub rule: &'static str,
+    pub message: String,
+}
+
+/// The largest launch set is the 8-node switched cluster.
+const MAX_NODES: u32 = 8;
+
+fn check_recipe(name: &str, r: &Recipe, images_dir: &Path, out: &mut Vec<Violation>) {
+    // Build context must exist (a recipe promising an on-demand build from
+    // a deleted tree is a dead lane).
+    if let Some(dir) = &r.docker_image_dir
+        && !images_dir.join(dir).is_dir()
+    {
+        out.push(Violation {
+            recipe: name.to_string(),
+            level: Level::Error,
+            rule: "image-dir-missing",
+            message: format!("docker_image_dir '{dir}' not found under docker-images/"),
+        });
+    }
+    // Port convention: every recipe serves on the cluster default port.
+    if r.port != 30000 {
+        out.push(Violation {
+            recipe: name.to_string(),
+            level: Level::Error,
+            rule: "port-convention",
+            message: format!("port {} — the cluster default is 30000", r.port),
+        });
+    }
+    // No floating engine tags (reproducibility; pin a digest).
+    if r.docker_image.ends_with(":latest") && !r.docker_image.starts_with("alpine") {
+        out.push(Violation {
+            recipe: name.to_string(),
+            level: Level::Error,
+            rule: "floating-tag",
+            message: format!(
+                "docker_image '{}' uses a floating :latest tag — pin a digest",
+                r.docker_image
+            ),
+        });
+    }
+    // Models float; a revision goes in a comment + the bench manifest.
+    if r.model.contains('@') && !r.model.starts_with('/') {
+        out.push(Violation {
+            recipe: name.to_string(),
+            level: Level::Error,
+            rule: "pinned-revision",
+            message: format!(
+                "model '{}' pins a revision — float it (org/name), record the rev in a comment",
+                r.model
+            ),
+        });
+    }
+    if let Some(n) = r.required_nodes
+        && (n == 0 || n > MAX_NODES)
+    {
+        out.push(Violation {
+            recipe: name.to_string(),
+            level: Level::Error,
+            rule: "node-count",
+            message: format!("required_nodes {n} outside 1..={MAX_NODES}"),
+        });
+    }
+    // Advisory (both active and archived):
+    if !r.model.is_empty() && !r.model.starts_with('/') && r.parent_model.is_none() {
+        out.push(Violation {
+            recipe: name.to_string(),
+            level: Level::Warning,
+            rule: "no-parent-model",
+            message: "model set without parent_model (report tree grouping)".to_string(),
+        });
+    }
+    let multi = r.required_nodes.is_some_and(|n| n > 1);
+    if multi && r.tensor_parallel_size == 1 && r.pipeline_parallel_size.unwrap_or(1) == 1 {
+        out.push(Violation {
+            recipe: name.to_string(),
+            level: Level::Warning,
+            rule: "multi-node-tp1",
+            message: "required_nodes > 1 with TP=1/PP=1 (model replicated across nodes)"
+                .to_string(),
+        });
+    }
+}
+
+/// Validate every `recipes/<name>/recipe.toml` under `recipes_dir`.
+pub fn validate_dir(recipes_dir: &Path, images_dir: &Path) -> Vec<Violation> {
+    let mut out: Vec<Violation> = Vec::new();
+    let Ok(entries) = std::fs::read_dir(recipes_dir) else {
+        return out;
+    };
+    for entry in entries.flatten() {
+        let path = entry.path();
+        if !path.is_dir() {
+            continue;
+        }
+        let Some(name) = entry.file_name().to_str().map(String::from) else {
+            continue;
+        };
+        let toml_path = path.join("recipe.toml");
+        let Ok(toml_str) = std::fs::read_to_string(&toml_path) else {
+            continue;
+        };
+        let r = match toml::from_str::<Recipe>(&toml_str) {
+            Ok(r) => r,
+            Err(e) => {
+                out.push(Violation {
+                    recipe: name,
+                    level: Level::Error,
+                    rule: "parse",
+                    message: format!("recipe.toml: {e}"),
+                });
+                continue;
+            }
+        };
+        let mut v = Vec::new();
+        check_recipe(&name, &r, images_dir, &mut v);
+        if r.archived {
+            for x in v.iter_mut() {
+                if x.level == Level::Error {
+                    x.level = Level::Warning;
+                }
+            }
+        }
+        out.extend(v);
+    }
+    out
+}
+
+/// Validate every recipe source from the config (`./recipes` +
+/// `recipe_sources`). Build dirs are checked against `<cwd>/docker-images`.
+pub fn validate_all(config: &Config) -> Vec<Violation> {
+    let images_dir = std::env::current_dir()
+        .map(|cwd| cwd.join("docker-images"))
+        .unwrap_or_default();
+    let mut out: Vec<Violation> = Vec::new();
+    for dir in recipe_dirs(config) {
+        out.extend(validate_dir(&dir, &images_dir));
+    }
+    out
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    /// The make-check gate: the repo's own ACTIVE recipes must pass.
+    /// Archived lanes are grandfathered (warnings only).
+    #[test]
+    fn repo_recipes_pass_the_gate() {
+        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
+        let v = validate_dir(&root.join("recipes"), &root.join("docker-images"));
+        let errs: Vec<&Violation> = v.iter().filter(|x| x.level == Level::Error).collect();
+        assert!(
+            errs.is_empty(),
+            "recipe gate failed:\n{}",
+            errs.iter()
+                .map(|e| format!("  {} [{}]: {}", e.recipe, e.rule, e.message))
+                .collect::<Vec<_>>()
+                .join("\n")
+        );
+    }
+
+    #[test]
+    fn rules_fire_on_bad_recipe() {
+        let toml = r#"
+name = "bad"
+docker_image = "eugr/x:latest"
+docker_image_dir = "no-such-dir"
+model = "org/m@deadbeef"
+port = 8000
+required_nodes = 9
+"#;
+        let r: Recipe = toml::from_str(toml).expect("test recipe parses");
+        let mut out = Vec::new();
+        check_recipe("bad", &r, Path::new("/nonexistent/docker-images"), &mut out);
+        let rules: Vec<&str> = out.iter().map(|v| v.rule).collect();
+        for want in [
+            "floating-tag",
+            "image-dir-missing",
+            "pinned-revision",
+            "port-convention",
+            "node-count",
+            "no-parent-model",
+            "multi-node-tp1",
+        ] {
+            assert!(rules.contains(&want), "missing rule {want}; got {rules:?}");
+        }
+    }
+
+    #[test]
+    fn good_recipe_is_clean() {
+        let toml = r#"
+name = "ok"
+docker_image = "img:tag"
+model = "org/m"
+parent_model = "org/base"
+required_nodes = 2
+tensor_parallel_size = 2
+"#;
+        let r: Recipe = toml::from_str(toml).expect("test recipe parses");
+        let mut out = Vec::new();
+        check_recipe("ok", &r, Path::new("/nonexistent/docker-images"), &mut out);
+        assert!(out.is_empty(), "unexpected violations: {out:?}");
+    }
+
+    #[test]
+    fn archived_lane_downgrades_errors() {
+        let toml = r#"
+name = "old"
+archived = true
+docker_image = "eugr/x:latest"
+"#;
+        let r: Recipe = toml::from_str(toml).expect("test recipe parses");
+        let mut out = Vec::new();
+        check_recipe("old", &r, Path::new("/nonexistent/docker-images"), &mut out);
+        // simulate the archived downgrade from validate_dir
+        for x in out.iter_mut() {
+            if x.level == Level::Error {
+                x.level = Level::Warning;
+            }
+        }
+        assert!(out.iter().all(|v| v.level == Level::Warning), "{out:?}");
+    }
+}