(** Persistence port. A pure module type: no database or web framework. Identity lives here rather than in the core, which carries none — a routine, a stored workout, or a trainee needs a name only once something has to remember it. Every operation is scoped to a {!Trainee.id}. There is no server-wide state: the active routine and the workout in progress belong to a trainee and are stored, so nothing is lost across a restart and no two trainees share a slot. *) type routine_id = private string type workout_id = private string val routine_id : string -> routine_id val workout_id : string -> workout_id type record = { id : workout_id; workout : Evidence.Workout.t } (** A stored workout. It already knows its prescription, its timestamps, and the basis on which it was begun. *) type app_feedback_id = private string val app_feedback_id : string -> app_feedback_id val app_feedback_id_to_string : app_feedback_id -> string type app_feedback = { feedback_id : app_feedback_id; author : string; contributions : int; submitted_at : Recovery.timestamp; message : string; upvotes : int; viewer_upvoted : bool; viewer_owns : bool; } (** A ranked application comment with viewer vote and ownership state. *) (** Effects run in Lwt: an adapter may talk to a database. *) module type S = sig type t (** {2 Accounts} *) val create_trainee : t -> username:Trainee.username -> credential:Trainee.credential -> (Trainee.t, [ `Username_taken ]) result Lwt.t val find_trainee_by_username : t -> Trainee.username -> Trainee.t option Lwt.t val find_trainee : t -> Trainee.id -> Trainee.t option Lwt.t val update_username : t -> Trainee.id -> Trainee.username -> (Trainee.t, [ `Username_taken ]) result Lwt.t (** Rename the account. [`Username_taken] if another account holds the name. The trainee's own current name is accepted as a no-op. Returns the updated trainee on success. *) val update_credential : t -> Trainee.id -> Trainee.credential -> Trainee.t option Lwt.t (** Replace the account's password verifier. [None] if unknown. *) (** {2 Catalog} Shared, not trainee-scoped. *) val list_routines : t -> (routine_id * Prescription.Routine.t) list val find_routine : t -> routine_id -> Prescription.Routine.t option (** {2 Per-trainee selection and workout in progress} *) val active_routine : t -> Trainee.id -> routine_id option Lwt.t val set_active_routine : t -> Trainee.id -> routine_id -> unit Lwt.t val in_progress : t -> Trainee.id -> Evidence.Workout.t option Lwt.t val set_in_progress : t -> Trainee.id -> Evidence.Workout.t option -> unit Lwt.t (** [None] clears the slot. *) (** {2 History} *) val finish_workout : t -> Trainee.id -> Evidence.Workout.t -> record Lwt.t (** Store a finished workout and clear the in-progress slot as one unit. An adapter with transactions performs both in a single transaction, so a workout is never both saved to history and still shown as in progress. *) val find : t -> Trainee.id -> workout_id -> record option Lwt.t val replace : t -> Trainee.id -> record -> bool Lwt.t (** Replace an existing record by identity. *) val log : t -> Trainee.id -> Evidence.Log.t Lwt.t (** The stored log — the only source of evidence. *) val history : t -> Trainee.id -> record list Lwt.t (** Most recent first. *) (** {2 Subjective feedback} *) val save_feedback : t -> Trainee.id -> Evidence.Feedback.t -> unit Lwt.t (** Store a subjective feedback report. Feedback is a standalone observation, not tied to a workout: a trainee may report it at any time. *) val feedback : t -> Trainee.id -> Evidence.Feedback.t list Lwt.t (** Stored feedback reports, most recent first. *) val save_app_feedback : t -> Trainee.id -> submitted_at:Recovery.timestamp -> message:string -> app_feedback Lwt.t (** Store a freeform application feedback report and return its identity. *) val app_feedback : t -> viewer:Trainee.id -> app_feedback list Lwt.t (** Return all reports, ranked by upvotes, with the viewer's vote state. *) val upvote_app_feedback : t -> voter:Trainee.id -> app_feedback_id -> bool Lwt.t (** Add one vote from another trainee. [false] means no vote was added. *) val update_app_feedback : t -> author:Trainee.id -> app_feedback_id -> message:string -> bool Lwt.t (** Update a report only when [author] owns it. *) val delete_app_feedback : t -> author:Trainee.id -> app_feedback_id -> bool Lwt.t (** Remove a report and its votes only when [author] owns it. *) end