Skip to content

Architecture Decision Records, Personal Training OS

Artifact: ADRs (Phase 3 backfill), formalizes design decisions D1, D5 from 02-design-schema.md. Date: 2026-07-04

An ADR records one significant decision: its context, the decision, and its consequences. (Convention is one file per ADR; consolidated here for brevity, split later if the log grows.) Status values: Proposed · Accepted · Superseded.


ADR-0001, Equipment as a constrained multi-select, not a database

Status:Superseded by ADR-0006 (2026-07-04) · Traces: EQP-005, DES-D1 (Retained for history. The decision below was reversed, see ADR-0006.) Context: The requirements mandate exactly 15 databases; Equipment is not one. Templates must never assume unavailable gear. Decision: Model equipment as a multi-select whose options are restricted to the available + pending set, so unavailable gear (barbell/kettlebell/net/medicine ball) is unselectable. Consequences: EQP-005/006 satisfied by construction; no validation rule needed. Trade-off: no per-item equipment metadata (purchase date, availability transitions). Revisit if inventory tracking is wanted (a 16th database), a candidate change request.

ADR-0002, Times stored as text H:MM:SS with optional numeric seconds

Status: Accepted · Traces: RACE-003, DES-D2 Context: Notion has no native duration type; Goal/Result times need display and occasional math. Decision: Store as text H:MM:SS, plus an optional numeric seconds field where computation (pace, PR comparison) is needed. Consequences: Human-readable and math-capable; slight duplication between the two fields.

ADR-0003, Two hockey-game relations (Team A, Team B)

Status: Accepted · Traces: HOK-002, DES-D3 Context: Notion relations target one database; the two teams' games live in two separate source-of-truth databases. Decision: Training Sessions carry two relation properties, disambiguated by a Team select. Consequences: Faithful to HOK-002 (no merging source DBs); a single unified relation is impossible without consolidating the sources.

ADR-0004, Metrics / Check-ins as daily-wide rows

Status: Accepted · Traces: MET-001, DES-D4 Context: Manual daily check-ins vs. high-frequency wearable streams have different shapes. Decision: One wide row per person per day (weight, HR, HRV, sleep, …), optimized for manual entry. Consequences: Ergonomic for v1 manual capture; a long-format companion table is deferred until a real wearable integration lands. Update (2026-07-08): that trigger has now fired. Garmin daily wellness metrics (Increment 2b) and Withings weight (Increment 2c) are live, both writing to Metrics/Check-ins. The daily-wide model held for everything that shipped (upsert is one row per person per day). The deferred long-format companion-table decision is therefore now revisitable, should sub-daily wearable streams warrant it. This note records a consequence; it does not change the original decision.

ADR-0005, Compound seed statuses kept literal

Status: Accepted · Traces: RACE-003, RACE-004, DES-D5 Context: The seed data specifies compound statuses ("Training / Registered", "Registered / Planned"). Decision: Preserve those compound labels as literal select options alongside atomic options. Consequences: Seeds match the requirements exactly; the option list carries some redundancy.

ADR-0006, Add an Equipment database (supersedes ADR-0001)

Status: Accepted · Date: 2026-07-04 · Supersedes: ADR-0001 · Traces: EQP-001, EQP-002, EQP-004, EQP-005 Context: ADR-0001 modeled equipment as a constrained multi-select to stay within the original 15-database list. On review the PO chose to track equipment as first-class inventory, its own database with availability that changes over time (e.g. the stickhandling ball going Pending → Available), per-item notes, and relations from the things that use it. Decision: Add an Equipment database (a 16th database). Fields: Equipment (title), Category, Availability (Available / Pending / Not available), Location / Notes. Exercise Library, Hockey Drills, and Workout Templates relate to Equipment (relation) instead of a constrained multi-select. A workout/drill's "doable now?" derives from whether all its related equipment is Available (rollup/formula). Consequences: Richer than the multi-select, tracks pending→available transitions and enables a real "currently doable?" signal. Cost: one more database + relations to maintain, and the database count is now 16. EQP-005/006 are satisfied by the Availability field + a doability rollup rather than by omitting options from a list. Requirements impact: the "exactly 15 databases" framing in D1 is void; ARCH-001 ("the full set") still holds, the full set is now 16.

ADR-0007: Nutrition ingest via MyFitnessPal export; Nutrition Log schema extension

Status: Accepted · Date: 2026-07-09 · Traces: NUT-001..004, the deferred MyFitnessPal item (MoSCoW), Increment 4 Context: MyFitnessPal has no personal API and its ToS bars automated site access (an unofficial scraper was built and abandoned). Triangulation across every app MFP connects to found no personal, headless API exposing the full macro set (Garmin = calories only; TrainingPeaks / Google Fit REST = macros but partner-gated / EOL 2026; Apple Health / Health Connect = on-device; Withings = no nutrition), see discovery/myfitnesspal/feasibility-brief.md section 10. The one compliant path is MFP Premium's "Download Your Data" emailed CSV export. Decision: Ingest the emailed "Meal Level Nutrition Details" CSV, capturing every column. The export is one row per logged food (several per meal, no food name), so foods are summed into per-meal totals (one Notion row per date + meal, nothing dropped), via automation/mfp_email_to_notion.py: header-driven mapping, aggregate by (date, meal), idempotent upsert keyed on Source ID = mfp-<date>-<meal>, dry-run default. Extend the Nutrition Log database with 14 new properties (Meal, Time, Saturated / Polyunsaturated / Monounsaturated / Trans Fat, Cholesterol, Sodium, Potassium, Sugar, Vitamin A / C, Calcium, Iron), reusing the existing Calories / Fat / Carbs / Fiber / Protein / Notes and the automation-ready provenance fields. Decision (trigger): the export tap stays manual (about 10 seconds); automating it via a headless browser is ToS-barred and rejected. Everything after the tap is automated by the Gmail relay (IMAP + App Password), wired into the daily job and activated 2026-07-09 (the App Password secret was provisioned; the relay ran end-to-end and labeled the email done). See MFP-SETUP.md. Consequences: Full-fidelity nutrition (all macros incl. fat sub-types, plus micronutrients and per-meal timestamps) with no MFP site access (ToS-clean). Nutrition Log grows to about 30 properties. First ingest 2026-07-09 loaded 71 meal rows (aggregated from 115 food-level export rows) across 24 days, every macro and micro column populated on all rows, all linked to the Primary User, distinct keys, idempotent on re-run. Not yet in the formal V&V suite (operational evidence only). Each export is a rolling window (this one 30 days), so re-imports reconcile and gaps backfill on the next export.

ADR-0008: Portfolio frozen at the closure baseline; generator retired

Status: Accepted · Date: 2026-07-17 · Traces: G7 closure; the published delivery record (this site)

Context: The published portfolio was created by a scrub script (scripts/build_portfolio.py) that derived it from the private delivery record, replacing personal specifics with generic ones. The script's docstring promised a "generated, git-ignored derivative rebuilt on every docs build." As built, none of that was true: the pages were committed, nothing invoked the script, and the published copies had been hand-polished to meet the house style (no em or en dashes), so a regeneration could not pass CI. Meanwhile the private record kept moving after go-live (the increment verification run of 2026-07-10, the RAID as-built reconcile, the closure amendments), so the public case study silently lagged the record it exists to present. A portfolio-wide drift audit flagged the underlying fault: the generator and the hand edits both claimed to own the same files, which is the one state that is always wrong. Either the generator owns the artifact and is enforced in CI, or humans own the artifact and the generator goes.

Decision: This project is closed, so the artifact freezes and the generator goes. One final controlled regeneration from the private sources first: the scrub list completed (location shorthand it missed), the closure pages brought under the same scrub, the house-style dash conversion applied mechanically, and the personal-data pass re-run on the full output. That brings every published page to the closure baseline, including the post-release increments and their formal verification. Then scripts/build_portfolio.py is deleted. From here portfolio/ is hand-maintained: a frozen snapshot of the closure baseline, changed only by pull request through the existing house-style and state-drift gates. The private record under pm/ remains the single source of truth for history; this site is a projection of it as of closure, not a mirror.

Consequences: The docstring's promise and reality now agree, by deleting the promise. No idle machinery remains to rot or to mislead a future maintainer into running it; git history keeps the generator if the project ever reopens. The landing page states the baseline this site reflects. The accepted cost: if the private record is ever amended again, this site will not follow automatically. That is not a defect of the freeze; it is the freeze.