0010 — Exercise naming and catalogues
Status: Accepted · Date: 2026-08-28
Context
Two capabilities were asked for, and building them one exercise at a time made the feature feel more complicated than what it does:
- Push. An organisation names movements its own way, adds its own coaching detail, and its athletes simply see that — without acting, deciding, or knowing the mechanism exists.
- Pull. A coach discovers what another organisation published and follows the whole catalogue, rather than adopting movement by movement.
The shipped implementation models neither. It models individual namings: publish one, adopt one, reset one, bulk-adopt several. The result is that the user manipulates hundreds of atoms while thinking about one thing — "our vocabulary" — and that sharing chrome ends up attached to every row, because there is no object for it to hang on.
A live probe (tests/live/sharing/30-naming-inheritance.live.test.ts, backdev,
2026-08-28) also settled a question the client could only guess at. Author and
reader published different namings for the same definition and read the same
shared template item:
| Reader | exerciseName delivered | resolved from |
|---|---|---|
| the author, on their own template | ZZ-Author-Naming | the author's row |
| a reader with their own naming | ZZ-Reader-Naming | the reader's row |
| a reader without one | null | — |
The server already resolves naming per reader. Nobody inherits foreign words, and the author's naming never travels over that field.
Decision
1. One precedence ladder, resolved by the server
For any movement exactly one naming applies to a reader, from the highest occupied rung:
- My own naming — I renamed it, or I chose one (server, shipped)
- My organisation's naming — set by a coach/owner for their scope (missing)
- A catalogue I follow (missing)
- My own exercise / the stock catalogue name (client)
- Whatever anyone published for this definition (client)
The server resolves it, not the client: three clients cannot reimplement one rule without drifting, and the tracking app would have to reproduce it offline from cache. Rung 1 beats rung 2 — someone who deliberately renamed a movement keeps their name when the organisation sets a different one. A bulk action must never silently take away a naming a person chose.
2. A catalogue is an object, not a pile of namings
Three nouns — my catalogue, catalogues I follow, my organisation's catalogue — and four verbs: rename, set my catalogue's reach, follow / unfollow, set for my organisation.
Renaming a movement adds it to my catalogue; the catalogue carries the reach setting, once, instead of a visibility control on every exercise. Adopting stops being a distinct gesture: taking someone's word is renaming, and rung 1 is where it lands. Bulk adopt is replaced by following.
3. The name is identity; the detail is instruction
- Name — the reader's wins, silently. Two names for one movement is the problem the feature exists to remove, so a shared workout is displayed in the reader's vocabulary with no second line and no comparison affordance.
- Detail — the author's travels, visibly. "3s pause at the chest, our standard" is a prescription attached to that workout, not a property of the movement. It is the main reason an organisation curates at all.
4. Foreign words are reachable by search, not by display
A coach saying a word the athlete has never seen is a findability problem.
matchesExerciseSearch already takes extraTerms for exactly this — other
people's names for the same movement — so the word is typeable without costing
a pixel of display. This is why (3) can afford to be silent.
5. Provenance is one line, and only when the name is not yours
Amended 2026-08-31 — the exercise LIST carries none of it. The line moved out of the table entirely; whose words a movement wears is now said only where it can also be changed: the exercise drawer and the Catalogues drawer. Two surfaces answering the same question is how they came to disagree — the table read one flag and the drawer another, and a coach saw "Named by Team Enode" in one and an offer to publish those same words in the other. The rule below still holds wherever provenance IS shown; there is simply one fewer place.
The exercise detail states where a name came from — the shipped string is
Named by {name}, and Named by someone else when the backend delivers no
author name — only when the naming in force belongs to someone else. Own
namings and the plain catalogue name say nothing. No badge, no state to
dismiss, nothing an athlete has to interpret.
Consequences
-
The per-exercise visibility control, adopt/reset as a primary gesture, and the bulk-adopt action are all superseded by (2). They stay until the catalogue object exists server-side, then shrink to a name field plus one sentence.
-
Athletes see none of it. The
writeExercisegate stays as the divider. -
workoutItemDetailLinessuppresses a published detail as soon as the reader resolves a name of their own — nearly always, since a seeded account owns ~187 exercises. Correct at rung 5 (the description cache is keyed by definition alone, so an ungated read would print a stranger's qualifier under the reader's own rows), wrong at rung 2. It is unblocked by the server ask below, not by loosening the client. -
Amended 2026-08-31 — on a shared workout item the AUTHOR's word titles the row. Decision (3) said the name is identity and the reader's wins silently. The backend resolves a shared item's name against the description the item points at, on the argument that a template is the author's programming, and we accept that: a coach reading a colleague's template is reading their prescription. What (3) was protecting is kept by a different means — the reader's own word is shown beneath the title (
Your “…”), never hidden, so two names for one movement still cannot cause a mix-up. The silence rule survives where it started: in the viewer's own library their word still wins, and nothing is annotated there. -
Amended 2026-08-28 — the author's detail is shown BESIDE the reader's own, not instead of it. (3) said the author's detail travels; it did so by replacing the reader's line, which fails when the two are in different languages: a Spanish cue then leaves a reader with no line they can read. Both render, the author's attributed (
From {name}), collapsing to one when they say the same thing. The name half of (3) is unchanged — identity is still silent, and no second name is ever shown. -
A publisher is a USER, and (2)'s catalogue object ships as a client-side grouping. Everything is a user in this product, so "my organisation's catalogue" has no separate entity to hang on; a catalogue is one user's namings, grouped out of the discovery feed. Follow / unfollow / catch-up are derived from
isActiveand materialised as oneadoptper row, because there is no subscription endpoint.docs/sharing.md— "Catalogues: a publisher is a user" — records what that costs and what the surfaces say about it. -
Server asks, in
handoffs/SHARING_SERVER_HANDOFF.md: on a foreign item,exerciseDetailshould carry the author's detail whileexerciseNamekeeps the reader's name; plus the catalogue as an object (reach, follow, set-for-scope) replacing the per-description asks. -
Amended 2026-09-01 — following a catalogue takes the publisher's MOVEMENTS too, with no second switch.
adoptExercisesshipped as an off-by-default toggle on the subscription, on the argument that "I read your words" and "put your programme in my library" are different consents. The default follow therefore only covered the intersection of two libraries: forty published movements against a follower who owns twelve renamed twelve things and did nothing for the rest, on a card that said "reading their 40 descriptions". And the write turned out to be reversible — unfollow removes the minted movements that were never trained and keeps the ones that were (live-verified, 187 → 188 → 187), so the irreversible bulk import the separate consent guarded against does not exist. The client now sendsadoptExercises: trueon every subscription. The consent is still stated, just not clicked: an unfollowed card counts the movements a follow would add before it is pressed. What this costs is the reader who wants somebody's wording without their library — they get both, and unfollow to undo. -
Amended 2026-09-01 — the per-movement pick is back, and an adopt outranks your own naming. The simplification made "whose words do I read" a decision per PERSON and left renaming as the only per-movement move. That covers "their word is wrong for me" and not "I named this one myself, but for THIS movement I want theirs" — renaming authors your own words, so you stop receiving the author's later corrections, and it cannot pick up a video or a cue in a language you do not speak.
adoptdoes exactly that job and was still deployed with nothing calling it. The blocker was the assumed ladder (own at rung 1 would make the button a lie); measured, it is the other way round — an explicit adopt beats the caller's own description, andunadoptrestores it (tests/live/sharing/90-pick-one-movement.live.test.ts). So rung 1 is "the description I last chose", not "my own". Decision (2)'s alternatives list returns as one collapsed disclosure with a Use this one verb, and browse rows now say why they are not in use instead of showing no chip at all.
Alternatives considered
- Show "the author calls this X" inline. Rejected: eight items means eight extra lines, and most differ only by locale, where the second name says nothing. Search covers the real need without the noise.
- The organisation's naming wins over the athlete's. Rejected: more consistent within a team, but it silently overwrites a name someone deliberately chose.
- Resolve the ladder client-side. Rejected: portal, tracking web and tracking native would each carry the rule, and it would have to survive offline in the cache.