1use std::collections::{BTreeMap, BTreeSet};
52use std::fmt;
53
54use serde::{Deserialize, Serialize};
55
56use crate::composite::{parse_declared_field, AgentSubmission, MandateDeclarations};
57
58#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
64pub struct RunKey {
65 pub window: String,
66 pub seed: u64,
67}
68
69impl fmt::Display for RunKey {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 write!(f, "(window `{}`, seed {})", self.window, self.seed)
72 }
73}
74
75#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
82pub struct RunIdentity {
83 #[serde(flatten)]
84 pub key: RunKey,
85 #[serde(default)]
86 pub periods: Vec<String>,
87}
88
89#[derive(Clone, Debug, Deserialize)]
95struct RunKeysDoc {
96 agent_id: String,
97 run_keys: Option<Vec<RunIdentity>>,
98}
99
100#[derive(Clone, Debug, PartialEq, Eq)]
104pub enum RunIdentityError {
105 InvalidField(String),
107 EmptyField,
109 UnkeyedSubmission { agent_id: String },
111 KeyCountMismatch {
113 agent_id: String,
114 runs: usize,
115 keys: usize,
116 },
117 InvalidKey {
119 agent_id: String,
120 index: usize,
121 reason: String,
122 },
123 DuplicateKey { agent_id: String, key: RunKey },
125 MissingCell { agent_id: String, key: RunKey },
127 IncompleteGrid {
129 windows: usize,
130 seeds: usize,
131 submitted: usize,
132 first_missing: RunKey,
133 },
134 PeriodCountMismatch {
136 agent_id: String,
137 key: RunKey,
138 periods: usize,
139 returns: usize,
140 },
141 DuplicatePeriod {
145 agent_id: String,
146 key: RunKey,
147 period: String,
148 first: usize,
149 second: usize,
150 },
151 PeriodInTwoWindows(Box<PeriodOverlap>),
155 PeriodMismatch {
157 key: RunKey,
158 agent_id: String,
159 other_agent_id: String,
160 index: usize,
161 },
162 WindowOrderMismatch {
165 agent_id: String,
166 other_agent_id: String,
167 windows: Vec<String>,
168 other_windows: Vec<String>,
169 },
170}
171
172#[derive(Clone, Debug, PartialEq, Eq)]
176pub struct PeriodOverlap {
177 pub period: String,
178 pub first: RunKey,
179 pub first_agent_id: String,
180 pub second: RunKey,
181 pub second_agent_id: String,
182}
183
184impl fmt::Display for RunIdentityError {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 match self {
187 Self::InvalidField(error) => write!(f, "{error}"),
188 Self::EmptyField => write!(f, "run identity: the field contains no submissions"),
189 Self::UnkeyedSubmission { agent_id } => write!(
190 f,
191 "run identity: agent `{agent_id}` submitted an unkeyed run array; \
192 add a `run_keys` entry per run (window and seed). Runs are not \
193 aligned across agents by position"
194 ),
195 Self::KeyCountMismatch {
196 agent_id,
197 runs,
198 keys,
199 } => write!(
200 f,
201 "run identity: agent `{agent_id}` declared {keys} run key(s) for {runs} run(s)"
202 ),
203 Self::InvalidKey {
204 agent_id,
205 index,
206 reason,
207 } => write!(
208 f,
209 "run identity: agent `{agent_id}` run key {index} is invalid: {reason}"
210 ),
211 Self::DuplicateKey { agent_id, key } => write!(
212 f,
213 "run identity: agent `{agent_id}` declared {key} more than once"
214 ),
215 Self::MissingCell { agent_id, key } => write!(
216 f,
217 "run identity: agent `{agent_id}` is missing {key}, which another \
218 agent submitted. A partial field cannot be scored: restricting to \
219 shared support would rescope every other entrant's evidence"
220 ),
221 Self::IncompleteGrid {
222 windows,
223 seeds,
224 submitted,
225 first_missing,
226 } => write!(
227 f,
228 "run identity: incomplete grid, {submitted} cell(s) submitted for \
229 {windows} window(s) times {seeds} seed(s); first missing {first_missing}"
230 ),
231 Self::PeriodCountMismatch {
232 agent_id,
233 key,
234 periods,
235 returns,
236 } => write!(
237 f,
238 "run identity: agent `{agent_id}` {key} declared {periods} period \
239 identity/identities for {returns} return(s)"
240 ),
241 Self::DuplicatePeriod {
242 agent_id,
243 key,
244 period,
245 first,
246 second,
247 } => write!(
248 f,
249 "run identity: agent `{agent_id}` {key} declares period `{period}` at \
250 index {first} and again at index {second}; one period cannot contribute \
251 two returns to a run"
252 ),
253 Self::PeriodInTwoWindows(overlap) => {
254 let PeriodOverlap {
255 period,
256 first,
257 first_agent_id,
258 second,
259 second_agent_id,
260 } = overlap.as_ref();
261 write!(
262 f,
263 "run identity: period `{period}` is declared in {first} by agent \
264 `{first_agent_id}` and in {second} by agent `{second_agent_id}`; a \
265 period belongs to one window, and the pooled track would count its \
266 return twice. Seeds of one window may share periods, different \
267 windows may not"
268 )
269 }
270 Self::PeriodMismatch {
271 key,
272 agent_id,
273 other_agent_id,
274 index,
275 } => write!(
276 f,
277 "run identity: {key} has different period identities at index {index} \
278 for agents `{other_agent_id}` and `{agent_id}`"
279 ),
280 Self::WindowOrderMismatch {
281 agent_id,
282 other_agent_id,
283 windows,
284 other_windows,
285 } => write!(
286 f,
287 "run identity: agent `{other_agent_id}` lists windows {other_windows:?} \
288 and agent `{agent_id}` lists {windows:?}. The declared order is the \
289 order the pooled track concatenates them in, so the field must not \
290 leave it to the labels' spelling"
291 ),
292 }
293 }
294}
295
296impl std::error::Error for RunIdentityError {}
297
298#[derive(Clone, Debug)]
302pub struct KeyedField {
303 pub keys: Vec<RunKey>,
307 pub windows: Vec<String>,
310 pub seeds: Vec<u64>,
312 pub submissions: Vec<AgentSubmission>,
314 pub declarations: MandateDeclarations,
316}
317
318fn declared_window_order(identities: &[RunIdentity]) -> Vec<String> {
322 let mut seen: BTreeSet<&str> = BTreeSet::new();
323 let mut order = Vec::new();
324 for identity in identities {
325 if seen.insert(identity.key.window.as_str()) {
326 order.push(identity.key.window.clone());
327 }
328 }
329 order
330}
331
332fn check_window(window: &str) -> Result<(), String> {
333 if window.is_empty() {
334 return Err("window identity is empty".to_string());
335 }
336 if window.trim() != window {
337 return Err(format!(
338 "window identity `{window}` has leading or trailing whitespace, so exact \
339 comparison across producers is unsafe"
340 ));
341 }
342 if window.chars().any(char::is_control) {
343 return Err(format!(
344 "window identity `{window}` contains a control character"
345 ));
346 }
347 Ok(())
348}
349
350fn first_repeated_period(periods: &[String]) -> Option<(usize, usize)> {
354 let mut seen: BTreeMap<&str, usize> = BTreeMap::new();
355 for (second, period) in periods.iter().enumerate() {
356 if let Some(first) = seen.insert(period.as_str(), second) {
357 return Some((first, second));
358 }
359 }
360 None
361}
362
363pub fn parse_keyed_field(json: &str) -> Result<KeyedField, RunIdentityError> {
371 let (subs, declarations) =
372 parse_declared_field(json).map_err(RunIdentityError::InvalidField)?;
373 let docs: Vec<RunKeysDoc> = serde_json::from_str(json)
374 .map_err(|error| RunIdentityError::InvalidField(format!("invalid run keys: {error}")))?;
375 if subs.is_empty() {
376 return Err(RunIdentityError::EmptyField);
377 }
378
379 let mut per_agent: Vec<(usize, Vec<RunIdentity>)> = Vec::with_capacity(subs.len());
381 for (position, sub) in subs.iter().enumerate() {
382 let doc = docs
383 .iter()
384 .find(|d| d.agent_id == sub.agent_id)
385 .ok_or_else(|| RunIdentityError::UnkeyedSubmission {
386 agent_id: sub.agent_id.clone(),
387 })?;
388 let identities =
389 doc.run_keys
390 .clone()
391 .ok_or_else(|| RunIdentityError::UnkeyedSubmission {
392 agent_id: sub.agent_id.clone(),
393 })?;
394 if identities.len() != sub.runs.len() {
395 return Err(RunIdentityError::KeyCountMismatch {
396 agent_id: sub.agent_id.clone(),
397 runs: sub.runs.len(),
398 keys: identities.len(),
399 });
400 }
401 let mut seen = BTreeSet::new();
402 for (index, identity) in identities.iter().enumerate() {
403 check_window(&identity.key.window).map_err(|reason| RunIdentityError::InvalidKey {
404 agent_id: sub.agent_id.clone(),
405 index,
406 reason,
407 })?;
408 if !seen.insert(identity.key.clone()) {
409 return Err(RunIdentityError::DuplicateKey {
410 agent_id: sub.agent_id.clone(),
411 key: identity.key.clone(),
412 });
413 }
414 if !identity.periods.is_empty()
415 && identity.periods.len() != sub.runs[index].returns.len()
416 {
417 return Err(RunIdentityError::PeriodCountMismatch {
418 agent_id: sub.agent_id.clone(),
419 key: identity.key.clone(),
420 periods: identity.periods.len(),
421 returns: sub.runs[index].returns.len(),
422 });
423 }
424 if let Some((first, second)) = first_repeated_period(&identity.periods) {
425 return Err(RunIdentityError::DuplicatePeriod {
426 agent_id: sub.agent_id.clone(),
427 key: identity.key.clone(),
428 period: identity.periods[second].clone(),
429 first,
430 second,
431 });
432 }
433 }
434 per_agent.push((position, identities));
435 }
436
437 let mut union: BTreeSet<RunKey> = BTreeSet::new();
439 for (_, identities) in &per_agent {
440 union.extend(identities.iter().map(|i| i.key.clone()));
441 }
442 for (position, identities) in &per_agent {
443 let held: BTreeSet<&RunKey> = identities.iter().map(|i| &i.key).collect();
444 if let Some(missing) = union.iter().find(|key| !held.contains(key)) {
445 return Err(RunIdentityError::MissingCell {
446 agent_id: subs[*position].agent_id.clone(),
447 key: missing.clone(),
448 });
449 }
450 }
451
452 let (first_position, first_identities) = &per_agent[0];
463 let windows = declared_window_order(first_identities);
464 for (position, identities) in &per_agent {
465 let declared = declared_window_order(identities);
466 if declared != windows {
467 return Err(RunIdentityError::WindowOrderMismatch {
468 agent_id: subs[*position].agent_id.clone(),
469 other_agent_id: subs[*first_position].agent_id.clone(),
470 windows: declared,
471 other_windows: windows,
472 });
473 }
474 }
475
476 let seeds: Vec<u64> = union
479 .iter()
480 .map(|key| key.seed)
481 .collect::<BTreeSet<_>>()
482 .into_iter()
483 .collect();
484 if union.len() != windows.len() * seeds.len() {
485 let first_missing = windows
486 .iter()
487 .flat_map(|window| {
488 seeds.iter().map(move |seed| RunKey {
489 window: window.clone(),
490 seed: *seed,
491 })
492 })
493 .find(|key| !union.contains(key))
494 .expect("a short product has at least one missing cell");
495 return Err(RunIdentityError::IncompleteGrid {
496 windows: windows.len(),
497 seeds: seeds.len(),
498 submitted: union.len(),
499 first_missing,
500 });
501 }
502
503 let mut declared_periods: BTreeMap<RunKey, (String, Vec<String>)> = BTreeMap::new();
505 for (position, identities) in &per_agent {
506 let agent_id = &subs[*position].agent_id;
507 for identity in identities {
508 if identity.periods.is_empty() {
509 continue;
510 }
511 match declared_periods.get(&identity.key) {
512 None => {
513 declared_periods.insert(
514 identity.key.clone(),
515 (agent_id.clone(), identity.periods.clone()),
516 );
517 }
518 Some((other_agent_id, periods)) => {
519 if let Some(index) = periods
520 .iter()
521 .zip(&identity.periods)
522 .position(|(a, b)| a != b)
523 .or_else(|| {
524 (periods.len() != identity.periods.len())
525 .then(|| periods.len().min(identity.periods.len()))
526 })
527 {
528 return Err(RunIdentityError::PeriodMismatch {
529 key: identity.key.clone(),
530 agent_id: agent_id.clone(),
531 other_agent_id: other_agent_id.clone(),
532 index,
533 });
534 }
535 }
536 }
537 }
538 }
539
540 let mut period_window: BTreeMap<&str, (&RunKey, &str)> = BTreeMap::new();
545 for (key, (agent_id, periods)) in &declared_periods {
546 for period in periods {
547 let (first, first_agent_id) = *period_window
548 .entry(period.as_str())
549 .or_insert((key, agent_id.as_str()));
550 if first.window != key.window {
551 return Err(RunIdentityError::PeriodInTwoWindows(Box::new(
552 PeriodOverlap {
553 period: period.clone(),
554 first: first.clone(),
555 first_agent_id: first_agent_id.to_string(),
556 second: key.clone(),
557 second_agent_id: agent_id.clone(),
558 },
559 )));
560 }
561 }
562 }
563
564 let keys: Vec<RunKey> = windows
569 .iter()
570 .flat_map(|window| {
571 seeds.iter().map(move |seed| RunKey {
572 window: window.clone(),
573 seed: *seed,
574 })
575 })
576 .collect();
577 let mut submissions = subs;
578 for (position, identities) in per_agent {
579 let mut by_key: BTreeMap<&RunKey, usize> = BTreeMap::new();
580 for (index, identity) in identities.iter().enumerate() {
581 by_key.insert(&identity.key, index);
582 }
583 let order: Vec<usize> = keys
584 .iter()
585 .map(|key| *by_key.get(key).expect("every cell checked present"))
586 .collect();
587 let runs = std::mem::take(&mut submissions[position].runs);
588 let mut slots: Vec<Option<_>> = runs.into_iter().map(Some).collect();
589 submissions[position].runs = order
590 .into_iter()
591 .map(|index| {
592 slots[index]
593 .take()
594 .expect("permutation visits each run once")
595 })
596 .collect();
597 }
598
599 Ok(KeyedField {
600 keys,
601 windows,
602 seeds,
603 submissions,
604 declarations,
605 })
606}
607
608#[cfg(test)]
609mod tests {
610 use super::*;
611 use crate::composite::{score_agent, ScoreConfig};
612
613 fn spelled_field(windows: &[String], returns: &[f64]) -> String {
616 let docs = serde_json::json!([{
617 "agent_id": "a",
618 "runs": returns
619 .iter()
620 .map(|r| serde_json::json!({ "returns": [r] }))
621 .collect::<Vec<_>>(),
622 "run_keys": windows
623 .iter()
624 .map(|w| serde_json::json!({ "window": w, "seed": 0 }))
625 .collect::<Vec<_>>(),
626 }]);
627 serde_json::to_string(&docs).expect("serialize field")
628 }
629
630 #[test]
636 fn a_pooled_statistic_does_not_change_when_the_window_labels_are_respelled() {
637 let mut returns = vec![-0.3, -0.3];
641 returns.extend(std::iter::repeat_n(0.4, 10));
642 let short: Vec<String> = (1..=12).map(|i| format!("w{i}")).collect();
643 let padded: Vec<String> = (1..=12).map(|i| format!("w{i:02}")).collect();
644
645 let cfg = ScoreConfig {
646 execution_seeds_per_window: 1,
647 ..ScoreConfig::default()
648 };
649 let drawdown = |windows: &[String]| {
650 let keyed = parse_keyed_field(&spelled_field(windows, &returns)).expect("keyed field");
651 score_agent(&keyed.submissions[0], &cfg).max_drawdown
652 };
653
654 assert_eq!(
655 drawdown(&short),
656 drawdown(&padded),
657 "renaming w1..w12 to w01..w12 changed no return, so it must change no statistic"
658 );
659 }
660
661 #[test]
665 fn agents_that_declare_different_window_orders_are_refused() {
666 let json = field(&[
667 ("a", &[("w0", 0), ("w1", 0)]),
668 ("b", &[("w1", 0), ("w0", 0)]),
669 ]);
670 let error = parse_keyed_field(&json).expect_err("contradictory window order");
671 assert!(
672 matches!(error, RunIdentityError::WindowOrderMismatch { .. }),
673 "{error}"
674 );
675 }
676
677 fn field(entries: &[(&str, &[(&str, u64)])]) -> String {
678 let docs: Vec<serde_json::Value> = entries
679 .iter()
680 .map(|(agent_id, cells)| {
681 serde_json::json!({
682 "agent_id": agent_id,
683 "runs": cells
684 .iter()
685 .map(|(window, seed)| serde_json::json!({
686 "returns": [0.01, 0.02, *seed as f64 * 0.001, window.len() as f64 * 0.001],
687 }))
688 .collect::<Vec<_>>(),
689 "run_keys": cells
690 .iter()
691 .map(|(window, seed)| serde_json::json!({
692 "window": window,
693 "seed": seed,
694 }))
695 .collect::<Vec<_>>(),
696 })
697 })
698 .collect();
699 serde_json::to_string(&docs).expect("serialize field")
700 }
701
702 #[test]
703 fn a_complete_keyed_grid_is_reordered_into_one_canonical_cell_order() {
704 let json = field(&[
709 ("a", &[("w0", 0), ("w0", 1), ("w1", 0), ("w1", 1)]),
710 ("b", &[("w0", 1), ("w1", 0), ("w0", 0), ("w1", 1)]),
711 ]);
712 let keyed = parse_keyed_field(&json).expect("complete grid");
713 assert_eq!(
714 keyed.keys,
715 vec![
716 RunKey {
717 window: "w0".into(),
718 seed: 0
719 },
720 RunKey {
721 window: "w0".into(),
722 seed: 1
723 },
724 RunKey {
725 window: "w1".into(),
726 seed: 0
727 },
728 RunKey {
729 window: "w1".into(),
730 seed: 1
731 },
732 ]
733 );
734 assert_eq!(keyed.windows, vec!["w0".to_string(), "w1".to_string()]);
735 assert_eq!(keyed.seeds, vec![0, 1]);
736 for index in 0..keyed.keys.len() {
737 assert_eq!(
738 keyed.submissions[0].runs[index].returns, keyed.submissions[1].runs[index].returns,
739 "cell {index} must hold the same window and seed for both agents"
740 );
741 }
742 }
743
744 #[test]
745 fn a_legacy_unkeyed_run_array_is_refused_not_positionally_aligned() {
746 let json = r#"[{"agent_id":"a","runs":[{"returns":[0.01,0.02]}]}]"#;
747 assert_eq!(
748 parse_keyed_field(json).unwrap_err(),
749 RunIdentityError::UnkeyedSubmission {
750 agent_id: "a".to_string()
751 }
752 );
753 assert!(parse_keyed_field(json)
754 .unwrap_err()
755 .to_string()
756 .contains("run_keys"));
757 }
758
759 #[test]
760 fn a_missing_cell_fails_instead_of_scoring_a_partial_field() {
761 let json = field(&[
762 ("a", &[("w0", 0), ("w0", 1), ("w1", 0), ("w1", 1)]),
763 ("b", &[("w0", 0), ("w0", 1), ("w1", 0)]),
764 ]);
765 assert_eq!(
766 parse_keyed_field(&json).unwrap_err(),
767 RunIdentityError::MissingCell {
768 agent_id: "b".to_string(),
769 key: RunKey {
770 window: "w1".into(),
771 seed: 1
772 },
773 }
774 );
775 }
776
777 #[test]
778 fn an_incomplete_cartesian_grid_fails_even_when_every_agent_agrees() {
779 let json = field(&[
780 ("a", &[("w0", 0), ("w0", 1), ("w1", 0)]),
781 ("b", &[("w0", 0), ("w0", 1), ("w1", 0)]),
782 ]);
783 assert_eq!(
784 parse_keyed_field(&json).unwrap_err(),
785 RunIdentityError::IncompleteGrid {
786 windows: 2,
787 seeds: 2,
788 submitted: 3,
789 first_missing: RunKey {
790 window: "w1".into(),
791 seed: 1
792 },
793 }
794 );
795 }
796
797 #[test]
798 fn a_duplicated_cell_fails_instead_of_inflating_the_grid() {
799 let json = field(&[("a", &[("w0", 0), ("w0", 0)])]);
800 assert_eq!(
801 parse_keyed_field(&json).unwrap_err(),
802 RunIdentityError::DuplicateKey {
803 agent_id: "a".to_string(),
804 key: RunKey {
805 window: "w0".into(),
806 seed: 0
807 },
808 }
809 );
810 }
811
812 #[test]
813 fn key_count_must_match_run_count() {
814 let json = serde_json::json!([{
815 "agent_id": "a",
816 "runs": [{"returns": [0.01, 0.02]}, {"returns": [0.03, 0.04]}],
817 "run_keys": [{"window": "w0", "seed": 0}],
818 }])
819 .to_string();
820 assert_eq!(
821 parse_keyed_field(&json).unwrap_err(),
822 RunIdentityError::KeyCountMismatch {
823 agent_id: "a".to_string(),
824 runs: 2,
825 keys: 1,
826 }
827 );
828 }
829
830 #[test]
831 fn a_whitespace_padded_window_identity_is_refused() {
832 let json = field(&[("a", &[(" w0", 0)])]);
833 assert!(matches!(
834 parse_keyed_field(&json),
835 Err(RunIdentityError::InvalidKey { .. })
836 ));
837 }
838
839 #[test]
840 fn declared_period_identities_must_index_the_returns_and_agree_across_agents() {
841 let one = serde_json::json!([{
842 "agent_id": "a",
843 "runs": [{"returns": [0.01, 0.02]}],
844 "run_keys": [{"window": "w0", "seed": 0, "periods": ["d1"]}],
845 }])
846 .to_string();
847 assert_eq!(
848 parse_keyed_field(&one).unwrap_err(),
849 RunIdentityError::PeriodCountMismatch {
850 agent_id: "a".to_string(),
851 key: RunKey {
852 window: "w0".into(),
853 seed: 0
854 },
855 periods: 1,
856 returns: 2,
857 }
858 );
859
860 let two = serde_json::json!([
861 {
862 "agent_id": "a",
863 "runs": [{"returns": [0.01, 0.02]}],
864 "run_keys": [{"window": "w0", "seed": 0, "periods": ["d1", "d2"]}],
865 },
866 {
867 "agent_id": "b",
868 "runs": [{"returns": [0.03, 0.04]}],
869 "run_keys": [{"window": "w0", "seed": 0, "periods": ["d1", "d3"]}],
870 },
871 ])
872 .to_string();
873 assert_eq!(
874 parse_keyed_field(&two).unwrap_err(),
875 RunIdentityError::PeriodMismatch {
876 key: RunKey {
877 window: "w0".into(),
878 seed: 0
879 },
880 agent_id: "b".to_string(),
881 other_agent_id: "a".to_string(),
882 index: 1,
883 }
884 );
885 }
886
887 fn periods_field(entries: &[(&str, &[&str])]) -> String {
888 let docs: Vec<serde_json::Value> = entries
889 .iter()
890 .map(|(agent_id, periods)| {
891 let returns: Vec<f64> = (0..periods.len().max(2))
892 .map(|i| 0.001 * (i as f64 + 1.0))
893 .collect();
894 let mut key = serde_json::json!({"window": "w0", "seed": 0});
895 if !periods.is_empty() {
896 key["periods"] = serde_json::json!(periods);
897 }
898 serde_json::json!({
899 "agent_id": agent_id,
900 "runs": [{"returns": returns}],
901 "run_keys": [key],
902 })
903 })
904 .collect();
905 serde_json::to_string(&docs).expect("serialize field")
906 }
907
908 fn w0() -> RunKey {
909 RunKey {
910 window: "w0".into(),
911 seed: 0,
912 }
913 }
914
915 #[test]
916 fn a_run_repeating_a_period_is_refused_naming_the_period_and_both_indices() {
917 let distinct = periods_field(&[("a", &["d1", "d2", "d3", "d4"])]);
919 assert!(parse_keyed_field(&distinct).is_ok());
920
921 let repeated = periods_field(&[("a", &["d1", "d2", "d3", "d2"])]);
922 let error = parse_keyed_field(&repeated).unwrap_err();
923 assert_eq!(
924 error,
925 RunIdentityError::DuplicatePeriod {
926 agent_id: "a".to_string(),
927 key: w0(),
928 period: "d2".to_string(),
929 first: 1,
930 second: 3,
931 }
932 );
933 assert_eq!(
934 error.to_string(),
935 "run identity: agent `a` (window `w0`, seed 0) declares period `d2` at \
936 index 1 and again at index 3; one period cannot contribute two returns \
937 to a run"
938 );
939 }
940
941 #[test]
942 fn the_first_repeat_is_the_one_reported() {
943 assert_eq!(first_repeated_period(&[]), None);
944 let periods: Vec<String> = ["d1", "d2", "d3", "d3", "d1"]
945 .iter()
946 .map(|p| p.to_string())
947 .collect();
948 assert_eq!(first_repeated_period(&periods), Some((2, 3)));
949 }
950
951 #[test]
952 fn a_repeated_period_is_refused_even_when_every_agent_repeats_it() {
953 let shared = periods_field(&[("a", &["d1", "d1"]), ("b", &["d1", "d1"])]);
956 assert_eq!(
957 parse_keyed_field(&shared).unwrap_err(),
958 RunIdentityError::DuplicatePeriod {
959 agent_id: "a".to_string(),
960 key: w0(),
961 period: "d1".to_string(),
962 first: 0,
963 second: 1,
964 }
965 );
966
967 let one_declares = periods_field(&[("a", &[]), ("b", &["d1", "d1"])]);
970 assert_eq!(
971 parse_keyed_field(&one_declares).unwrap_err(),
972 RunIdentityError::DuplicatePeriod {
973 agent_id: "b".to_string(),
974 key: w0(),
975 period: "d1".to_string(),
976 first: 0,
977 second: 1,
978 }
979 );
980 }
981
982 type Cell<'a> = (&'a str, u64, &'a [&'a str]);
984
985 fn cells_field(entries: &[(&str, &[Cell<'_>])]) -> String {
988 let docs: Vec<serde_json::Value> = entries
989 .iter()
990 .map(|(agent_id, cells)| {
991 serde_json::json!({
992 "agent_id": agent_id,
993 "runs": cells
994 .iter()
995 .map(|(_, seed, periods)| serde_json::json!({
996 "returns": (0..periods.len().max(2))
997 .map(|i| 0.001 * (i as f64 + 1.0) + *seed as f64 * 0.0001)
998 .collect::<Vec<_>>(),
999 }))
1000 .collect::<Vec<_>>(),
1001 "run_keys": cells
1002 .iter()
1003 .map(|(window, seed, periods)| {
1004 let mut key = serde_json::json!({"window": window, "seed": seed});
1005 if !periods.is_empty() {
1006 key["periods"] = serde_json::json!(periods);
1007 }
1008 key
1009 })
1010 .collect::<Vec<_>>(),
1011 })
1012 })
1013 .collect();
1014 serde_json::to_string(&docs).expect("serialize field")
1015 }
1016
1017 fn cell(window: &str, seed: u64) -> RunKey {
1018 RunKey {
1019 window: window.into(),
1020 seed,
1021 }
1022 }
1023
1024 fn overlap(
1025 period: &str,
1026 first: RunKey,
1027 first_agent_id: &str,
1028 second: RunKey,
1029 second_agent_id: &str,
1030 ) -> RunIdentityError {
1031 RunIdentityError::PeriodInTwoWindows(Box::new(PeriodOverlap {
1032 period: period.to_string(),
1033 first,
1034 first_agent_id: first_agent_id.to_string(),
1035 second,
1036 second_agent_id: second_agent_id.to_string(),
1037 }))
1038 }
1039
1040 #[test]
1041 fn seeds_of_one_window_share_its_periods() {
1042 let both: &[Cell<'_>] = &[
1043 ("w0", 0, &["a", "b"]),
1044 ("w0", 1, &["a", "b"]),
1045 ("w1", 0, &["c", "d"]),
1046 ("w1", 1, &["c", "d"]),
1047 ];
1048 let keyed = parse_keyed_field(&cells_field(&[("x", both), ("y", both)]))
1049 .expect("replicate seeds of one window are the grid, not a repeat");
1050 assert_eq!(keyed.seeds, vec![0, 1]);
1051 assert_eq!(keyed.windows, vec!["w0".to_string(), "w1".to_string()]);
1052 }
1053
1054 #[test]
1055 fn a_period_declared_in_two_windows_is_refused() {
1056 let overlapping: &[Cell<'_>] = &[("w0", 0, &["a", "b"]), ("w1", 0, &["b", "c"])];
1059 let error =
1060 parse_keyed_field(&cells_field(&[("x", overlapping), ("y", overlapping)])).unwrap_err();
1061 assert_eq!(error, overlap("b", cell("w0", 0), "x", cell("w1", 0), "x"));
1062 assert_eq!(
1063 error.to_string(),
1064 "run identity: period `b` is declared in (window `w0`, seed 0) by agent `x` \
1065 and in (window `w1`, seed 0) by agent `x`; a period belongs to one window, \
1066 and the pooled track would count its return twice. Seeds of one window may \
1067 share periods, different windows may not"
1068 );
1069
1070 let copied: &[Cell<'_>] = &[("w0", 0, &["a", "b"]), ("w1", 0, &["a", "b"])];
1073 assert_eq!(
1074 parse_keyed_field(&cells_field(&[("x", copied)])).unwrap_err(),
1075 overlap("a", cell("w0", 0), "x", cell("w1", 0), "x")
1076 );
1077
1078 let disjoint: &[Cell<'_>] = &[("w0", 0, &["a", "b"]), ("w1", 0, &["c", "d"])];
1080 assert!(parse_keyed_field(&cells_field(&[("x", disjoint)])).is_ok());
1081 }
1082
1083 #[test]
1084 fn a_period_in_two_windows_is_refused_whatever_the_seeds() {
1085 let crossed: &[Cell<'_>] = &[
1088 ("w0", 0, &["a", "b"]),
1089 ("w0", 1, &["c", "d"]),
1090 ("w1", 0, &["c", "d"]),
1091 ("w1", 1, &["a", "b"]),
1092 ];
1093 assert_eq!(
1094 parse_keyed_field(&cells_field(&[("x", crossed)])).unwrap_err(),
1095 overlap("c", cell("w0", 1), "x", cell("w1", 0), "x")
1096 );
1097 }
1098
1099 #[test]
1100 fn a_period_in_two_windows_is_refused_when_different_agents_declare_them() {
1101 let x: &[Cell<'_>] = &[("w0", 0, &["a", "b"]), ("w1", 0, &[])];
1105 let y: &[Cell<'_>] = &[("w0", 0, &[]), ("w1", 0, &["b", "c"])];
1106 assert_eq!(
1107 parse_keyed_field(&cells_field(&[("x", x), ("y", y)])).unwrap_err(),
1108 overlap("b", cell("w0", 0), "x", cell("w1", 0), "y")
1109 );
1110 }
1111
1112 #[test]
1113 fn a_keyed_field_still_parses_as_a_plain_submission_array() {
1114 let json = field(&[("a", &[("w0", 0)])]);
1116 let subs: Vec<AgentSubmission> = serde_json::from_str(&json).expect("plain parse");
1117 assert_eq!(subs.len(), 1);
1118 assert_eq!(subs[0].runs.len(), 1);
1119 }
1120
1121 #[test]
1122 fn an_empty_field_has_no_grid() {
1123 assert_eq!(
1124 parse_keyed_field("[]").unwrap_err(),
1125 RunIdentityError::EmptyField
1126 );
1127 }
1128}