[OCaml] High Intensity Training Online
docs Align architecture and steering with the doctrine update
The doctrine-alignment work merged without its documentation step, so the binding steering contradicted the code: domain.md and the doctrine-check skill still listed rest-pause, ocaml.md described three libraries, and ARCHITECTURE.org named the Progression module, eight feedback signals on a five-point score, and a web-layer CSV adapter. Bring each in line with the code as it stands: three quantified HD1 extensions, the Progress module with its Decompensating state, the per-muscle ceiling in the enforcement table, two relative feedback signals, six libraries including hito.import, Present as the one owner of wording, and the seed module beside the composition root.
Changed files
.kiro/skills/doctrine-check/SKILL.md
@@ -68,7 +68,8 @@
68
68
- **Intensity is categorical, not scalar.** A working set ends at momentary
69
69
muscular failure; that is its definition (HD1, "The Intensity Factor": 0% at
70
70
rest, 100% at failure). Effort past failure is qualitative
71
Removed:
(`Beyond_failure`: forced reps, negatives, rest-pause, static hold). Flag any
71
Added:
(`Beyond_failure`: forced reps, negatives, static hold — each a count).
72
Added:
Rest-pause is not in HD1; flag any attempt to add it. Flag any
72
73
numeric "intensity score".
73
74
- **Recovery produces growth; the gym only stimulates it.** Stimulation is
74
75
immediate; production is not (HD1, "The Economics of Growth and Recovery").
.kiro/steering/domain.md
@@ -16,7 +16,8 @@
16
16
definition (HD1, "The Intensity Factor": the only accurate measures are 0%
17
17
at rest and 100% at failure).
18
18
- Going further is qualitative — `Beyond_failure`: forced reps, negatives,
19
Removed:
rest-pause, static hold.
19
Added:
static hold. Each is a count (reps, or seconds held). Rest-pause is not in
20
Added:
HD1 and is not modeled.
20
21
- Never introduce a numeric "intensity score".
21
22
- **Recovery produces growth; the gym only stimulates it.**
22
23
- Stimulation is immediate; production is not (HD1, "The Economics of Growth
@@ -24,6 +25,8 @@
24
25
- Recovery is systemic and time-based — any hard workout gates the next one.
25
26
- **Volume beyond the minimum is harmful.**
26
27
- One working set per exercise is nominal; three sets per muscle is the ceiling.
28
Added:
A prescription that exceeds it is unrepresentable — `Prescription.Workout.make`
29
Added:
refuses it.
27
30
- The net negative effect of each extra set increases geometrically.
28
31
- Rising volume is a diagnostic warning — never an achievement or a progress
29
32
target.
@@ -59,6 +62,8 @@
59
62
- A log that refuses to state what happened is worse than one recording an
60
63
error.
61
64
- On a stall, the answer is never more work.
65
Added:
- HD1 names two failure states: a *stall* (progress halts, strength holds)
66
Added:
and *decompensation* (strength is lost). Both get the same corrective.
62
67
- No remedy may add volume or frequency.
63
68
- Doctrinal remedy (HD1, "Regulating Volume and Frequency"): after two weeks
64
69
of no progress, take a week off, then *reduce* volume and frequency —
.kiro/steering/ocaml.md
@@ -33,14 +33,32 @@
33
33
34
34
## Layering
35
35
36
Removed:
Three libraries, dependencies pointing inward only:
36
Added:
Six libraries, dependencies pointing inward only. The build graph enforces the
37
Added:
rule: a library cannot name what it does not link.
37
38
38
Removed:
- `hito.core` — pure domain. No framework, database, or serialization
39
Removed:
dependencies. Ever.
40
Removed:
- `hito.app` — `Repository` port, `Service`, adapters. Knows nothing of Dream.
41
Removed:
- `hito.web` — Dream application. The only place Dream and dream-html appear.
39
Added:
- `hito.core` — pure domain. No framework, database, serialization, or Lwt
40
Added:
dependency. Ever.
41
Added:
- `hito.app` — `Repository` and `Password_hash` ports, `Service`, `Catalog`,
42
Added:
`Workout_import`. Depends on `hito.core` and `lwt` only. Knows nothing of
43
Added:
Dream, Caqti, or any adapter.
44
Added:
- `hito.import` — adapters for third-party exports (`Hevy_csv`). Depends on
45
Added:
`hito.app` and the parser it needs. No web framework.
46
Added:
- `hito.persistence` — adapters that implement the ports: `Sqlite_repo`,
47
Added:
`Bcrypt_hash`, `Codec`, `Migrations`. The only place SQL appears.
48
Added:
- `hito.presenter` — view models, routes, pages. Depends on Dream and
49
Added:
dream-html only. It cannot name a core entity, a service, or an adapter.
50
Added:
- `hito.web` — Dream controllers. Maps entities to view models, owns all
51
Added:
user-facing phrasing (`Present`), and never reaches an adapter directly.
42
52
53
Added:
`bin/` is the composition root: it alone names both an adapter and the use
54
Added:
case it plugs in.
55
Added:
43
56
Rules:
44
57
45
Removed:
- Ports are module types; adapters implement them.
58
Added:
- Ports are module types; adapters implement them; `Service.Make` and
59
Added:
`Handlers.Make` are the dependency inversion.
46
60
- Persistence identity is assigned in `hito.app`, not in the core.
61
Added:
- Data crossing a boundary is a plain value: a view-model DTO outward, a core
62
Added:
type inward. Never an entity into the presenter, never a row into the core.
63
Added:
- `pp` functions in the core are debugging output. User-facing wording lives in
64
Added:
`hito.web`'s `Present` and nowhere inward.
ARCHITECTURE.org
@@ -39,9 +39,10 @@
39
39
/Consequence./ Reaching failure is an invariant of the recorded type, not a
40
40
field to be filled in. What varies is load, the reps it happened to yield, and
41
41
whether anything carried the effort *past* failure — which is qualitative, not
42
Removed:
more of the same: =Beyond_failure= of forced reps, negatives, rest-pause, or a
43
Removed:
static hold. There is no numeric intensity score anywhere, and there never
44
Removed:
should be.
42
Added:
more of the same: =Beyond_failure= of forced reps, negatives, or a static hold,
43
Added:
each carrying its count (reps, or seconds held). These three are the whole of
44
Added:
HD1's beyond-failure work; rest-pause is later Mentzer and is not modeled. There
45
Added:
is no numeric intensity score anywhere, and there never should be.
45
46
46
47
** The stimulus is the unit of work
47
48
@@ -94,7 +95,7 @@
94
95
Progress is knowable only from what was performed. A log that argues with
95
96
history is useless.
96
97
97
Removed:
/Consequence./ =Evidence= records and never interprets. =Progression= is the
98
Added:
/Consequence./ =Evidence= records and never interprets. =Progress= is the
98
99
only judge, and it may decline: =Insufficient_data= rather than a guess, so two
99
100
flat sessions a day apart never condemn a routine. Invariants belong on plans,
100
101
which are *authored*, and not on history, which is *observed*.
@@ -108,7 +109,7 @@
108
109
| vocabulary | =Exercise= | what movements exist, and pair |
109
110
| normative | =Prescription= | what should be done |
110
111
| factual | =Evidence= | what was done |
111
Removed:
| inferential | =Progression= | what that means |
112
Added:
| inferential | =Progress= | what that means |
112
113
113
114
=Recovery= straddles the divide deliberately: it measures a fact (elapsed rest)
114
115
and issues a normative artifact (a clearance). That is the one place the two
@@ -124,13 +125,14 @@
124
125
| one drive to failure per slot | shape of =Prescription.Stimulus= | absence |
125
126
| a recorded effort reached failure | =Evidence.Stimulus.outcome= | type |
126
127
| training requires recovery or acknowledgement | =Evidence.Workout.start= | type |
127
Removed:
| a stall is never answered with work | =Progression.remedy= return type | type |
128
Added:
| a stall is never answered with work | =Progress.remedy= return type | type |
128
129
| a correction is not extra volume | =Evidence.Workout.replace_stimulus= | type |
129
Removed:
| 6–12 calibration window | =Prescription.Rep_range.limits= | runtime |
130
Added:
| at most three sets per muscle in a plan | =Prescription.Workout.make= | runtime |
131
Added:
| 6–12 authoring envelope for the rep band | =Prescription.Rep_range.limits= | runtime |
130
132
| isolation into compound, sharing a target | =Exercise.may_pre_exhaust= | runtime |
131
133
| substitutions come from author lists | =Exercise.may_substitute= | runtime |
132
134
| 48h between workouts, 72h after the cycle | =Prescription.Routine.recovery_after= | value |
133
Removed:
| load rises at twelve reps, by 10–20% | =Progression.judge_load= | value |
135
Added:
| load rises at twelve reps, by 10–20% | =Progress.judge_load= | value |
134
136
135
137
The bias is intentional: a principle carried by a type cannot be forgotten, and
136
138
the runtime checks are confined to *authoring* — building a prescription,
@@ -139,27 +141,37 @@
139
141
140
142
* Layers
141
143
142
Removed:
Four libraries. Dependencies point inward only.
144
Added:
Six libraries. Dependencies point inward only.
143
145
144
146
#+begin_src
145
Removed:
hito.web Dream · dream-html · js_of_ocaml client
146
Removed:
| HTTP, HTML, sessions, CSRF, wall-clock time
147
Added:
hito.web Handlers · Decode · Present
148
Added:
| HTTP, sessions, CSRF, wall-clock time, all user-facing wording
147
149
|
148
Removed:
| hito.persistence Sqlite_repo · Memory_repo · Codec · Migrations · Bcrypt_hash
149
Removed:
| | Caqti, the SQLite driver, serialization, hashing
150
Added:
| hito.presenter View_model · Routes · Pages
151
Added:
| | Dream · dream-html — renders DTOs, names no domain type
152
Added:
| |
153
Added:
| hito.import Hevy_csv
154
Added:
| | third-party exports into Workout_import; no web framework
155
Added:
| |
156
Added:
| hito.persistence Sqlite_repo · Codec · Migrations · Bcrypt_hash
157
Added:
| | Caqti, the SQLite driver, serialization, hashing
150
158
v v
151
Removed:
hito.app Trainee · Repository · Password_hash (ports) · Service · Catalog
159
Added:
hito.app Trainee · Repository · Password_hash (ports) · Service · Catalog · Workout_import
152
160
| identity, the ports, recovery policy, Lwt — no database
153
161
v
154
Removed:
hito.core Exercise · Recovery · Prescription · Evidence · Progression
162
Added:
hito.core Exercise · Recovery · Prescription · Evidence · Progress
155
163
pure domain: no framework, no database, no serialization
156
164
#+end_src
157
165
158
166
The use case (=hito.app=) depends inward on =hito.core= only. It names no
159
167
database library, so the Dependency Rule at the persistence boundary holds by
160
168
construction: the compiler forbids the =Service= from reaching Caqti. The
161
Removed:
adapters live one layer out in =hito.persistence=, which depends on =hito.app=
162
Removed:
and implements its =Repository= port.
169
Added:
adapters live one layer out — =hito.persistence= implements the =Repository=
170
Added:
port, =hito.import= turns a vendor export into the import use case — and each
171
Added:
depends on =hito.app=, never the reverse. =hito.web= links the app, the import
172
Added:
adapter, and the presenter; it never links persistence. =bin/main.ml= is the
173
Added:
composition root, the one file that names both an adapter and the use case it
174
Added:
plugs into.
163
175
164
176
The core is five modules, unwrapped (=wrapped false=), so they are referred to
165
177
bare: =Prescription.Stimulus=, =Evidence.Workout=, =Evidence.Log=. It has no
@@ -230,30 +242,38 @@
230
242
=Observation= is the canonical insight input: a catalogued exercise, a confirmed
231
243
effort, and a performance time. Native workouts produce observations through
232
244
=Log=. A cleaned import can produce the same type without pretending that it had
233
Removed:
a prescription or clearance. =Progression= judges observations and therefore
245
Added:
a prescription or clearance. =Progress= judges observations and therefore
234
246
does not know their provenance.
235
247
236
Removed:
=Feedback= is a typed vocabulary for reported signals — sleep, appetite,
237
Removed:
readiness, motivation, difficulty, pain, injury, insufficient preparation — with
238
Removed:
duplicate categories rejected. Leveled signals carry a five-point score, 1 (very
239
Removed:
poor) to 5 (very good). These are wellness self-reports, never a measure of set
240
Removed:
intensity. The application stores reports independently of workouts, and the web
241
Removed:
tier records them through a sequential optional flow. The flow keeps its partial
242
Removed:
answers: Back and Skip preserve them, Close cancels the flow, and a report may be
243
Removed:
saved as a partial list. The logbook draws a small inline-SVG time graph of the
244
Removed:
leveled scores, one line per factor, once two reports carry a factor.
248
Added:
=Feedback= is a typed vocabulary for the two self-reports HD1 grounds —
249
Added:
motivation (chapter 6, and a named overtraining symptom) and perceived
250
Added:
difficulty ("the weights feel much heavier than usual") — with duplicate
251
Added:
categories rejected. Each carries a relative reading, not an absolute score:
252
Added:
=Worse_than_usual=, =As_usual=, or =Better_than_usual=. Three values resist the
253
Added:
false precision of a finer scale, in the spirit of HD1's refusal of spurious
254
Added:
measurement gradations, and =Worse_than_usual= is the concerning direction for
255
Added:
both. HD1 gauges recovery by time and by effect, so no wider battery of wellness
256
Added:
proxies is modeled. The application stores reports independently of workouts,
257
Added:
and the web tier records them through a sequential optional flow. The flow keeps
258
Added:
its partial answers: Back and Skip preserve them, Close cancels the flow, and a
259
Added:
report may be saved as a partial list. The logbook draws a small inline-SVG time
260
Added:
graph of the readings, one line per factor, once two reports carry a factor.
245
261
246
Removed:
** Progression — the judgment
262
Added:
** Progress — the judgment
247
263
248
Removed:
=assess= reports =Progressing= or =Stalled= from observations, or raises
249
Removed:
=Insufficient_data=. =stall_window= is 14 days. =remedy= returns =Some= only for
250
Removed:
a stall, and only ever subtracts. =judge_load= reads a single effort against its
251
Removed:
prescribed band: below the floor is =Too_heavy=, twelve reps triggers
252
Removed:
=Increase=, otherwise =Hold= — the ceiling of the authored band is deliberately
253
Removed:
not a trigger, since =load_increase_trigger= is twelve regardless of the range.
254
Removed:
=diagnose= reports habits HD1 names as causes of overtraining: recorded volume
255
Removed:
beyond the prescription, extensions on every stimulus, and training
256
Removed:
under-recovered.
264
Added:
=assess= reports =Progressing=, =Stalled=, or =Decompensating= from
265
Added:
observations, or raises =Insufficient_data=. =stall_window= is 14 days. Once the
266
Added:
window passes without an advance, the latest effort is read against the
267
Added:
reference — the peak reached, or the first observation when the record never
268
Added:
advanced: strength below it is decompensation (HD1: "lose strength and mass"),
269
Added:
strength holding at it is a stall. =remedy= returns =Some= for either, the same
270
Added:
corrective HD1 gives both, and only ever subtracts. =judge_load= reads a single
271
Added:
effort against its prescribed band: below the floor is =Too_heavy=, twelve reps
272
Added:
triggers =Increase=, otherwise =Hold= — the ceiling of the authored band is
273
Added:
deliberately not a trigger, since =load_increase_trigger= is twelve regardless
274
Added:
of the range. =diagnose= reports habits HD1 names as causes of overtraining:
275
Added:
recorded volume beyond the prescription, extensions on every stimulus, and
276
Added:
training under-recovered.
257
277
258
278
* Boundaries
259
279
@@ -342,8 +362,10 @@
342
362
** Workout import
343
363
344
364
Native Hevy CSV import is an application use case, not a core entity. The
345
Removed:
=Hevy_csv= web adapter parses the strict 14-column native format and retains the
346
Removed:
complete source text. The upload supplies an explicit UTC offset because Hevy
365
Added:
=Hevy_csv= adapter in =hito.import= parses the strict 14-column native format
366
Added:
and retains the complete source text. It depends on the app and the CSV parser
367
Added:
only, so a client other than the web tier can import without linking Dream. The
368
Added:
upload supplies an explicit UTC offset because Hevy
347
369
exports local wall time without a timezone.
348
370
349
371
A =Workout_import.batch= owns provisional workouts, ordered warnings, confirmed
@@ -364,7 +386,7 @@
364
386
a batch never deletes promoted evidence. The original CSV remains immutable so
365
387
a later importer can reprocess rows that the current version dropped.
366
388
367
Removed:
Promoted observations feed =Progression= with native observations. Provenance
389
Added:
Promoted observations feed =Progress= with native observations. Provenance
368
390
does not change judgment. The latest promoted workout also gates recovery with
369
391
the routine's 48-hour training interval when it is newer than native evidence.
370
392
Imports never advance routine rotation, and diagnostics remain native-only
@@ -378,7 +400,7 @@
378
400
WP -->|start, needs clearance| W[Evidence.Workout]
379
401
S[Evidence.Stimulus] --> W
380
402
W -->|finish| L[Evidence.Log]
381
Removed:
L -->|dated observations| P[Progression]
403
Added:
L -->|dated observations| P[Progress]
382
404
L -->|elapsed| RC[Recovery]
383
405
RC -->|clearance| W
384
406
P -.->|remedy| R
@@ -389,7 +411,7 @@
389
411
interprets.
390
412
391
413
The dashed arrow is the one link nothing
392
Removed:
automates. =Progression.remedy= returns a remedy. No code applies it
414
Added:
automates. =Progress.remedy= returns a remedy. No code applies it
393
415
to a routine. Acting on it belongs to a tier that does not exist yet,
394
416
and inventing one silently would violate the rule that plans are
395
417
authored.
@@ -524,8 +546,10 @@
524
546
dune build && ./_build/default/bin/main.exe # http://localhost:8080/
525
547
#+end_src
526
548
527
Removed:
=bin/main.ml= opens the store, applies migrations, seeds the single
528
Removed:
account, and starts Dream. The socket binds *loopback only*.
549
Added:
=bin/main.ml= opens the store, applies migrations, runs =Seed.single_user= for
550
Added:
the one pre-seeded account, and starts Dream. The seed lives in =bin/seed.ml=,
551
Added:
beside the root rather than in a library: it names a concrete adapter and is
552
Added:
temporary until public registration returns. The socket binds *loopback only*.
529
553
530
554
| Variable | Purpose | Default |
531
555
|----------------------+---------------------------------------+-------------------------|
@@ -625,7 +649,7 @@
625
649
626
650
* Deferred
627
651
628
Removed:
- =Progression.remedy= is computed but never applied to a routine —
652
Added:
- =Progress.remedy= is computed but never applied to a routine —
629
653
the dashed arrow above.
630
654
- Equipment granularity, so a suggested load is one a bar can actually
631
655
hold.
@@ -639,13 +663,20 @@
639
663
* Software boundaries
640
664
641
665
The build graph enforces the dependency rule. =hito.core= has no library
642
Removed:
dependencies. =hito.app= depends only on =hito.core= and =lwt=. Persistence
643
Removed:
adapters depend on the app and core. =hito.web= depends inward on the app and
644
Removed:
core, and on =hito.presenter= for rendering.
666
Added:
dependencies. =hito.app= depends only on =hito.core= and =lwt=. The persistence
667
Added:
and import adapters depend on the app and core, and on their own external
668
Added:
library (Caqti, csv) — never on each other or on the web. =hito.web= depends
669
Added:
inward on the app, the import adapter, and core, and on =hito.presenter= for
670
Added:
rendering. It never links persistence.
645
671
646
672
=hito.presenter= contains routes, view models, and pages. It depends only on
647
673
Dream and dream-html. It cannot name a core entity, a service, or a repository
648
674
adapter. This compiler boundary replaces a source-scanning architecture test.
675
Added:
676
Added:
User-facing wording has one owner, =Handlers.Present= in =hito.web=. It matches
677
Added:
every core and service error variant itself; the core's =pp= functions are
678
Added:
debugging output and are never rendered as copy, so a wording change cannot
679
Added:
reach the entity layer.
649
680
650
681
Inside each tier, the large renderer and controller are split into feature
651
682
modules behind a stable façade. =Pages= re-exports the per-feature page modules