@@ -167,6 +167,48 @@ def _build_report(
167167 }
168168
169169
170+ def _update_top_history (history_path : Path , leader : str , score : float ) -> list [dict ]:
171+ """Append to top_history.json if the leader changed. Returns the history."""
172+ history : list [dict ] = []
173+ if history_path .exists ():
174+ try :
175+ history = json .loads (history_path .read_text ())
176+ except (json .JSONDecodeError , ValueError ):
177+ history = []
178+
179+ now = datetime .now (timezone .utc ).isoformat ()
180+
181+ # Append if leader changed (or first entry)
182+ if not history or history [- 1 ].get ("team" ) != leader :
183+ history .append ({"team" : leader , "score" : score , "timestamp" : now })
184+ history_path .write_text (json .dumps (history , indent = 2 ) + "\n " )
185+
186+ return history
187+
188+
189+ def _compute_time_at_top (history : list [dict ]) -> dict [str , float ]:
190+ """Compute fraction of time each team has been the leader."""
191+ if not history :
192+ return {}
193+
194+ now = datetime .now (timezone .utc )
195+ durations : dict [str , float ] = {}
196+
197+ for i , entry in enumerate (history ):
198+ start = datetime .fromisoformat (entry ["timestamp" ])
199+ if i + 1 < len (history ):
200+ end = datetime .fromisoformat (history [i + 1 ]["timestamp" ])
201+ else :
202+ end = now
203+ secs = max (0 , (end - start ).total_seconds ())
204+ durations [entry ["team" ]] = durations .get (entry ["team" ], 0 ) + secs
205+
206+ total = sum (durations .values ())
207+ if total == 0 :
208+ return {t : 1.0 for t in durations } # single snapshot
209+ return {t : d / total for t , d in durations .items ()}
210+
211+
170212def update_leaderboard (scores_dir : Path , output_path : Path ) -> None :
171213 """Read all score files and produce a ranked leaderboard.json."""
172214 entries = []
@@ -194,6 +236,18 @@ def update_leaderboard(scores_dir: Path, output_path: Path) -> None:
194236 reverse = True ,
195237 )
196238
239+ # Track time at top
240+ history_path = output_path .parent / "top_history.json"
241+ if entries :
242+ history = _update_top_history (
243+ history_path , entries [0 ]["team" ], entries [0 ]["score" ]
244+ )
245+ time_at_top = _compute_time_at_top (history )
246+ for entry in entries :
247+ entry ["time_at_top" ] = round (time_at_top .get (entry ["team" ], 0 ), 4 )
248+ else :
249+ time_at_top = {}
250+
197251 leaderboard = {
198252 "updated" : datetime .now (timezone .utc ).isoformat (),
199253 "entries" : entries ,
0 commit comments