(** Read run data from day10's log directory *) let list_runs ~log_dir = let runs_dir = Filename.concat log_dir "runs" in if Sys.file_exists runs_dir && Sys.is_directory runs_dir then Sys.readdir runs_dir |> Array.to_list |> List.filter (fun name -> let path = Filename.concat runs_dir name in Sys.is_directory path) |> List.sort (fun a b -> String.compare b a) (* Descending *) else [] let get_latest_run_id ~log_dir = let latest = Filename.concat log_dir "latest" in if Sys.file_exists latest then try let target = Unix.readlink latest in (* Target is like "runs/2026-02-04-120000" *) Some (Filename.basename target) with Unix.Unix_error _ -> None else None (** Get the most recent run, including runs in progress. This scans the runs/ directory directly rather than relying on the 'latest' symlink which is only created when a run completes. *) let get_most_recent_run_id ~log_dir = match list_runs ~log_dir with | [] -> None | most_recent :: _ -> Some most_recent let read_summary ~log_dir ~run_id = let path = Filename.concat log_dir (Filename.concat "runs" (Filename.concat run_id "summary.json")) in if Sys.file_exists path then try let content = In_channel.with_open_text path In_channel.input_all in let json = Yojson.Safe.from_string content in let open Yojson.Safe.Util in let failures = json |> member "failures" |> to_list |> List.map (fun f -> (f |> member "package" |> to_string, f |> member "error" |> to_string)) in Some { Day10_lib.Run_log.run_id = json |> member "run_id" |> to_string; start_time = json |> member "start_time" |> to_string; end_time = json |> member "end_time" |> to_string; duration_seconds = json |> member "duration_seconds" |> to_float; targets_requested = json |> member "targets_requested" |> to_int; solutions_found = json |> member "solutions_found" |> to_int; build_success = json |> member "build_success" |> to_int; build_failed = json |> member "build_failed" |> to_int; doc_success = json |> member "doc_success" |> to_int; doc_failed = json |> member "doc_failed" |> to_int; doc_skipped = json |> member "doc_skipped" |> to_int; failures; } with _ -> None else None let read_log_file path = if Sys.file_exists path then try Some (In_channel.with_open_text path In_channel.input_all) with _ -> None else None let read_build_log ~log_dir ~run_id ~package = let path = Filename.concat log_dir (Filename.concat "runs" (Filename.concat run_id (Filename.concat "build" (package ^ ".log")))) in read_log_file path let read_doc_log ~log_dir ~run_id ~package = let path = Filename.concat log_dir (Filename.concat "runs" (Filename.concat run_id (Filename.concat "docs" (package ^ ".log")))) in read_log_file path let list_logs_in_dir dir = if Sys.file_exists dir && Sys.is_directory dir then Sys.readdir dir |> Array.to_list |> List.filter (fun name -> Filename.check_suffix name ".log") |> List.map (fun name -> Filename.chop_suffix name ".log") |> List.sort String.compare else [] let list_build_logs ~log_dir ~run_id = let dir = Filename.concat log_dir (Filename.concat "runs" (Filename.concat run_id "build")) in list_logs_in_dir dir let list_doc_logs ~log_dir ~run_id = let dir = Filename.concat log_dir (Filename.concat "runs" (Filename.concat run_id "docs")) in list_logs_in_dir dir let has_build_log ~log_dir ~run_id ~package = let path = Filename.concat log_dir (Filename.concat "runs" (Filename.concat run_id (Filename.concat "build" (package ^ ".log")))) in Sys.file_exists path let has_doc_log ~log_dir ~run_id ~package = let path = Filename.concat log_dir (Filename.concat "runs" (Filename.concat run_id (Filename.concat "docs" (package ^ ".log")))) in Sys.file_exists path let is_run_in_progress ~log_dir ~run_id = let summary_path = Filename.concat log_dir (Filename.concat "runs" (Filename.concat run_id "summary.json")) in (* If no summary.json exists, the run is likely still in progress *) not (Sys.file_exists summary_path) let get_package_status_from_summary ~log_dir ~run_id ~package = match read_summary ~log_dir ~run_id with | None -> None | Some summary -> (* Check if package is in the failures list *) match List.find_opt (fun (pkg, _) -> pkg = package) summary.failures with | Some (_, error) -> Some (`Failed error) | None -> (* Not in failures - check if logs exist to confirm it was processed *) if has_build_log ~log_dir ~run_id ~package || has_doc_log ~log_dir ~run_id ~package then Some `Success else Some `Not_in_run