[OCaml] High Intensity Training Online
docs update architecture and enhancements
Keep the architecture guide aligned with the current web routes, progressive enhancement behavior, runtime commands, and verification process. Remove the stale builder task and record the prescribed-exercise row enhancement.
Changed files
ARCHITECTURE.org
@@ -1,7 +1,6 @@
1
Removed:
#+title: hito architecture
2
Removed:
#+options: toc:2 num:nil
1
Added:
#+TITLE: Hito architecture
3
2
4
Removed:
hito is a training tracker for Mike Mentzer's Heavy Duty. This document derives
3
Added:
Hito is a training tracker for Mike Mentzer's Heavy Duty. This document derives
5
4
the code from the doctrine, in that order: what the source claims, what those
6
5
claims force to be true of a program, and how the code carries them.
7
6
@@ -329,74 +328,80 @@
329
328
P -.->|remedy| R
330
329
#+end_src
331
330
332
Removed:
Plan and record are distinct types with a one-way transition: a prescription
333
Removed:
never learns what was performed, and the log never interprets.
331
Added:
Plan and record are distinct types with a one-way transition: a
332
Added:
prescription never learns what was performed, and the log never
333
Added:
interprets.
334
334
335
Removed:
The dashed arrow is the one link nothing automates. =Progression.remedy= returns
336
Removed:
a remedy; no code applies it to a routine. Acting on it belongs to a tier that
337
Removed:
does not exist yet, and inventing one silently would violate the rule that plans
338
Removed:
are authored.
335
Added:
The dashed arrow is the one link nothing
336
Added:
automates. =Progression.remedy= returns a remedy; no code applies it
337
Added:
to a routine. Acting on it belongs to a tier that does not exist yet,
338
Added:
and inventing one silently would violate the rule that plans are
339
Added:
authored.
339
340
340
341
* The web tier
341
342
342
Removed:
=Routes=, =Decode=, =Pages=, =Handlers=, and a small =js_of_ocaml= client. Dream
343
Removed:
and dream-html appear nowhere else. =Handlers= is a functor over =Repository.S=,
344
Removed:
so production runs on SQLite while tests run in memory.
343
Added:
=Routes=, =Decode=, =Pages=, =Handlers=, and a small =js_of_ocaml=
344
Added:
client. Dream and dream-html appear nowhere else. =Handlers= is a
345
Added:
functor over =Repository.S=, so production runs on SQLite while tests
346
Added:
run in memory.
345
347
346
Removed:
Every domain and service error becomes a user-facing sentence in one =Present=
347
Removed:
module, so a handler renders a message rather than inventing wording, and a new
348
Removed:
error variant surfaces as a missing case.
348
Added:
Every domain and service error becomes a user-facing sentence in one
349
Added:
=Present= module, so a handler renders a message rather than inventing
350
Added:
wording, and a new error variant surfaces as a missing case.
349
351
350
352
** Route surface
351
353
352
Removed:
| Route | Method | Purpose |
353
Removed:
|---------------------------------+-----------+-----------------------------------------------|
354
Removed:
| =/login= | GET, POST | Sign in |
355
Removed:
| =/register= | GET, POST | Create an account (absent unless opened) |
356
Removed:
| =/logout= | POST | Sign out |
357
Removed:
| =/= | GET | Overview: next workout and the recovery gate |
358
Removed:
| =/routines= | GET | Choose a routine |
359
Removed:
| =/routines/:id/select= | POST | Make a routine active |
360
Removed:
| =/routine= | GET | Active routine detail |
361
Removed:
| =/workout= | GET | The workout in progress |
362
Removed:
| =/workout= | POST | Begin the next workout, or refuse at the gate |
363
Removed:
| =/workout/slots/:slot= | POST | Record one prescribed stimulus |
364
Removed:
| =/workout/slots/:slot/edit= | POST | Correct a recorded slot |
365
Removed:
| =/workout/finish= | POST | Complete and persist |
366
Removed:
| =/workout/cancel= | POST | Discard without saving |
367
Removed:
| =/logbook= | GET | All recorded evidence for the user |
368
Removed:
| =/logbook/:id= | GET | A saved workout |
369
Removed:
| =/logbook/:id/slots/:slot= | POST | Add a missing record |
370
Removed:
| =/logbook/:id/slots/:slot/edit= | POST | Correct a saved slot |
371
Removed:
| =/profile= | GET | Account profile: rename and change password |
372
Removed:
| =/profile/username= | POST | Change the username |
373
Removed:
| =/profile/password= | POST | Change the password (verifies the current one)|
374
Removed:
| =/assets/hito.css= | GET | Stylesheet (embedded at build time) |
375
Removed:
| =/assets/workout-client.js= | GET | Enhancement client (embedded at build time) |
354
Added:
| Route | Method | Purpose |
355
Added:
|---------------------------------+-----------+------------------------------------------------|
356
Added:
| =/login= | GET, POST | Sign in |
357
Added:
| =/register= | GET, POST | Create an account (absent unless opened) |
358
Added:
| =/logout= | POST | Sign out |
359
Added:
| =/= | GET | Overview: next workout and the recovery gate |
360
Added:
| =/routines= | GET | Choose a routine |
361
Added:
| =/routines/:id/select= | POST | Make a routine active |
362
Added:
| =/routine= | GET | Active routine detail |
363
Added:
| =/workout= | GET | The workout in progress |
364
Added:
| =/workout= | POST | Begin the next workout, or refuse at the gate |
365
Added:
| =/workout/slots/:slot= | POST | Record one prescribed stimulus |
366
Added:
| =/workout/slots/:slot/edit= | POST | Correct a recorded slot |
367
Added:
| =/workout/finish= | POST | Complete and persist |
368
Added:
| =/workout/cancel= | POST | Discard without saving |
369
Added:
| =/logbook= | GET | All recorded evidence for the user |
370
Added:
| =/logbook/:id= | GET | A saved workout |
371
Added:
| =/logbook/:id/slots/:slot= | POST | Add a missing record |
372
Added:
| =/logbook/:id/slots/:slot/edit= | POST | Correct a saved slot |
373
Added:
| =/profile= | GET | Account profile: rename and change password |
374
Added:
| =/profile/username= | POST | Change the username |
375
Added:
| =/profile/password= | POST | Change the password (verifies the current one) |
376
Added:
| =/assets/hito.css= | GET | Stylesheet (embedded at build time) |
377
Added:
| =/assets/workout-client.js= | GET | Enhancement client (embedded at build time) |
376
378
377
379
Slot and record identity live in the path rather than hidden fields.
378
380
379
Removed:
The refusal at =POST /workout= redirects to the overview, which reports the
380
Removed:
remaining rest and offers an explicit override action. The interface therefore
381
Removed:
makes the clearance invariant visible instead of training early by accident.
381
Added:
The refusal at =POST /workout= redirects to the overview, which
382
Added:
reports the remaining rest and offers an explicit override action. The
383
Added:
interface therefore makes the clearance invariant visible instead of
384
Added:
training early by accident.
382
385
383
386
** The workout view
384
387
385
Removed:
One page serves both the workout in progress and a saved record. It shows one
386
Removed:
prescribed slot at a time under a vertical group of named exercise buttons, each
387
Removed:
a server-rendered link carrying its slot in =?slot==. The active link is marked
388
Removed:
=aria-current="page"=. A handler clamps a requested slot against the
389
Removed:
prescription and otherwise opens on the default: the first slot still awaiting a
390
Removed:
record, or the first slot when all are filled.
388
Added:
One page serves both the workout in progress and a saved record. It
389
Added:
shows one prescribed slot at a time under a vertical group of named
390
Added:
exercise buttons, each a server-rendered link carrying its slot in
391
Added:
=?slot==. The active link is marked =aria-current="page"=. A handler
392
Added:
clamps a requested slot against the prescription and otherwise opens
393
Added:
on the default: the first slot still awaiting a record, or the first
394
Added:
slot when all are filled.
391
395
392
Removed:
This is link navigation and route state, not an ARIA widget — deterministic, and
393
Removed:
correct with no client script at all.
396
Added:
This is link navigation and route state, not an ARIA widget —
397
Added:
deterministic, and correct with no client script at all.
394
398
395
399
** Progressive enhancement
396
400
397
Removed:
The =js_of_ocaml= client is an enhancement, never a requirement. Server-rendered
398
Removed:
HTML remains the only source of UI, and the fallback is the ordinary link/form
399
Removed:
behaviour the pages already have.
401
Added:
The =js_of_ocaml= client is an enhancement, never a
402
Added:
requirement. Server-rendered HTML remains the only source of UI, and
403
Added:
the fallback is the ordinary link/form behaviour the pages already
404
Added:
have.
400
405
401
406
The contract is a set of markers the server emits:
402
407
@@ -410,27 +415,30 @@
410
415
| =data-hito-workout-status= | an =aria-live= region for announcements |
411
416
| =data-hito-toast= | a transient notice the client dismisses |
412
417
413
Removed:
Behaviour: the client fetches the same URL the link or form would have used,
414
Removed:
extracts the marked region from the response, and swaps it. Successful
415
Removed:
transitions push the final response URL, so redirects stay reflected in the
416
Removed:
address bar; server-rendered validation responses render in place *without* a
417
Removed:
history entry. A monotonic request counter makes the newest interaction win, and
418
Removed:
=aria-busy= marks the shell while one is pending. Recording a stimulus returns
419
Removed:
to the submitted slot rather than the server's default, which is the one place
420
Removed:
the client shapes the flow. Anything it cannot apply — a missing marker, a
421
Removed:
transport failure, a modified click — falls back to native navigation.
418
Added:
Behaviour: the client fetches the same URL the link or form would have
419
Added:
used, extracts the marked region from the response, and swaps
420
Added:
it. Successful transitions push the final response URL, so redirects
421
Added:
stay reflected in the address bar; server-rendered validation
422
Added:
responses render in place *without* a history entry. A monotonic
423
Added:
request counter makes the newest interaction win, and =aria-busy=
424
Added:
marks the shell while one is pending. Recording a stimulus returns to
425
Added:
the submitted slot rather than the server's default, which is the one
426
Added:
place the client shapes the flow. Anything it cannot apply — a missing
427
Added:
marker, a transport failure, a modified click — falls back to native
428
Added:
navigation.
422
429
423
Removed:
The browser holds no domain logic. It never decides whether a set is valid,
424
Removed:
whether recovery is complete, or whether a correction is volume.
430
Added:
The browser holds no domain logic. It never decides whether a set is
431
Added:
valid, whether recovery is complete, or whether a correction is
432
Added:
volume.
425
433
426
434
* Running it
427
435
428
436
#+begin_src sh
429
Removed:
dune build && ./_build/default/bin/main.exe # http://localhost:8080/
437
Added:
dune build && ./_build/default/bin/main.exe # http://localhost:8080/
430
438
#+end_src
431
439
432
Removed:
=bin/main.ml= opens the store, applies migrations, seeds the single account, and
433
Removed:
starts Dream. The socket binds *loopback only*.
440
Added:
=bin/main.ml= opens the store, applies migrations, seeds the single
441
Added:
account, and starts Dream. The socket binds *loopback only*.
434
442
435
443
| Variable | Purpose | Default |
436
444
|----------------------+---------------------------------------+-------------------------|
@@ -439,34 +447,36 @@
439
447
| =HITO_PORT= | TCP port | =8080= |
440
448
| =HITO_SEED_PASSWORD= | Password for the seeded account | none — refuses to start |
441
449
442
Removed:
Set =HITO_SECRET= in production; unset, sessions do not survive a restart.
450
Added:
Set =HITO_SECRET= in production; unset, sessions do not survive a
451
Added:
restart.
443
452
444
453
Dream runs its own SQL pool for sessions against the same file as
445
Removed:
=Sqlite_repo=. SQLite serializes writers, so this is safe for local use; a
446
Removed:
separate session store is the path to higher concurrency.
454
Added:
=Sqlite_repo=. SQLite serializes writers, so this is safe for local
455
Added:
use; a separate session store is the path to higher concurrency.
447
456
448
457
* Verification
449
458
450
Removed:
One Alcotest suite per module, =test/test_<module>.ml=, each exposing =suite=,
451
Removed:
registered in =test/test_hito.ml= in layer order: vocabulary, plan, record,
452
Removed:
judgment, service, serialization, storage, decoding, web.
459
Added:
One Alcotest suite per module, =test/test_<module>.ml=, each exposing
460
Added:
=suite=, registered in =test/test_hito.ml= in layer order: vocabulary,
461
Added:
plan, record, judgment, service, serialization, storage, decoding,
462
Added:
web.
453
463
454
464
What the less obvious suites are for:
455
465
456
Removed:
- =test_codec= — a stored workout round-trips faithfully, and malformed or
457
Removed:
incomplete text is *reported*, not raised.
458
Removed:
- =test_sqlite_repo= — state survives a reconnect, trainees stay isolated,
459
Removed:
reapplying migrations is idempotent, and finishing is atomic across a
460
Removed:
reconnect, against a temporary database file.
461
Removed:
- =test_decode= — the trust boundary: form values are validated before the core
462
Removed:
sees them.
463
Removed:
- =test_web= — carries the session cookie and CSRF token between requests, so
464
Removed:
authentication, the recovery gate, corrections, and the no-JavaScript
465
Removed:
behaviour are exercised end to end.
466
Added:
- =test_codec= — a stored workout round-trips faithfully, and
467
Added:
malformed or incomplete text is *reported*, not raised.
468
Added:
- =test_sqlite_repo= — state survives a reconnect, trainees stay
469
Added:
isolated, reapplying migrations is idempotent, and finishing is
470
Added:
atomic across a reconnect, against a temporary database file.
471
Added:
- =test_decode= — the trust boundary: form values are validated before
472
Added:
the core sees them.
473
Added:
- =test_web= — carries the session cookie and CSRF token between
474
Added:
requests, so authentication, the recovery gate, corrections, and the
475
Added:
no-JavaScript behaviour are exercised end to end.
466
476
467
Removed:
Done means, in order: =dune build @check= clean, =dune runtest= passing,
468
Removed:
=dune fmt= applied with =dune build @fmt= clean, and documentation that matches
469
Removed:
the change.
477
Added:
Done means, in order: =dune build @check= clean, =dune runtest=
478
Added:
passing, =dune fmt= applied with =dune build @fmt= clean, and
479
Added:
documentation that matches the change.
470
480
471
481
* Tensions with the source
472
482
:PROPERTIES:
@@ -475,43 +485,53 @@
475
485
476
486
Recorded rather than resolved, because HD1 does not settle them.
477
487
478
Removed:
*48h against 72h.* Chapter 3 says up to 72 hours of rest, sometimes more, is
479
Removed:
needed for growth; the Ideal Routine prescribes training every other day.
480
Removed:
Mentzer does not reconcile these. We encode the routine — 48h within a cycle,
481
Removed:
72h after it — and note the discrepancy here.
488
Added:
*48h against 72h.* Chapter 3 says up to 72 hours of rest, sometimes
489
Added:
more, is needed for growth; the Ideal Routine prescribes training
490
Added:
every other day. Mentzer does not reconcile these. We encode the
491
Added:
routine — 48h within a cycle, 72h after it — and note the discrepancy
492
Added:
here.
482
493
483
Removed:
*Per-exercise rep windows.* HD1 gives one guideline, 6–10, for every listed
484
Removed:
exercise. We let a prescription name its own window, bounded to 6–12, on the
485
Removed:
argument that leg work may warrant a different range. A deliberate departure —
486
Removed:
and note that Mentzer's own rationale for the upper bound, cardiorespiratory
487
Removed:
failure arriving before muscular failure, cuts against higher reps for large
488
Removed:
compounds rather than for them.
494
Added:
*Per-exercise rep windows.* HD1 gives one guideline, 6–10, for every
495
Added:
listed exercise. We let a prescription name its own window, bounded to
496
Added:
6–12, on the argument that leg work may warrant a different range. A
497
Added:
deliberate departure — and note that Mentzer's own rationale for the
498
Added:
upper bound, cardiorespiratory failure arriving before muscular
499
Added:
failure, cuts against higher reps for large compounds rather than for
500
Added:
them.
489
501
490
Removed:
*Extra volume is recordable.* HD1 forbids it in the strongest terms, but a log
491
Removed:
that refuses to state what happened is worse than one recording an error. The
492
Removed:
constraint lives on the prescription side, which cannot ask for extra work;
493
Removed:
performing extra shows up as more stimuli than slots.
502
Added:
*Extra volume is recordable.* HD1 forbids it in the strongest terms,
503
Added:
but a log that refuses to state what happened is worse than one
504
Added:
recording an error. The constraint lives on the prescription side,
505
Added:
which cannot ask for extra work; performing extra shows up as more
506
Added:
stimuli than slots.
494
507
495
Removed:
*Extension rarity is a diagnostic, not an invariant.* Same reason. HD1 says
496
Removed:
never to extend every exercise of a workout; if you did, the record must say so.
508
Added:
*Extension rarity is a diagnostic, not an invariant.* Same reason. HD1
509
Added:
says never to extend every exercise of a workout; if you did, the
510
Added:
record must say so.
497
511
498
Removed:
*Deadlifts substitute for hyperextensions* across mechanics, because HD1 offers
499
Removed:
them as alternatives despite one being an isolation and the other a compound.
512
Added:
*Deadlifts substitute for hyperextensions* across mechanics, because
513
Added:
HD1 offers them as alternatives despite one being an isolation and the
514
Added:
other a compound.
500
515
501
Removed:
*The log form offers one extension, not a stack.* =Evidence.Stimulus= permits
502
Removed:
forced reps /then/ negatives, as HD1 describes; the web form currently offers a
503
Removed:
single choice. A limitation of the form, not the model.
516
Added:
*The log form offers one extension, not a stack.* =Evidence.Stimulus=
517
Added:
permits forced reps /then/ negatives, as HD1 describes; the web form
518
Added:
currently offers a single choice. A limitation of the form, not the
519
Added:
model.
504
520
505
Removed:
*No consolidation routine.* HD2 material, and we have no source for it.
521
Added:
*No consolidation routine.* HD2 material, and we have no source for
522
Added:
it.
506
523
507
524
* Deferred
508
525
509
Removed:
- =Evidence.Feedback= is modelled and tested but not yet recorded or shown.
510
Removed:
- =Progression.remedy= is computed but never applied to a routine — the dashed
511
Removed:
arrow above.
512
Removed:
- Equipment granularity, so a suggested load is one a bar can actually hold.
513
Removed:
- A trainee layer carrying individual recovery ability, spotter availability, and
514
Removed:
the way recovery needs outgrow strength as a trainee gets stronger.
526
Added:
- =Evidence.Feedback= is modelled and tested but not yet recorded or
527
Added:
shown.
528
Added:
- =Progression.remedy= is computed but never applied to a routine —
529
Added:
the dashed arrow above.
530
Added:
- Equipment granularity, so a suggested load is one a bar can actually
531
Added:
hold.
532
Added:
- A trainee layer carrying individual recovery ability, spotter
533
Added:
availability, and the way recovery needs outgrow strength as a
534
Added:
trainee gets stronger.
515
535
- HD2 consolidation, once sourced.
516
536
- A separate session store, for concurrency beyond local use.
517
537
- A native client, which would sit on =hito.app= beside the web tier.
ENHANCEMENTS.org
@@ -118,15 +118,11 @@
118
118
- Existing data can be dropped safely.
119
119
120
120
- result :: commit=79bbbd7 tests=215 pass; migration 5 drops affected legacy evidence
121
Removed:
122
Removed:
** TODO Make the builder subagent produce one commit per feature
123
Removed:
SCHEDULED: [2026-09-05 Sat 22:30]
124
Removed:
125
Removed:
the enhancement builder should produce one commit per feature.
126
Removed:
127
Removed:
Clarification:
128
Removed:
- The builder is the builder subagent.
129
Removed:
121
Added:
** TODO Give each prescribed exercise its own routine sub-row
122
Added:
SCHEDULED: [2026-09-05 Sat 23:10]
123
Added:
- Each prescribed exercise should exist in a separate sub-row.
124
Added:
- Stimuli count column should not exist.
125
Added:
- Each prescribed exercise should have specific sub-columns for exercise name, rep count.
130
126
* Core domain
131
127
132
128
* Navigation