-
Notifications
You must be signed in to change notification settings - Fork 16
fix(site): map Prism submissions to live v2.1 identity #178
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,8 +22,8 @@ use site_data::map::{ | |
| }; | ||
| use site_prism::{ | ||
| enrich_leaderboard_row_from_detail_with_zone, enrich_submission_from_detail_with_zone, | ||
| infer_recipe_era_with_live, map_benchmarks, payload_is_v21_contest, pin_id_from_payload, | ||
| prism_reference_baselines, prism_submission_detail_with_zone, | ||
| fill_v21_run_from_live, infer_recipe_era_with_live, map_benchmarks, payload_is_v21_contest, | ||
| pin_id_from_payload, prism_reference_baselines, prism_submission_detail_with_zone, | ||
| }; | ||
| use site_types::coding_arena; | ||
| use site_types::page_slice; | ||
|
|
@@ -605,40 +605,46 @@ async fn get_submissions( | |
| .trim() | ||
| .to_ascii_lowercase(); | ||
| let champions_only = matches!(scope.as_str(), "champions" | "champion"); | ||
| let mut items: Vec<_> = rows | ||
| let mut paired: Vec<(crate::Submission, &Value)> = rows | ||
| .iter() | ||
| .filter(|r| !champions_only || is_prism_champion_submission(r)) | ||
| .filter_map(prism_submission) | ||
| .filter_map(|r| prism_submission(r).map(|s| (s, r))) | ||
| .collect(); | ||
| // Prefer in-flight + recent rows for detail fan-out (era / benches). | ||
| let mut ids: Vec<String> = items.iter().map(|s| s.id.clone()).collect(); | ||
| let mut ids: Vec<String> = paired.iter().map(|(s, _)| s.id.clone()).collect(); | ||
| ids.truncate(PRISM_CHAMPION_DETAIL_FANOUT); | ||
| let details = fetch_prism_details(&st, &ids).await; | ||
| let live_recipe = fetch_prism_recipe(&st) | ||
| .await | ||
| let recipe_json = fetch_prism_recipe(&st).await; | ||
| let live = recipe_json | ||
| .as_ref() | ||
| .and_then(|r| r.get("version")) | ||
| .and_then(Value::as_str) | ||
| .map(str::to_owned); | ||
| let live = live_recipe.as_deref(); | ||
| for item in &mut items { | ||
| .and_then(Value::as_str); | ||
| for (item, raw) in &mut paired { | ||
| if let Some(fan) = details.get(&item.id) { | ||
| enrich_submission_from_detail_with_zone(item, &fan.detail, fan.zone_a.as_ref()); | ||
| item.recipe_era = Some(infer_recipe_era_with_live(&fan.detail, live)); | ||
| if item.recipe_era == Some(RecipeEra::V21) { | ||
| let harvested = infer_recipe_era_with_live(&fan.detail, None); | ||
| let era = infer_recipe_era_with_live(&fan.detail, live); | ||
| item.recipe_era = Some(era); | ||
| // Live-recipe fallback only — keep harvest/gate eligibility intact. | ||
| if era == RecipeEra::V21 && harvested != RecipeEra::V21 { | ||
| item.run.weight_eligible = Some(true); | ||
| } | ||
| } else if item.recipe_era.is_none() { | ||
| // Pre-2.0 / unknown → legacy so era tabs are not empty. | ||
| item.recipe_era = Some(RecipeEra::Legacy); | ||
| fill_v21_run_from_live(&mut item.run, recipe_json.as_ref(), era); | ||
| } else { | ||
| let era = infer_recipe_era_with_live(raw, live); | ||
| item.recipe_era = Some(era); | ||
| item.run.weight_eligible = Some(era == RecipeEra::V21); | ||
| fill_v21_run_from_live(&mut item.run, recipe_json.as_ref(), era); | ||
|
Comment on lines
623
to
+637
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Apply live-recipe enrichment to direct detail responses. This flow labels an unlabeled in-flight row as v2.1 and fills its run metadata. Extract the live-enrichment step and use it in both endpoints. Add a direct-detail test for 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
| let mut items: Vec<_> = paired.into_iter().map(|(s, _)| s).collect(); | ||
| decorate_submissions(&st, &mut items); | ||
| if let Some(st_f) = status_filter { | ||
| items.retain(|s| match st_f { | ||
| "scored" => s.status == crate::SubmissionStatus::Scored, | ||
| "pending" => s.status == crate::SubmissionStatus::Pending, | ||
| "failed" => s.status == crate::SubmissionStatus::Failed, | ||
| "queued" | "running" | "terminated" => s.stage.eq_ignore_ascii_case(st_f), | ||
| _ => true, | ||
| }); | ||
| } | ||
|
|
@@ -1447,6 +1453,7 @@ mod tests { | |
| assert_eq!(v["items"][0]["recipeEra"], "v21"); | ||
| assert_eq!(v["items"][0]["pinId"], "automodel@v0.5.0"); | ||
| assert_eq!(v["items"][0]["weightEligible"], true); | ||
| assert_eq!(v["items"][0]["gatesComplete"], true); | ||
| assert_eq!(v["items"][0]["competitionId"], "prism-v2.1"); | ||
| assert_eq!(v["items"][0]["tokens"], 2048.0); | ||
| assert_eq!(v["items"][0]["tokensPerSec"], 1200.0); | ||
|
|
@@ -1458,7 +1465,24 @@ mod tests { | |
| assert_eq!(v["items"][1]["weightEligible"], false); | ||
| assert_eq!(v["items"][2]["id"], "sub-running"); | ||
| assert_eq!(v["items"][2]["status"], "pending"); | ||
| assert_eq!(v["items"][2]["recipeEra"], "legacy"); | ||
| assert_eq!(v["items"][2]["stage"], "running"); | ||
| assert_eq!( | ||
| v["items"][2]["recipeEra"], "v21", | ||
| "live recipe 2.1.x labels unlabeled in-flight as v2.1: {v}" | ||
| ); | ||
| assert_eq!(v["items"][2]["weightEligible"], true); | ||
| assert_eq!(v["items"][2]["competitionId"], "prism-v2.1"); | ||
| assert_eq!(v["items"][2]["scoringGeneration"], 21); | ||
| assert_eq!(v["items"][2]["recipeVersion"], "2.1.0"); | ||
|
|
||
| let (s, v) = call( | ||
| app.clone(), | ||
| "/v1/site/arenas/prism/submissions?status=running", | ||
| ) | ||
| .await; | ||
| assert_eq!(s, StatusCode::OK, "{v}"); | ||
| assert_eq!(v["total"], 1, "status=running filters by stage: {v}"); | ||
| assert_eq!(v["items"][0]["id"], "sub-running"); | ||
|
|
||
| // scope=champions keeps Score>0 gallery; default scope=all includes in-flight. | ||
| let (s, v) = call( | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -113,7 +113,8 @@ pub fn infer_recipe_era(payload: &Value) -> RecipeEra { | |
| } | ||
|
|
||
| /// Same as [`infer_recipe_era`], but a live recipe `2.1.x` lets pin-only | ||
| /// in-flight rows (no harvest metrics yet) count as v2.1. | ||
| /// or unlabeled in-flight rows (no harvest metrics yet) count as v2.1. | ||
| /// Explicit `1.x` / `2.0.x` stay archived even while 2.1 is live. | ||
| #[must_use] | ||
| pub fn infer_recipe_era_with_live(payload: &Value, live_recipe: Option<&str>) -> RecipeEra { | ||
| if payload_is_v21_contest(payload) { | ||
|
|
@@ -123,6 +124,9 @@ pub fn infer_recipe_era_with_live(payload: &Value, live_recipe: Option<&str>) -> | |
| if recipe_is_major_minor(sub, 2, 0) { | ||
| return RecipeEra::Automodel; | ||
| } | ||
| if recipe_major(sub).is_some_and(|m| m < 2) { | ||
| return RecipeEra::Legacy; | ||
| } | ||
| if pin_id_from_payload(payload).is_some() { | ||
| if live_recipe.is_some_and(recipe_semver_is_v21) { | ||
| return RecipeEra::V21; | ||
|
|
@@ -142,9 +146,37 @@ pub fn infer_recipe_era_with_live(payload: &Value, live_recipe: Option<&str>) -> | |
| } | ||
| return RecipeEra::Automodel; | ||
| } | ||
| if live_recipe.is_some_and(recipe_semver_is_v21) { | ||
| return RecipeEra::V21; | ||
| } | ||
| RecipeEra::Legacy | ||
| } | ||
|
|
||
| /// Copy live contest identity onto a v2.1 row that has no harvest metrics yet. | ||
| pub fn fill_v21_run_from_live(run: &mut PrismRunStats, live: Option<&Value>, era: RecipeEra) { | ||
| if era != RecipeEra::V21 { | ||
| return; | ||
| } | ||
| if run.competition_id.is_none() { | ||
| run.competition_id = live | ||
| .and_then(|r| r.get("competition_id")) | ||
| .and_then(Value::as_str) | ||
| .map(str::to_owned); | ||
| } | ||
| if run.scoring_generation.is_none() { | ||
| run.scoring_generation = live | ||
| .and_then(|r| r.get("scoring_generation")) | ||
| .and_then(Value::as_u64) | ||
| .and_then(|n| u16::try_from(n).ok()); | ||
| } | ||
| if run.recipe_version.is_none() { | ||
| run.recipe_version = live | ||
| .and_then(|r| r.get("version")) | ||
| .and_then(Value::as_str) | ||
| .map(str::to_owned); | ||
| } | ||
| } | ||
|
|
||
| /// True when the payload is the live Prism v2.1 contest (fail-closed). | ||
| #[must_use] | ||
| pub fn payload_is_v21_contest(payload: &Value) -> bool { | ||
|
|
@@ -463,27 +495,41 @@ fn map_run_stats(detail: &Value, era: RecipeEra) -> PrismRunStats { | |
| .and_then(Value::as_str) | ||
| }) | ||
| }) | ||
| .or_else(|| root.get("competition_id").and_then(Value::as_str)) | ||
| .map(str::to_owned); | ||
| let scoring_generation = metrics.and_then(|m| { | ||
| m.get("scoring_generation") | ||
| .and_then(Value::as_u64) | ||
| .or_else(|| { | ||
| m.get("scoring_generation") | ||
| .and_then(Value::as_str) | ||
| .and_then(|s| s.trim().parse::<u64>().ok()) | ||
| }) | ||
| .or_else(|| { | ||
| m.pointer("/pod_manifest/scoring_generation") | ||
| .and_then(Value::as_u64) | ||
| }) | ||
| .and_then(|n| u16::try_from(n).ok()) | ||
| }); | ||
| let scoring_generation = metrics | ||
| .and_then(|m| { | ||
| m.get("scoring_generation") | ||
| .and_then(Value::as_u64) | ||
| .or_else(|| { | ||
| m.get("scoring_generation") | ||
| .and_then(Value::as_str) | ||
| .and_then(|s| s.trim().parse::<u64>().ok()) | ||
| }) | ||
| .or_else(|| { | ||
| m.pointer("/pod_manifest/scoring_generation") | ||
| .and_then(Value::as_u64) | ||
| }) | ||
| .and_then(|n| u16::try_from(n).ok()) | ||
| }) | ||
| .or_else(|| { | ||
| root.get("scoring_generation") | ||
| .and_then(Value::as_u64) | ||
| .and_then(|n| u16::try_from(n).ok()) | ||
| }); | ||
|
Comment on lines
+500
to
+519
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Parse string scoring generations in every fallback location. Line 510 accepts only a numeric manifest value. Line 516 accepts only a numeric root value. A valid string value such as Apply the same numeric-or-string conversion to manifest and root fields. Add regression tests for both forms. 🤖 Prompt for AI Agents |
||
| let eval = root.get("eval").filter(|e| !e.is_null()); | ||
| let eval_ineligible = eval | ||
| .and_then(|e| e.get("status")) | ||
| .and_then(Value::as_str) | ||
| .is_some_and(|s| s.eq_ignore_ascii_case("ineligible")); | ||
| let gates_complete = eval | ||
| .and_then(|e| e.pointer("/gates/complete")) | ||
| .and_then(Value::as_bool); | ||
| PrismRunStats { | ||
| competition_id, | ||
| scoring_generation, | ||
| recipe_version: recipe, | ||
| weight_eligible: Some(era == RecipeEra::V21), | ||
| weight_eligible: Some(era == RecipeEra::V21 && !eval_ineligible), | ||
| bits_per_byte: metrics.and_then(|m| { | ||
| metric_f64( | ||
| m, | ||
|
|
@@ -521,9 +567,7 @@ fn map_run_stats(detail: &Value, era: RecipeEra) -> PrismRunStats { | |
| .and_then(|m| m.get("gpu_type")) | ||
| .and_then(Value::as_str) | ||
| .map(str::to_owned), | ||
| gates_complete: eval | ||
| .and_then(|e| e.pointer("/gates/complete")) | ||
| .and_then(Value::as_bool), | ||
| gates_complete, | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -670,6 +714,33 @@ mod tests { | |
| infer_recipe_era_with_live(&pin, Some("2.1.0")), | ||
| RecipeEra::V21 | ||
| ); | ||
| let inflight = json!({"id": "run", "status": "running", "label": "inflight"}); | ||
| assert_eq!(infer_recipe_era(&inflight), RecipeEra::Legacy); | ||
| assert_eq!( | ||
| infer_recipe_era_with_live(&inflight, Some("2.1.0")), | ||
| RecipeEra::V21 | ||
| ); | ||
| assert_eq!( | ||
| infer_recipe_era_with_live(&auto, Some("2.1.0")), | ||
| RecipeEra::Automodel | ||
| ); | ||
| assert_eq!( | ||
| infer_recipe_era_with_live(&legacy, Some("2.1.0")), | ||
| RecipeEra::Legacy | ||
| ); | ||
| let mut run = PrismRunStats::default(); | ||
| fill_v21_run_from_live( | ||
| &mut run, | ||
| Some(&json!({ | ||
| "version": "2.1.0", | ||
| "competition_id": "prism-v2.1", | ||
| "scoring_generation": 21 | ||
| })), | ||
| RecipeEra::V21, | ||
| ); | ||
| assert_eq!(run.competition_id.as_deref(), Some("prism-v2.1")); | ||
| assert_eq!(run.scoring_generation, Some(21)); | ||
| assert_eq!(run.recipe_version.as_deref(), Some("2.1.0")); | ||
| assert_eq!( | ||
| pin_id_from_payload(&pin).as_deref(), | ||
| Some("automodel@v0.5.0") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Do not overwrite an ineligible evaluation.
enrich_submission_from_detail_with_zonesetsweight_eligibletofalsewheneval.statusis"ineligible". If that detail has no harvested v2.1 marker, Lines 625-630 then replace that value withtruefrom the live-recipe fallback.Preserve
falsewhen the evaluation is ineligible. Add a detail-fanout regression case witheval.status: "ineligible"and no harvest recipe metadata.🤖 Prompt for AI Agents