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.

Commit
31972d09088037823d8045d134a5ef5096c07dc9
Author
Marius Peter <dev@marius-peter.com>
Author date
Committer
Marius Peter <dev@marius-peter.com>
Committer date
Changed files
ARCHITECTURE.org
index 6ffc0536..a44fd91b 100644..100644
@@ -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
index 3760b246..b0919831 100644..100644
@@ -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