mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 05:12:14 +00:00
docs(ios-handoff): E-Closeout — recommended flow + research pointers
Two new files closing the v1.4.25 / v1.5 iOS handoff doc-pack. 09-recommended-flow.md (~6000 tokens) - First-time setup recipe (clone, dev server, API token, Simulator pointing) - Contract-diff loop (openapi:check + Swift codegen regen) - Touch-disjoint discipline for parallel iOS sub-agents - Marc-Voice commit-message style guide with examples + counterexamples - Marathon-pattern handoff section (non-negotiables, pre-flight rule, research-first) - Pre-release Quality Gate checklist (8 reviewers + reconcile pass, modelled on W21) - Mock vs hit-live test strategy (unit / integration / e2e split) - CI workflow surfaces (server-side vs iOS-side, the cross-side handoff) - 10 iOS-only gotchas the server log never sees (Simulator HealthKit, Withings deep-link return, RESEARCH_MODE_DISCLAIMER_VERSION drift, SSE streaming via URLSession.bytes, idempotency-key lifecycle, etc.) 10-research-pointers.md (~3500 tokens) - Ecosystem research index (Apple Health, GLP-1, wearables comparison, Withings) - UX + design research index (health-score provenance, insights sub-pages, source-priority two-axis, zielwerte redesign) - Feature-specific research (W8d enums, W14b onboarding, W14c native Coach prompts, W16b workouts, W16c PR detection, W17b-c Withings v2) - W21 review findings index with what's still pending in v1.4.26 - W10 baseline (historical context) - Pointer to .planning/phase-W21-reconcile-plan.md as the canonical post-W21 inventory + the 7 Fix-* surface map - Research-first directive (which feature classes require a research note before code lands) + the format convention for new notes
This commit is contained in:
1 parent
24d88858e9
commit
b4a98ef38e
2 files changed
+991
No files matched your search
@@ -0,0 +1,622 @@
|
||||
---
|
||||
file: 09-recommended-flow.md
|
||||
purpose: The iOS-side workflow recipe — first-time setup, contract-diff loop, parallel-dispatch discipline, pre-release Quality Gate checklist, mock-vs-live test strategy, CI surfaces, and the iOS-only gotchas that the server doesn't see.
|
||||
when_to_read: Day 1 of iOS work. Re-read before every PR that touches a server-bound contract, before every parallel-dispatch session, and before tagging any v1.5.x release.
|
||||
prerequisites: 00-philosophy.md, 01-repo-tour.md, 03-api-contracts.md, 08-locked-contracts.md
|
||||
estimated_tokens: ~6000
|
||||
version_anchor: v1.4.25 / sha 49f71c92
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
The iOS app is a thin client of a strict-Zod server, so the day-to-day workflow centres on three loops: a contract-diff loop that catches drift before it lands, a parallel-dispatch loop that lets multiple iOS sub-agents work in touch-disjoint slots without merge pain, and a Quality Gate loop that runs eight reviewers in parallel against `develop` before the release-merge to `main`. Mocks belong in unit tests, testcontainers in integration tests, and a staging Coolify instance in e2e. Everything else here is the why, the recipes, and the gotchas.
|
||||
|
||||
## Decision tree — what to do now
|
||||
|
||||
| Situation | First action |
|
||||
| --- | --- |
|
||||
| Cold-start, never run the dev server | § 1 First-time setup |
|
||||
| Touching anything server-bound (DTO, endpoint URL, query params) | § 2 Contract-diff loop |
|
||||
| Spawning more than one iOS sub-agent in parallel | § 3 Touch-disjoint discipline |
|
||||
| Tagging a v1.5.x release | § 4 Marathon-pattern handoff + § 5 Quality Gate |
|
||||
| Writing a new test | § 6 Mock vs live |
|
||||
| Looking at red CI on a PR | § 7 CI workflow + § 8 iOS-only gotchas |
|
||||
|
||||
---
|
||||
|
||||
## § 1 — First-time setup
|
||||
|
||||
### 1.1 Clone + bootstrap
|
||||
|
||||
```bash
|
||||
# from a fresh shell
|
||||
git clone git@github.com:MarcBombeck/HealthLog.git
|
||||
cd HealthLog
|
||||
git checkout develop # daily work goes here, never on main
|
||||
nvm use 20 # Node 20+ — required by Next.js 16
|
||||
corepack enable # pnpm 10.31.0 ships via Corepack
|
||||
pnpm install
|
||||
pnpm db:generate # regenerate Prisma client
|
||||
```
|
||||
|
||||
### 1.2 Postgres + the dev server
|
||||
|
||||
```bash
|
||||
docker compose up -d postgres # Postgres 16 on 5432
|
||||
cp .env.example .env.local # then edit DATABASE_URL + SESSION_SECRET
|
||||
pnpm db:migrate:deploy # apply migrations 0001..0060
|
||||
pnpm dev # Next.js dev server on :3000 (Turbopack)
|
||||
```
|
||||
|
||||
Health-check the boot:
|
||||
|
||||
```bash
|
||||
curl http://localhost:3000/api/health
|
||||
# expect: {"data":{"status":"ok","worker":"running",...},"error":null}
|
||||
```
|
||||
|
||||
### 1.3 Issue an API token for the iOS Simulator
|
||||
|
||||
The iOS app uses Bearer auth. The fastest way to a working iOS build:
|
||||
|
||||
```bash
|
||||
# 1. Register a dev user (creates the cookie session)
|
||||
curl -X POST http://localhost:3000/api/auth/register \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"email":"ios-dev@example.com","username":"ios-dev","password":"correct-horse-battery-staple"}' \
|
||||
-c cookies.txt
|
||||
|
||||
# 2. Mint a wildcard-scope token (web Settings → API tokens does the same)
|
||||
curl -X POST http://localhost:3000/api/tokens \
|
||||
-H 'Content-Type: application/json' \
|
||||
-b cookies.txt \
|
||||
-d '{"name":"iOS Simulator dev","permissions":["*"],"expiresInDays":365}'
|
||||
# → returns { data: { token: "hlk_..." } }
|
||||
```
|
||||
|
||||
Drop the `hlk_*` value into the iOS Simulator app's Settings → API base URL + token. Default API base for the Simulator is `http://localhost:3000` — the iOS app's network layer reads it from `UserDefaults` so multi-host operators (self-hosters) can point at their own Coolify instance.
|
||||
|
||||
### 1.4 Apple Health permissions in the Simulator
|
||||
|
||||
The iOS Simulator **cannot read Apple Health data** — HealthKit on Simulator is a stubbed framework. Two options:
|
||||
|
||||
| Need | Path |
|
||||
| --- | --- |
|
||||
| Manual ad-hoc data | Tap the manual-entry sheet in the iOS app; it round-trips through `POST /api/measurements` (single) |
|
||||
| Realistic HealthKit data shapes | Test on a physical iPhone with seeded HealthKit data, OR seed Postgres directly with `POST /api/measurements/batch` against the seeded fixtures |
|
||||
|
||||
For batch-shape testing, the canonical fixture lives in `tests/integration/__fixtures__/apple-health-batch.json`. Replay it with:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/measurements/batch \
|
||||
-H "Authorization: Bearer $HLK_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "X-Client-Type: native" \
|
||||
-H "Idempotency-Key: $(uuidgen)" \
|
||||
--data-binary @tests/integration/__fixtures__/apple-health-batch.json
|
||||
```
|
||||
|
||||
### 1.5 STOP HERE if…
|
||||
|
||||
| If you only need… | …skip to… |
|
||||
| --- | --- |
|
||||
| To call ONE endpoint and verify the wire shape | `03-api-contracts.md` § matching route |
|
||||
| To wire login + refresh in Swift | `05-auth-flows.md` § 2 |
|
||||
| To debug a 422 from a batch | `08-locked-contracts.md` § 2 |
|
||||
|
||||
---
|
||||
|
||||
## § 2 — The contract-diff loop
|
||||
|
||||
Every iOS PR that touches an endpoint, a DTO, a query string, or an enum value MUST verify the contract before merge. Drift hides until prod.
|
||||
|
||||
### 2.1 The 4-step loop
|
||||
|
||||
```
|
||||
1. git fetch origin && git rebase origin/develop # absorb latest server changes
|
||||
2. pnpm openapi:check # diff Zod registry vs committed YAML
|
||||
3. (if 2 fails) pnpm openapi:generate # regenerate the YAML, commit if intentional
|
||||
4. Regenerate iOS Swift codegen from docs/api/openapi.yaml
|
||||
```
|
||||
|
||||
### 2.2 What `openapi:check` actually does
|
||||
|
||||
```typescript
|
||||
// from scripts/check-openapi.ts:6
|
||||
// Hard-fails on drift since v1.4.25 — the Zod registry is the
|
||||
// source of truth for the public API contract that the v1.5 iOS
|
||||
// Swift codegen consumes.
|
||||
```
|
||||
|
||||
Two outcomes:
|
||||
|
||||
| Result | What happened | Action |
|
||||
| --- | --- | --- |
|
||||
| Exit 0 | Zod registry == committed YAML | Safe; proceed |
|
||||
| Exit 1 with diff | A server change drifted the Zod schemas without regenerating | Run `pnpm openapi:generate`, review the diff, commit if the change is intentional. If the diff includes a breaking shape change, raise it before merging |
|
||||
|
||||
### 2.3 Swift codegen recipe
|
||||
|
||||
```bash
|
||||
# On the iOS side, mirror the server YAML into Swift
|
||||
swift run swift-openapi-generator generate \
|
||||
--output-directory Sources/HealthLogAPI/Generated \
|
||||
--config openapi-generator-config.yaml \
|
||||
../HealthLog/docs/api/openapi.yaml
|
||||
```
|
||||
|
||||
Codegen-driven DTOs replace hand-written `Codable` structs. If a field that the iOS UI relies on disappeared from the regenerated source, the build breaks — exactly the failure mode you want.
|
||||
|
||||
### 2.4 What to do when the diff is intentional
|
||||
|
||||
```
|
||||
1. Open the server PR that introduced the drift.
|
||||
2. Confirm the change is additive (server policy: additive across minor versions).
|
||||
3. If the change is breaking (rare): coordinate the iOS rev-lock with the server tag.
|
||||
4. Commit the regenerated YAML in the SAME PR that ships the iOS-side change.
|
||||
```
|
||||
|
||||
Marc's rule: never ship an iOS PR that depends on un-merged server work. The server tag lands first; the iOS PR rebases against it.
|
||||
|
||||
### 2.5 Where the contract-diff fire alarms go off
|
||||
|
||||
| Symptom | Most likely cause | Fix |
|
||||
| --- | --- | --- |
|
||||
| iOS 422 with `meta.errorCode: "validation_failed"` on a field your DTO sends | iOS DTO has a key the server's Zod-strict parser rejects | Regenerate Swift codegen; remove the stale key |
|
||||
| iOS 422 on a field your DTO does NOT send but the server now requires | Server added a required field; codegen catches it | Regenerate; populate the new field |
|
||||
| iOS 200 with wrong-shaped data | Enum value changed spelling | Regenerate; the Swift enum now reflects new spelling |
|
||||
| CI red on `openapi-drift` | A server hand-edit bypassed the registry | Server-side fix only — never patch the YAML by hand |
|
||||
|
||||
---
|
||||
|
||||
## § 3 — Touch-disjoint discipline (parallel iOS sub-agents)
|
||||
|
||||
Marc's release pattern depends on dispatching multiple sub-agents in parallel against touch-disjoint surfaces. The iOS workspace inherits the same convention.
|
||||
|
||||
### 3.1 The rule
|
||||
|
||||
> Two sub-agents working at the same time MUST NOT touch the same file. Period.
|
||||
|
||||
If two slots both edit `Sources/HealthLogCoach/CoachView.swift`, you get merge conflicts that compound across the wave. The dispatcher pre-divides surfaces so this is impossible.
|
||||
|
||||
### 3.2 Wave decomposition recipe
|
||||
|
||||
```
|
||||
Goal: ship N independent iOS features in one session.
|
||||
|
||||
Step 1 — Inventory the work. List every file each feature touches.
|
||||
Step 2 — Find a partition. Group features so no file appears in two groups.
|
||||
Step 3 — Dispatch in parallel. Each slot gets one group + a touch list.
|
||||
Step 4 — Merge order. Slot 1 commits first, slot 2 rebases on slot 1, etc.
|
||||
```
|
||||
|
||||
When a clean partition isn't possible (e.g. two features both touch `AppShell.swift`), serialise those two — one waits.
|
||||
|
||||
### 3.3 Atomic-commit rule
|
||||
|
||||
Each slot ships **one commit per logical change**, never a "WIP" or a squashed mega-commit. The release surfaces the commits as line-items in the CHANGELOG; one commit per item makes the changelog readable.
|
||||
|
||||
| Acceptable | Not acceptable |
|
||||
| --- | --- |
|
||||
| `feat(coach): add streaming chat view` | `wip: coach + dashboard + auth` |
|
||||
| `fix(auth): refresh on 401` | `various fixes` |
|
||||
| `chore(deps): bump Alamofire to 5.10` | `update deps and refactor` |
|
||||
|
||||
### 3.4 Marc-Voice commit-message style guide
|
||||
|
||||
| Rule | Example |
|
||||
| --- | --- |
|
||||
| English, terse, professional | `fix(measurements): unblock batch ingest on 422` |
|
||||
| Use Conventional Commits prefixes | `feat`, `fix`, `chore`, `docs`, `refactor`, `test`, `perf`, `style`, `build`, `ci` |
|
||||
| Imperative mood — "add" not "added" | `feat(coach): add streaming SSE parser` |
|
||||
| Scope in parentheses, lowercase | `feat(insights): render dailyBriefing tile` |
|
||||
| No AI tells | NEVER `Co-Authored-By: Claude`, NEVER `--no-verify`, NEVER reference "agent / marathon / phase / wave / session" in the message |
|
||||
| No PII | Marc's name, health figures, BD-Zielbereich values, measurement counts never appear in commit messages |
|
||||
|
||||
Counterexamples that would fail a review:
|
||||
|
||||
```
|
||||
# BAD — agent-voice
|
||||
fix(coach): patch streaming bug (Claude-assisted)
|
||||
|
||||
# BAD — AI mention
|
||||
feat(insights): add briefing tile (generated via Claude Opus)
|
||||
|
||||
# BAD — marathon language
|
||||
feat(auth): wave-3 refresh-token rotation
|
||||
|
||||
# BAD — PII leak
|
||||
fix(targets): restore Marc's 140/90 BD-Zielbereich after migration
|
||||
|
||||
# GOOD
|
||||
feat(auth): rotate refresh tokens on use
|
||||
feat(coach): add SSE streaming view with provenance disclosure
|
||||
fix(targets): persist user target range after edit
|
||||
```
|
||||
|
||||
### 3.5 Branch model — `develop` is the daily target
|
||||
|
||||
```
|
||||
feature/* ──┐
|
||||
├──► develop ──► (release-merge via PR) ──► main ──► tag vX.Y.Z
|
||||
fix/* ──┘ │
|
||||
└──► CHANGELOG entry pinned to the tag
|
||||
```
|
||||
|
||||
The iOS app's GHCR-equivalent (App Store TestFlight) builds **from main only**. A dev push to develop never produces a TestFlight build — the GitHub Actions workflow trigger is `tags: 'v*'` on `main`.
|
||||
|
||||
---
|
||||
|
||||
## § 4 — Marathon-pattern handoff (for v1.5.x release sessions)
|
||||
|
||||
Marc-memory has a `release-marathon` skill that codifies the pattern. The iOS workspace inherits the rules.
|
||||
|
||||
### 4.1 The non-negotiables
|
||||
|
||||
| Rule | Why |
|
||||
| --- | --- |
|
||||
| NO `Co-Authored-By: Claude` trailer | Marc-Voice: every artefact reads as Marc's authorship |
|
||||
| NO `--no-verify` on `git commit` or `git push` | Pre-commit hooks (lint, typecheck, test) gate every commit |
|
||||
| NO `--no-edit` on `git rebase` | The `--no-edit` flag is not a valid rebase option |
|
||||
| NO direct push to `main` | `main` only accepts release-merge PRs from `develop` |
|
||||
| NO tagging by sub-agent | Marc tags after the release-merge PR lands |
|
||||
| NO destructive git commands (`reset --hard`, `push --force` to main, `branch -D`, `clean -f`) unless Marc explicitly asks | Risk of losing other slots' work |
|
||||
|
||||
### 4.2 Pre-flight rule
|
||||
|
||||
Before opening any iOS PR in a marathon session, read the handoff doc (this directory) first. The wave-0 read budget is roughly:
|
||||
|
||||
- `README.md` (this directory)
|
||||
- `00-philosophy.md`
|
||||
- `01-repo-tour.md`
|
||||
- `08-locked-contracts.md`
|
||||
- `09-recommended-flow.md` (this file)
|
||||
|
||||
Total ~15k tokens. Skipping this surface is the most common cause of merge-blocking review feedback.
|
||||
|
||||
### 4.3 Research-first for architecturally-new work
|
||||
|
||||
If the iOS feature is genuinely new architecturally (e.g. APNs payload routing, HealthKit observer-query topology, Withings deep-link return), spin up a research note under `.planning/research/` BEFORE writing code. Benchmark Apple Health / Withings / Oura first, propose an ecosystem-fit plan, then implement. Marc's rule from `feedback_research_before_complex_features`.
|
||||
|
||||
### 4.4 Phase reports
|
||||
|
||||
Every completed phase writes a report at `.planning/phase-W<n>-v1.5.<patch>-<slug>-report.md`. The report captures:
|
||||
|
||||
- Phase scope (the task description verbatim)
|
||||
- What landed (commits + files)
|
||||
- What deferred (with a one-line "why")
|
||||
- Test deltas (unit / integration / e2e numbers before vs after)
|
||||
- Open questions for Marc
|
||||
|
||||
These are internal — never published to `docs/` or CHANGELOG.
|
||||
|
||||
### 4.5 Decision-relay communication
|
||||
|
||||
When a sub-agent hits a decision boundary that needs Marc's input (a UI taste call, an MDR-adjacent feature, a breaking-shape proposal), it pauses and writes a one-paragraph decision-relay note. It does NOT silently pick a direction. Examples that always relay:
|
||||
|
||||
- "Should the Coach drawer use a SwiftUI `.sheet` or `.fullScreenCover`?"
|
||||
- "Should iOS surface the Research Mode chart inline or behind a Settings link?"
|
||||
- "Should refresh-token reuse trigger a silent re-login or a hard logout?"
|
||||
|
||||
---
|
||||
|
||||
## § 5 — Pre-release Quality Gate checklist (W21-pattern)
|
||||
|
||||
Marc's v1.4.25 W21 ran an 8-reviewer parallel pass before the release-merge. The iOS workspace mirrors it.
|
||||
|
||||
### 5.1 The eight reviewers
|
||||
|
||||
| Reviewer | What they check | Output |
|
||||
| --- | --- | --- |
|
||||
| **code-review** | Bug-spotting, error handling, off-by-ones, dead branches | `phase-W<n>-code-review-findings.md` |
|
||||
| **security** | Secret leaks, auth boundary, injection vectors, token storage | `phase-W<n>-security-findings.md` |
|
||||
| **design** | Touch targets ≥ 44×44 pt, contrast WCAG-AA, palette consistency | `phase-W<n>-design-findings.md` |
|
||||
| **senior-dev** | Architecture, layering, future-proofing, idiomatic Swift | `phase-W<n>-senior-dev-findings.md` |
|
||||
| **simplifier** | Dead code, redundant abstractions, over-engineered surfaces | `phase-W<n>-simplifier-findings.md` |
|
||||
| **product-lead** | User-facing acceptance criteria, copy quality, flow polish | `phase-W<n>-product-lead-assessment.md` |
|
||||
| **i18n-runtime-probe** | Hard-coded strings, locale-bundle parity, missing keys | `phase-W<n>-i18n-runtime-findings.md` |
|
||||
| **dead-code-scan** | Unused exports, unreferenced files, orphaned migrations | `phase-W<n>-dead-code-findings.md` |
|
||||
|
||||
### 5.2 The reconcile pass
|
||||
|
||||
After the 8 reviewers land, run a **reconcile** pass that:
|
||||
|
||||
1. Collates findings into severity buckets (C / H / M / L)
|
||||
2. Maps each must-fix finding to a touch-disjoint Fix-* surface
|
||||
3. Estimates commit count per surface
|
||||
4. Dispatches fix sub-agents in parallel against those surfaces
|
||||
|
||||
The reconcile plan lives at `.planning/phase-W<n>-reconcile-plan.md` and is the single canonical source for "what's still pending before tag". Example: `.planning/phase-W21-reconcile-plan.md` (v1.4.25's W21 reconcile, 63 must-fix findings → 7 Fix-* surfaces).
|
||||
|
||||
### 5.3 Severity rubric
|
||||
|
||||
| Severity | Rule |
|
||||
| --- | --- |
|
||||
| **Critical** | Ship-blocker. Security leak, MDR-line crossing, data loss. Always applied before tag |
|
||||
| **High** | Apply before tag unless a clean defer-rationale exists |
|
||||
| **Medium** | Apply if the file is already open in another Fix-* slot (cheap reuse) |
|
||||
| **Low** | Defer to the next patch unless trivially cheap |
|
||||
|
||||
### 5.4 Pre-tag final-pass checklist
|
||||
|
||||
Once Fix-* surfaces land:
|
||||
|
||||
```
|
||||
[ ] pnpm typecheck && pnpm lint && pnpm test && pnpm format:check && pnpm build (server)
|
||||
[ ] pnpm test:integration (server, testcontainers)
|
||||
[ ] pnpm e2e (server, Playwright)
|
||||
[ ] pnpm openapi:check (no drift)
|
||||
[ ] Swift codegen runs cleanly against docs/api/openapi.yaml
|
||||
[ ] xcodebuild test -scheme HealthLog (iOS unit + UI tests)
|
||||
[ ] PII-grep against CHANGELOG + release notes: no Marc / 140/90 / BD-Zielbereich
|
||||
[ ] AI-tell grep: no "Claude / agent / marathon / phase / wave / session" in user-facing artefacts
|
||||
```
|
||||
|
||||
If every box ticks, open the release-merge PR develop → main. Marc reviews + tags.
|
||||
|
||||
---
|
||||
|
||||
## § 6 — Mock vs hit-live test strategy
|
||||
|
||||
Three test layers, three different data realities.
|
||||
|
||||
### 6.1 The decision tree
|
||||
|
||||
| Need | Layer | Backing |
|
||||
| --- | --- | --- |
|
||||
| Test a single function | Unit | Mocked Prisma (`vi.mock("@/lib/db")`) |
|
||||
| Test a full HTTP path against a real DB | Integration | Testcontainers Postgres (`pnpm test:integration`) |
|
||||
| Test a user flow across multiple pages | E2E | Playwright against a Coolify staging instance |
|
||||
| Test the iOS app against the real server | E2E | iOS Simulator pointed at staging URL |
|
||||
|
||||
### 6.2 Unit tests — what to mock
|
||||
|
||||
```typescript
|
||||
// from a typical Vitest unit test
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
vi.mock("@/lib/db", () => ({ prisma: { measurement: { findMany: vi.fn() } } }));
|
||||
vi.mock("@/lib/auth/session", () => ({ requireAuth: vi.fn().mockResolvedValue({ user: { id: "u1" } }) }));
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- Mock Prisma; never hit a real DB
|
||||
- Mock external HTTP (Withings, Anthropic, OpenAI)
|
||||
- Stub time with `vi.useFakeTimers()` and pin to a specific ISO instant
|
||||
- Never mock Zod — schemas must validate against real input shape
|
||||
|
||||
### 6.3 Integration tests — testcontainers
|
||||
|
||||
```bash
|
||||
pnpm test:integration
|
||||
# Boots a fresh Postgres 16 container, runs migrations, spawns a test instance, runs every spec, tears down.
|
||||
```
|
||||
|
||||
These tests are the safety net against drift between Prisma client and the migrations. If `pnpm test` passes but `pnpm test:integration` fails, you have a schema-vs-client mismatch — almost always a migration rollback bug.
|
||||
|
||||
### 6.4 E2E — staging only
|
||||
|
||||
The iOS Simulator's e2e flow points at the Coolify staging URL (separate from production). Staging has:
|
||||
|
||||
- A fresh Postgres + restored backup
|
||||
- Marc's seeded test account
|
||||
- Real AI providers (Anthropic key in env)
|
||||
- Real Withings sandbox creds
|
||||
|
||||
Never run iOS e2e against production. The smoke difference matters only for the dispatcher cron schedule — staging cron schedules use a `Europe/Berlin` clone but disable the offhost-backup queue (it shouldn't touch S3).
|
||||
|
||||
### 6.5 iOS unit + UI tests
|
||||
|
||||
```bash
|
||||
# Unit tests — XCTest, mocked URLSession
|
||||
xcodebuild test -scheme HealthLog -destination 'platform=iOS Simulator,name=iPhone 15'
|
||||
|
||||
# UI tests — XCUITest against the iOS Simulator with a stubbed server
|
||||
xcodebuild test -scheme HealthLogUITests -destination 'platform=iOS Simulator,name=iPhone 15'
|
||||
```
|
||||
|
||||
Stub the server with a local `URLProtocol` subclass that intercepts every request. No physical server runs during iOS unit tests — the goal is to verify the iOS state machine in isolation.
|
||||
|
||||
---
|
||||
|
||||
## § 7 — CI workflow surfaces
|
||||
|
||||
Two CI surfaces matter: the server's GitHub Actions and the iOS-side GitHub Actions (or Xcode Cloud). They overlap deliberately.
|
||||
|
||||
### 7.1 What the server CI runs on every PR
|
||||
|
||||
| Workflow | Trigger | What it does |
|
||||
| --- | --- | --- |
|
||||
| `e2e.yml` | PR + push | Playwright against ephemeral Postgres |
|
||||
| `integration.yml` | PR + push | Vitest integration (testcontainers) |
|
||||
| `security.yml` | PR + daily | `pnpm audit`, secret scanning, OpenAPI drift gate (HARD-FAIL) |
|
||||
| `docker-publish.yml` | Tag `v*` on `main` only | Multi-arch GHCR image build |
|
||||
| `post-publish-verify.yml` | After GHCR build | Pulls the new image, runs health probes |
|
||||
|
||||
The hard-flip gate to watch: `pnpm openapi:check` inside `security.yml`. If it fails, the PR cannot merge — even with admin override, Marc's policy is to fix the drift instead.
|
||||
|
||||
### 7.2 What the iOS CI should run
|
||||
|
||||
```
|
||||
1. xcodebuild test -scheme HealthLog # unit + UI tests
|
||||
2. xcodebuild archive -scheme HealthLog -configuration Release # ship-able archive
|
||||
3. Swift codegen drift check (regenerate, diff, fail if differ)
|
||||
4. SwiftLint --strict
|
||||
5. (Optional) accessibility audit on UI tests via XCTest
|
||||
```
|
||||
|
||||
iOS CI does NOT need to run the server's tests. The contract is OpenAPI YAML; if YAML and Swift codegen agree, iOS doesn't care how the server tests itself.
|
||||
|
||||
### 7.3 The cross-side handoff
|
||||
|
||||
```
|
||||
Server PR → develop → CI green → review → merge
|
||||
Server tag → main → CI green → GHCR build → Coolify staging
|
||||
Server tag → main → docs/api/openapi.yaml frozen on the tag
|
||||
│
|
||||
▼
|
||||
iOS PR → develop → CI green (Swift codegen against frozen YAML)
|
||||
iOS PR → develop → review → merge
|
||||
iOS tag → main → CI green → TestFlight
|
||||
```
|
||||
|
||||
iOS NEVER cross-references the server's `develop` branch's OpenAPI. iOS targets the latest server tag's YAML, period. This keeps both sides shippable independently.
|
||||
|
||||
---
|
||||
|
||||
## § 8 — Common iOS-only gotchas
|
||||
|
||||
These don't surface in any server log because the server never sees them.
|
||||
|
||||
### 8.1 Apple Health permissions on Simulator
|
||||
|
||||
```
|
||||
Symptom : HKHealthStore.requestAuthorization returns immediately, no prompt
|
||||
Cause : iOS Simulator stubs the HealthKit framework; no real permission flow exists
|
||||
Fix : Test on a physical device, OR seed Postgres via /api/measurements/batch
|
||||
```
|
||||
|
||||
The iOS app should detect the Simulator and surface a "running on Simulator — manual entry only" banner in dev builds.
|
||||
|
||||
### 8.2 Withings web-only OAuth vs iOS deep-link return
|
||||
|
||||
```
|
||||
Symptom : User taps "Connect Withings" → opens SFSafariViewController → never returns to app
|
||||
Cause : Withings redirect_uri is server-side (https://...example.com/api/withings/callback),
|
||||
but the iOS app needs a deep-link back into the app after the cookie session
|
||||
confirms the connect
|
||||
Fix : Server-side handler at /api/withings/callback ends with a 302 redirect to a custom
|
||||
URL scheme (e.g. healthlog://withings/connected), which iOS catches via Universal
|
||||
Links (in production) or a custom URL scheme (in dev). The Apple Health bridge
|
||||
does NOT have this issue — there's no OAuth involved.
|
||||
```
|
||||
|
||||
Details in `02-server-architecture.md` § Withings integration.
|
||||
|
||||
### 8.3 RESEARCH_MODE_DISCLAIMER_VERSION drift on server redeploy
|
||||
|
||||
```
|
||||
Symptom : iOS user previously acknowledged the Research Mode disclaimer; after a server
|
||||
redeploy, the chart is gated again with a "you must re-acknowledge" dialog
|
||||
Cause : The constant in src/lib/medications/glp1-pk.ts moved forward (e.g.
|
||||
"2026-05-14.1" → "2026-05-21.1") because the server team bumped it.
|
||||
Fix : iOS reads the live currentDisclaimerVersion from GET /api/auth/me/research-mode
|
||||
on every Coach turn and on every Research Mode chart render. NEVER cache the
|
||||
version string client-side longer than one session.
|
||||
```
|
||||
|
||||
See `08-locked-contracts.md` § 6.
|
||||
|
||||
### 8.4 SSE Coach streaming via URLSession.bytes(for:), not long-polling
|
||||
|
||||
```swift
|
||||
// CORRECT — URLSession.bytes streams as the server emits
|
||||
let (bytes, response) = try await URLSession.shared.bytes(for: request)
|
||||
for try await line in bytes.lines {
|
||||
if line.hasPrefix("data: ") {
|
||||
let frame = String(line.dropFirst(6))
|
||||
// decode SSE event frame
|
||||
}
|
||||
}
|
||||
|
||||
// WRONG — would buffer the entire response before any UI update
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
```
|
||||
|
||||
The Coach UX depends on token-by-token reveal. Buffered reads collapse to a "wait 4s, then everything appears at once" experience that defeats the entire point. See `14-coach-mental-model.md` § Evidence-grounding.
|
||||
|
||||
### 8.5 The Idempotency-Key lifecycle on iOS
|
||||
|
||||
```
|
||||
Step 1 : Generate UUID().uuidString once per logical write
|
||||
Step 2 : Persist the key + body to disk
|
||||
Step 3 : POST with Idempotency-Key header
|
||||
Step 4 : On 2xx or non-retryable 4xx (400/415/422), drop the key
|
||||
Step 5 : On 5xx or network failure, retry with the SAME key (24h replay window)
|
||||
Step 6 : On 401, refresh the token, retry with SAME key
|
||||
```
|
||||
|
||||
The most common iOS bug here: generating a fresh UUID on retry. That produces a duplicate write, not the idempotent replay. See `17-error-handling.md` § 3.
|
||||
|
||||
### 8.6 Time zones in measurement timestamps
|
||||
|
||||
```
|
||||
Symptom : An iOS sample at 23:50 in Pacific/Auckland lands in the wrong day on the dashboard
|
||||
Cause : iOS sent the timestamp as a UTC string, the server day-bucketed it in UTC instead
|
||||
of the user's TZ
|
||||
Fix : Send the timestamp as ISO-8601 WITH offset (e.g. "2026-05-14T23:50:00+12:00").
|
||||
The server's userDayKey() resolver uses User.timezone to bucket, but the offset
|
||||
on the wire is what locks the wall-clock moment.
|
||||
```
|
||||
|
||||
### 8.7 The `X-Client-Type: native` header is mandatory on login
|
||||
|
||||
```
|
||||
Symptom : iOS login succeeds but no refresh token comes back
|
||||
Cause : Without X-Client-Type: native (or the UA prefix HealthLog-iOS), the server treats
|
||||
the call as web and only sets a cookie
|
||||
Fix : Always send X-Client-Type: native on /api/auth/login and /api/auth/register
|
||||
```
|
||||
|
||||
See `05-auth-flows.md` § 2.
|
||||
|
||||
### 8.8 Push notifications — the APNs token lifecycle
|
||||
|
||||
```
|
||||
Step 1 : iOS requests notification permission
|
||||
Step 2 : iOS receives an APNs device token (32 bytes hex)
|
||||
Step 3 : iOS POSTs the token to /api/devices with platform: "ios" + apnsEnvironment
|
||||
("sandbox" for dev builds, "production" for App Store)
|
||||
Step 4 : Server stores Device.apnsToken with a partial unique index
|
||||
Step 5 : On every app launch, iOS re-POSTs the token (APNs tokens rotate silently)
|
||||
Step 6 : On logout, iOS DELETEs the token via /api/devices/[id]
|
||||
```
|
||||
|
||||
The most common bug: forgetting Step 5. APNs rotates tokens silently; stale tokens fail to deliver. The server's dispatcher cleans up tokens that fail twice in a row.
|
||||
|
||||
### 8.9 Coolify staging vs production URLs
|
||||
|
||||
```
|
||||
Production : https://healthlog.example.com (Marc's main instance)
|
||||
Staging : https://staging.healthlog.example.com (refreshed nightly from prod backup)
|
||||
Local : http://localhost:3000 (dev server)
|
||||
```
|
||||
|
||||
Never run iOS e2e tests against production. Never run integration tests against staging. Local-Postgres-via-testcontainers for integration; staging-only for e2e.
|
||||
|
||||
### 8.10 Six locales — string keys must exist in all of them
|
||||
|
||||
```
|
||||
Symptom : iOS app shows the raw key "coach.refusal.glp1_dose" instead of the translation
|
||||
Cause : The key was added to en.json but not to de.json / fr.json / es.json / it.json / pl.json
|
||||
Fix : Add the key to all six bundles in the same PR. The server's i18n integrity test
|
||||
blocks PRs that drop a key; the iOS side has no such gate — review carefully.
|
||||
```
|
||||
|
||||
See `19-i18n-system.md` § 3.
|
||||
|
||||
---
|
||||
|
||||
## § 9 — Common iOS PR shapes (cheat sheet)
|
||||
|
||||
| PR type | Files typically touched | Reviewer focus |
|
||||
| --- | --- | --- |
|
||||
| New endpoint client method | `Sources/HealthLogAPI/Generated/*` (regen) + one new `*ViewModel.swift` | Contract-diff loop |
|
||||
| New screen | One Swift file under `HealthLog/Views/`, one ViewModel, one string bundle update × 6 | Design + i18n |
|
||||
| New HealthKit metric | `HealthKitClient.swift` + `MeasurementType` mirror + server-side `apple-health-mapping.ts` first | Server PR first, iOS PR second |
|
||||
| Bug fix | Single file, single test | Single reviewer (code-review) |
|
||||
| Dependency bump | `Package.swift` + lockfile | senior-dev — check for behaviour changes |
|
||||
|
||||
---
|
||||
|
||||
## § 10 — STOP HERE if…
|
||||
|
||||
| If your task is… | …skip the rest and read… |
|
||||
| --- | --- |
|
||||
| Just calling one endpoint | `03-api-contracts.md` § matching route |
|
||||
| Wiring login + refresh | `05-auth-flows.md` |
|
||||
| Building a Coach UI | `14-coach-mental-model.md` + `15-insights-architecture.md` |
|
||||
| Building a dashboard tile | `11-web-ui-tour.md` § Dashboard + `12-design-system.md` |
|
||||
| Understanding contract-locks before any code change | `08-locked-contracts.md` |
|
||||
|
||||
Otherwise: continue to `10-research-pointers.md`.
|
||||
@@ -0,0 +1,369 @@
|
||||
---
|
||||
file: 10-research-pointers.md
|
||||
purpose: Cross-link map to the research and review notes already in .planning/research/ — the why behind every architecturally-load-bearing decision iOS-Claude will hit.
|
||||
when_to_read: When the doc-pack says "read the equivalent research note", when designing a feature that touches Apple Health / Withings / GLP-1 / Health Score / source-priority, or before tagging a v1.5.x release.
|
||||
prerequisites: 00-philosophy.md (the research-first directive)
|
||||
estimated_tokens: ~3500
|
||||
version_anchor: v1.4.25 / sha 49f71c92
|
||||
---
|
||||
|
||||
## TL;DR
|
||||
|
||||
Marc's rule from `feedback_research_before_complex_features`: research benchmarks the ecosystem before code lands. The artefacts live under `.planning/research/` and are the canonical "why" alongside the doc-pack's "what". Read the matching research note BEFORE building any architecturally-new iOS feature; reach for the W21 review notes BEFORE tagging a release.
|
||||
|
||||
## How to use this file
|
||||
|
||||
Every entry is `<file>` → `<word count>` → `<status>` → `<one-line summary>` → `<read this if you're about to>`. Status is **current** (still drives v1.4.26+) or **historical** (already absorbed into shipped code — read for context only).
|
||||
|
||||
```
|
||||
PROJECT_ROOT/.planning/research/ ← all paths in this file are relative to here
|
||||
```
|
||||
|
||||
## STOP HERE if…
|
||||
|
||||
| If your task is… | …skip the rest and read… |
|
||||
| --- | --- |
|
||||
| Just need an API contract | `03-api-contracts.md` |
|
||||
| Just need a refusal-probe shape | `08-locked-contracts.md` § 7 |
|
||||
| Just need the Coach mental model | `14-coach-mental-model.md` |
|
||||
| Need the v1.4.25 reconcile inventory | `.planning/phase-W21-reconcile-plan.md` (see § 4 below) |
|
||||
|
||||
Otherwise: continue here.
|
||||
|
||||
---
|
||||
|
||||
## § 1 — Ecosystem research (Apple Health, wearables, GLP-1)
|
||||
|
||||
### `apple-health-ecosystem-scan.md` — 4113 words — current
|
||||
|
||||
GitHub topic `apple-health` deep scan: 30 top repos by stars, license + tech + maturity per row. Catalogues the OSS landscape HealthLog could plug into (HealthGPT, react-native-healthkit, apple-health-grafana, healthkit-to-sqlite, SleepChartKit).
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Pick an iOS HealthKit binding library
|
||||
- Decide whether a SwiftUI native chart or a Recharts-WebView mirror is the right call
|
||||
- Evaluate licence compatibility for any OSS Apple Health adapter
|
||||
|
||||
### `apple-health-sync-deep-dive.md` — 4968 words — current
|
||||
|
||||
iOS sync patterns + clean-room teardowns. The deeper version of the ecosystem scan; covers observer-query topology, HKAnchoredObjectQuery cursoring, background-delivery quirks, and the conflict-reconciliation patterns the open-wearables and healthkit-to-sqlite projects converged on.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Wire `HKObserverQuery` + `HKAnchoredObjectQuery` for incremental sync
|
||||
- Decide between background-delivery and explicit foreground polling
|
||||
- Reconcile HealthKit's "edited samples are new IDs" model with the server's externalId-based dedup
|
||||
|
||||
### `glp1-feature-inspiration.md` — 12071 words — current — **CRITICAL**
|
||||
|
||||
The single largest research artefact in the pack. EMA EPAR PDFs (Tirzepatide / Semaglutide / Liraglutide / Dulaglutide / Saxenda / Rybelsus) extracted near-verbatim; ASCPT pop-PK paper (psp4.13099) for the journal-of-record two-compartment math; clean-room teardown of the closed-source `my-glp-shot` app; **EU MDR 2017/745 + MDCG 2021-24 boundary analysis**.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Build ANY GLP-1-touching feature (medication list, injection-site map, dose-change history, plateau detection, Research Mode chart)
|
||||
- Add or modify a GLP-1 drug in the catalog
|
||||
- Touch the Coach refusal logic around drug levels (GROUND RULE 9/15)
|
||||
- Surface a "weight projection" or "dose escalation" UI — those CROSS the MDR threshold and are explicitly out of scope
|
||||
|
||||
The four do-not-builds (N1–N4 in the research) are the iOS-side's hard line: never project weight loss, never recommend escalation, never check drug-drug interactions, never import code/schema from `my-glp-shot` (no LICENSE = all rights reserved).
|
||||
|
||||
### `glp1-injection-tracking.md` — 4488 words — historical
|
||||
|
||||
The W4 first-pass research. Predecessor to `glp1-feature-inspiration.md` — covers the foundational landscape (treatmentClass enum proposal, injection-site map design, side-effect taxonomy, dose-change history schema). Mostly absorbed into shipped code by v1.4.25.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Trace the historical reasoning behind why `treatmentClass = GLP1` exists as a separate column
|
||||
- Understand the injection-site rotation algorithm
|
||||
|
||||
### `open-wearables-comparison.md` — 4075 words — current
|
||||
|
||||
Side-by-side: Apple Health vs Withings vs HealthLog. Covers data-source coverage, sync mechanics, conflict resolution, and the unique design problems each platform solves. The source for the two-axis source-priority decision.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Design any UI that surfaces "which source provided this reading"
|
||||
- Add a new metric type and need to know which sources can supply it
|
||||
- Build a Settings → Sources screen
|
||||
|
||||
### `withings-api-coverage.md` — 4648 words — historical
|
||||
|
||||
Earlier Withings API audit (pre-W17). Coverage table of every Withings endpoint with what HealthLog ingested vs what it ignored. Largely absorbed; the W17 follow-up below supersedes it for activity + sleep specifics.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Need a historical "what did Marc consider but not ship" answer for Withings
|
||||
|
||||
### `withings-plus-comparison.md` — 5306 words — historical
|
||||
|
||||
Older competitive intel on Withings+ subscription. Captures the "what does the consumer app surface that HealthLog doesn't" gaps as of the time it was written. Useful as a feature-parity reference but not actionable for v1.5.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Pitch a feature parity argument with the Withings+ app
|
||||
|
||||
---
|
||||
|
||||
## § 2 — UX + design research
|
||||
|
||||
### `health-score-provenance-ux.md` — 3467 words — current
|
||||
|
||||
The W8e research that shaped the Health Score provenance accordion. Covers the "trust surface" reasoning, the four-component decomposition, and the `aria-labelledby` pairing pattern.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Build a Health Score tile on iOS
|
||||
- Design the per-component provenance disclosure UI
|
||||
|
||||
### `insights-sub-pages-ux.md` — 4961 words — current
|
||||
|
||||
The W4 Insights mother-page + sub-pages UX research. Apple Health vs Withings vs Oura tile patterns, hero+sub-tile composition, where the daily briefing sits.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Build the iOS Insights surface
|
||||
- Decide between a single scrollable feed and per-metric tabs
|
||||
- Design the Daily Briefing tile
|
||||
|
||||
### `source-priority-two-axis.md` — 2742 words — current
|
||||
|
||||
The W8c two-axis source-priority research. Why metric × device-type is the right shape (vs flat ladder), and how the iOS Settings → Sources screen surfaces both axes.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Build Settings → Sources on iOS
|
||||
- Touch the `pickCanonicalSource()` resolver semantics in any way
|
||||
|
||||
### `zielwerte-redesign.md` — 5600 words — historical
|
||||
|
||||
The older target-zone redesign research. Mostly absorbed; surfaces the BD-Zielbereich UI thinking that ships at `/insights/zielwerte`.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Build the iOS target-zone editor
|
||||
- Understand why "Zielwerte" is a distinct page (vs nested under Settings)
|
||||
|
||||
---
|
||||
|
||||
## § 3 — Feature-specific research
|
||||
|
||||
### `w8d-implementation-outline.md` — 5051 words — current
|
||||
|
||||
Apple Health enum extensions spec. The implementation outline for the v1.4.25 W8d additions (AUDIO_EXPOSURE_ENV, AUDIO_EXPOSURE_HEADPHONE, TIME_IN_DAYLIGHT, plus the workout-batch endpoint).
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Add a new MeasurementType to the iOS side
|
||||
- Wire workout ingest
|
||||
|
||||
### `w14b-onboarding-rebuild.md` — 3177 words — current
|
||||
|
||||
The onboarding wizard redesign spec. Four-step state machine, race-safe step advance, the rationale for skipping the localStorage goals shim.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Build the iOS onboarding flow
|
||||
- Touch any `/api/onboarding/*` endpoint
|
||||
|
||||
### `w14c-native-coach-prompts.md` — 3616 words — current
|
||||
|
||||
Multi-locale Coach prompt strategy. The reasoning behind native FR/ES/IT/PL prompts (vs the pre-v1.4.25 EN-with-language-footer approach), the YAML matrix structure, the parity test design.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Modify any Coach prompt or refusal copy
|
||||
- Understand why the iOS side cannot localise refusal copy itself (it MUST come from the server)
|
||||
|
||||
### `w16b-workout-ingest.md` — 2589 words — current
|
||||
|
||||
Workout batch ingest research (W16b). Covers HKWorkout shape, the 5 MB ceiling, the `endedAt < startedAt` guard, and the cross-source dedup gap deliberately left for v1.5.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Implement workout ingest from HealthKit on iOS
|
||||
- Hit a 413 from `/api/workouts/batch`
|
||||
|
||||
### `w16c-pr-detection.md` — 2501 words — current
|
||||
|
||||
PR detection research (W16c). The warm-up gate, the 6 workout slots, the silent-on-historical-backfill rule, the tie-handling logic.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Build the PR list screen
|
||||
- Wire APNs for `PERSONAL_RECORD_ACHIEVED` notifications
|
||||
|
||||
### `w17b-c-withings-activity-sleep.md` — 2653 words — current
|
||||
|
||||
Withings v2 spec (activity sync + sleep stage segments). Covers the webhook subscription channels, the OAuth scope upgrade banner, and the hourly fallback cron schedule.
|
||||
|
||||
Read this if iOS-Claude is about to:
|
||||
- Surface the Withings scope upgrade banner on iOS
|
||||
- Wire the Withings reconnect deep-link return
|
||||
|
||||
---
|
||||
|
||||
## § 4 — Pre-tag W21 review findings (current v1.4.25 state)
|
||||
|
||||
The 8-reviewer parallel pass that ran before the v1.4.25 release-merge. Each finding file is the unfiltered output of one reviewer; the consolidated reconcile plan is the canonical "what's still pending in v1.4.26".
|
||||
|
||||
### `w21-code-review-findings.md` — 2340 words
|
||||
|
||||
Audited: bug-spotting across the v1.4.25 diff. Caught the `endedAt < startedAt` workout-gate gap, the inventory state-machine bypass, and several quiet edge cases.
|
||||
|
||||
Still pending in v1.4.26:
|
||||
- Light-severity items deferred per the rubric (see reconcile plan)
|
||||
- Refactor recommendations tagged as M-low priority
|
||||
|
||||
### `w21-security-findings.md` — 2358 words
|
||||
|
||||
Audited: secret leaks, auth boundary, injection vectors. Caught the critical Withings webhook secret-in-logs leak (Fix-J in the reconcile) + the GLP-1 endpoint missing Zod/audit/rate-limit (Fix-K).
|
||||
|
||||
Still pending in v1.4.26:
|
||||
- Medium-severity dependency upgrade recommendations
|
||||
- Long-term auth-token rotation policy review
|
||||
|
||||
### `w21-design-findings.md` — 2023 words
|
||||
|
||||
Audited: touch targets ≥ 44×44 px, palette consistency, contrast WCAG-AA. Caught onboarding wizard buttons below floor, range-bar palette drift, PR-badge dark-mode contrast.
|
||||
|
||||
Still pending in v1.4.26:
|
||||
- Low-severity visual polish items (already documented as deferred)
|
||||
|
||||
### `w21-senior-dev-findings.md` — 2466 words
|
||||
|
||||
Audited: architecture + future-proofing. Flagged the `withRateLimit` positional-args drift (now option-bag), the `safeRequestProp` narrow-catch surface, several abstraction-needed surfaces.
|
||||
|
||||
Still pending in v1.4.26:
|
||||
- Module boundary cleanup recommendations
|
||||
- Test infrastructure improvements
|
||||
|
||||
### `w21-simplifier-findings.md` — 3340 words
|
||||
|
||||
Audited: dead code, redundant abstractions. Caught the `goalsStorageKey` orphaned export + the `GoalsChipPicker` localStorage block with no consumer.
|
||||
|
||||
Still pending in v1.4.26:
|
||||
- Lower-priority dead-code removals deferred to next cleanup pass
|
||||
|
||||
### `w21-product-lead-assessment.md` — 3394 words
|
||||
|
||||
Audited: user-facing acceptance, copy quality, flow polish. Caught medication history page hard-coded "Zurück" back-button (now `t()`-routed), several copy polish items.
|
||||
|
||||
Still pending in v1.4.26:
|
||||
- Copy-tone refinements scoped as Low
|
||||
- UX flow simplifications scoped as Medium-defer
|
||||
|
||||
### `w21-i18n-runtime-findings.md` — 2624 words
|
||||
|
||||
Audited: hard-coded strings, locale-bundle parity. Caught the medication history "Zurück" leak + several runtime-translatable-but-static strings.
|
||||
|
||||
Still pending in v1.4.26:
|
||||
- Long-tail copy that survives in admin-only surfaces
|
||||
- Locale-pluralisation edge cases for non-DE/EN
|
||||
|
||||
### `w21-dead-code-findings.md` — 2841 words
|
||||
|
||||
Audited: unused exports, unreferenced files. Caught the same `goalsStorageKey` issue (cross-referenced with simplifier), plus older onboarding-spec leftovers.
|
||||
|
||||
Still pending in v1.4.26:
|
||||
- Confirmed-dead items waiting for the v1.4.26 cleanup pass
|
||||
|
||||
---
|
||||
|
||||
## § 5 — Older review findings (W10 baseline)
|
||||
|
||||
Historical context — the pre-v1.4.20 review pass that established the W21-style 8-reviewer pattern.
|
||||
|
||||
| File | Words | Audited |
|
||||
| --- | --- | --- |
|
||||
| `w10-code-review-findings.md` | 1990 | First-pass bug audit |
|
||||
| `w10-security-review-findings.md` | 1465 | First-pass security audit |
|
||||
| `w10-design-review-findings.md` | 1521 | First-pass design audit |
|
||||
| `w10-senior-dev-findings.md` | 1974 | First-pass architecture audit |
|
||||
| `w10-simplifier-findings.md` | 1873 | First-pass simplification audit |
|
||||
| `w10-product-lead-assessment.md` | 2592 | First-pass UX audit |
|
||||
| `w10-i18n-runtime-gaps.md` | 1949 | i18n runtime probe baseline |
|
||||
| `w10-dead-code-candidates.md` | 2124 | Dead-code baseline |
|
||||
| `w10-reconcile-A-design-report.md` | 973 | Reconcile fix-out for design |
|
||||
| `w10-reconcile-B-architecture-report.md` | 991 | Reconcile fix-out for architecture |
|
||||
| `w10-reconcile-C-security-auth-report.md` | 715 | Reconcile fix-out for security/auth |
|
||||
| `w10-reconcile-D-simplifier-report.md` | 479 | Reconcile fix-out for simplifier |
|
||||
|
||||
These are historical — every must-fix item from the W10 pass landed across v1.4.20-v1.4.24. Read only when investigating the historical reasoning behind a v1.4.20-v1.4.24 change.
|
||||
|
||||
---
|
||||
|
||||
## § 6 — The W21 reconcile plan
|
||||
|
||||
### `.planning/phase-W21-reconcile-plan.md` — outside `research/`
|
||||
|
||||
The canonical post-W21 reconcile inventory + the 7-Fix-* surface map. Collates 103 findings (2 Critical + 20 High + 41 Medium + 40 Low) from the 8 reviewers into 7 touch-disjoint Fix-* surfaces totalling ~22 commits.
|
||||
|
||||
The 7 Fix-* surfaces (each touch-disjoint):
|
||||
|
||||
| Surface | Scope | Severity |
|
||||
| --- | --- | --- |
|
||||
| **Fix-J** | Withings webhook log redaction (`src/lib/logging/`) | Critical |
|
||||
| **Fix-K** | GLP-1 endpoint hardening + legacy webhook EOL counter | High |
|
||||
| **Fix-L** | Design polish (touch targets, palette, contrast, dialog footer, native selects, back-button i18n) | High + Medium |
|
||||
| **Fix-M** | Inventory state-machine + workout schema gate | Medium |
|
||||
| **Fix-N** | `withRateLimit` option-bag standardisation + helper extraction | Medium |
|
||||
| **Fix-O** | Audit-log retention + geo enrichment cleanup | Medium |
|
||||
| **Fix-P** | Dead-code sweep + simplifier follow-ups | Medium |
|
||||
|
||||
**iOS-Claude implication**: every item in the reconcile plan that touches a contract is already documented in `08-locked-contracts.md`. The reconcile plan itself is internal-only — never published. Read it BEFORE tagging v1.5.x to understand what v1.4.26 will likely close.
|
||||
|
||||
---
|
||||
|
||||
## § 7 — Research-first directive (the meta rule)
|
||||
|
||||
Marc-memory `feedback_research_before_complex_features`:
|
||||
|
||||
> AI / insights / chart work runs through a research agent first. The agent benchmarks Apple Health / Withings / Oura, proposes an ecosystem-fit plan, then the implementation phase starts.
|
||||
|
||||
For iOS-Claude this means: before any of these touches code, a `.planning/research/` note must exist:
|
||||
|
||||
- New Coach surface or refusal change
|
||||
- New Insight type or sub-page
|
||||
- New chart family (sparkline, range bar, scatter, etc.)
|
||||
- New ingest source (e.g. Garmin, Fitbit, Strava)
|
||||
- New medical-adjacent feature (anything PK / dose / drug-interaction / projection)
|
||||
|
||||
If the relevant note doesn't exist, file one before writing the iOS code. The research-first rule is the single most reliable predictor of which v1.X.Y features ship cleanly and which need an emergency follow-up patch.
|
||||
|
||||
---
|
||||
|
||||
## § 8 — How to write a new research note
|
||||
|
||||
Format convention (mirrors the existing notes):
|
||||
|
||||
```markdown
|
||||
# <Feature> Research — v1.5.X
|
||||
|
||||
**Scope.** One sentence on what's in / what's out.
|
||||
**Read-only.** No code commits, no schema commits.
|
||||
**Date.** YYYY-MM-DD
|
||||
**Author.** Marc Bombeck
|
||||
|
||||
## TL;DR
|
||||
<2-3 sentence summary of the recommendation>
|
||||
|
||||
## Section 1 — Ecosystem inventory
|
||||
<table of 10-30 comparable projects with stars / license / tech / one-liner>
|
||||
|
||||
## Section 2 — Clinical / regulatory foundation (when relevant)
|
||||
<EMA EPAR / FDA / WHO / MDR references with direct PDF links>
|
||||
|
||||
## Section 3 — App teardowns
|
||||
<closed-source app analysis, screenshots, feature lists>
|
||||
|
||||
## Section 4 — Boundary analysis (when relevant)
|
||||
<EU MDR / GDPR / accessibility line-not-to-cross>
|
||||
|
||||
## Section 5 — Implementation outline
|
||||
<schema sketches, route sketches, NOT prescriptive code>
|
||||
|
||||
## Section 6 — Recommendations
|
||||
<3-5 numbered recommendations the Marathon can dispatch from>
|
||||
|
||||
## Section 7 — Do-not-builds
|
||||
<explicit list of features that violate boundaries; cite the source>
|
||||
```
|
||||
|
||||
Word-count target: 2000-5000 for a single-feature note, up to 12000 for a multi-feature deep-dive (like `glp1-feature-inspiration.md`).
|
||||
|
||||
---
|
||||
|
||||
## § 9 — STOP HERE if…
|
||||
|
||||
| If your task is… | …skip the rest and read… |
|
||||
| --- | --- |
|
||||
| Just need a contract refresher | `08-locked-contracts.md` |
|
||||
| Just need the Coach pipeline | `14-coach-mental-model.md` |
|
||||
| Just need to call an endpoint | `03-api-contracts.md` |
|
||||
| Tagging v1.5.x | `09-recommended-flow.md` § 4 (marathon-pattern handoff) + `09-recommended-flow.md` § 5 (Quality Gate) |
|
||||
|
||||
Otherwise: you have read every file in the v1.5 iOS handoff doc-pack. Marc UAT awaits.
|
||||
Reference in new issue
Block a user