(** Application service: orchestrates the core over a {!Repository.S}. The API a client calls — no HTML or serialization. Every call is scoped to a {!Trainee.id}. The service holds no mutable state of its own: the active routine and the workout in progress live in the repository, so a restart loses nothing and two trainees never share a slot. Recovery gating lives here, not in the client. {!Evidence.Workout.start} demands a {!Recovery.clearance}, and this module is the only thing that decides how one is obtained: earned by having rested, or taken deliberately through {!begin_workout}'s [override] acknowledgment. Putting that policy here means a native client cannot quietly adopt looser rules than the web one. *) module Make (R : Repository.S) : sig type t val make : repo:R.t -> t (** [repo] is the store this service reads and writes. *) (** {2 Accounts} *) type register_error = [ `Username of Trainee.username_error | `Username_taken ] (** Why registration was refused: a malformed username or a username already in use. Passwords carry no policy, so they never refuse registration. *) val register : t -> username:string -> password:string -> (Trainee.t, register_error) result Lwt.t val authenticate : t -> username:string -> password:string -> Trainee.t option Lwt.t (** [Some] only when the username is known and the password verifies. *) val find_trainee : t -> Trainee.id -> Trainee.t option Lwt.t type change_username_error = [ `Username of Trainee.username_error | `Username_taken | `Unknown ] (** Why a rename was refused: a malformed name, a name already in use, or an unknown account. *) val change_username : t -> Trainee.id -> username:string -> (Trainee.t, change_username_error) result Lwt.t (** Rename the account. The trainee's own current name is accepted as a no-op. Returns the updated trainee on success. *) type change_password_error = [ `Incorrect_password | `Unknown ] (** Why a password change was refused: the current password did not verify, or the account is unknown. *) val change_password : t -> Trainee.id -> current:string -> next:string -> (Trainee.t, change_password_error) result Lwt.t (** Change the password. [current] must verify before [next] is stored, so a hijacked session cannot reset it silently. Passwords carry no policy. *) (** {2 Routines} *) val list_routines : t -> (Repository.routine_id * Prescription.Routine.t) list type error = | Unknown_routine | Not_recovered of Recovery.readiness (** Refused: recovery is incomplete and no override was given. Carries the reading so a client can say how much longer. *) val pp_error : Format.formatter -> error -> unit val select_routine : t -> Trainee.id -> Repository.routine_id -> (unit, error) result Lwt.t val active_routine : t -> Trainee.id -> (Repository.routine_id * Prescription.Routine.t) option Lwt.t val next_workout : t -> Trainee.id -> routine:Repository.routine_id -> (Prescription.Workout.t, error) result Lwt.t (** Where the cycle stands: the workout after the last one logged. *) val readiness : t -> Trainee.id -> routine:Repository.routine_id -> now:Recovery.timestamp -> (Recovery.readiness, error) result Lwt.t val begin_workout : t -> Trainee.id -> routine:Repository.routine_id -> now:Recovery.timestamp -> ?override:unit -> unit -> (Evidence.Workout.t, error) result Lwt.t (** Start the next workout. [Error (Not_recovered _)] unless recovery is complete or [override] explicitly acknowledges early training. *) val in_progress : t -> Trainee.id -> Evidence.Workout.t option Lwt.t (** The workout being logged, if any. Per-trainee and durable. *) type log_error = | No_workout_in_progress | Rejected of Evidence.Workout.error (** The workout refused the stimulus; see {!Evidence.Workout.error}. *) val pp_log_error : Format.formatter -> log_error -> unit val log : t -> Trainee.id -> Evidence.Stimulus.t -> (Evidence.Workout.t, log_error) result Lwt.t (** Record a stimulus against the workout in progress. *) val replace_current : t -> Trainee.id -> slot:int -> Evidence.Stimulus.t -> (Evidence.Workout.t, log_error) result Lwt.t (** Correct a recorded slot of the workout in progress. Replaces the slot's record rather than adding volume. *) val finish : t -> Trainee.id -> ended_at:Recovery.timestamp -> Repository.record option Lwt.t (** Complete and persist the workout in progress, clearing the slot. [None] if nothing was in progress. *) val cancel : t -> Trainee.id -> bool Lwt.t (** Discard the workout in progress, clearing the slot without saving it to history. Saved records are untouched. [true] if a workout was discarded, [false] if nothing was in progress. *) type edit_error = Unknown_workout | Rejected_edit of Evidence.Workout.error val find_record : t -> Trainee.id -> Repository.workout_id -> Repository.record option Lwt.t val add_to_record : t -> Trainee.id -> Repository.workout_id -> Evidence.Stimulus.t -> (Repository.record, edit_error) result Lwt.t val replace_in_record : t -> Trainee.id -> Repository.workout_id -> slot:int -> Evidence.Stimulus.t -> (Repository.record, edit_error) result Lwt.t (** Correct a recorded slot of a saved workout. Replaces the slot's record rather than adding volume. *) val history : t -> Trainee.id -> Repository.record list Lwt.t val progress : t -> Trainee.id -> Exercise.t -> (Progression.assessment, Progression.error) result Lwt.t val diagnostics : t -> Trainee.id -> Progression.diagnostic list Lwt.t (** Habits the record shows that HD1 names as causes of overtraining. *) (** {2 Subjective feedback} *) val record_feedback : t -> Trainee.id -> reported_at:Recovery.timestamp -> Evidence.Feedback.signal list -> (Evidence.Feedback.t, Evidence.Feedback.error) result Lwt.t (** Store a subjective feedback report. [Error] when a signal category repeats. Feedback is standalone: a trainee may report it at any time, independent of a workout. *) val feedback : t -> Trainee.id -> Evidence.Feedback.t list Lwt.t (** Stored feedback reports, most recent first. *) (** {2 Application feedback} *) type app_feedback_error = [ `Empty_message | `Unknown_feedback ] (** Why an application feedback operation was refused. *) val record_app_feedback : t -> Trainee.id -> submitted_at:Recovery.timestamp -> message:string -> (Repository.app_feedback, app_feedback_error) result Lwt.t (** Store a non-blank freeform application feedback message. *) val app_feedback : t -> Trainee.id -> Repository.app_feedback list Lwt.t (** All application feedback, ranked by upvotes for this viewer. *) val upvote_app_feedback : t -> Trainee.id -> Repository.app_feedback_id -> bool Lwt.t (** Try to add the trainee's vote to another trainee's feedback. *) val edit_app_feedback : t -> Trainee.id -> Repository.app_feedback_id -> message:string -> (unit, app_feedback_error) result Lwt.t (** Edit one of the trainee's own messages. *) val remove_app_feedback : t -> Trainee.id -> Repository.app_feedback_id -> bool Lwt.t (** Remove one of the trainee's own messages. *) end