I read this as a read-only UX/copy review task — locate the supplied screenshot and its generating copy strings, then deliver the Korean target summary, blocking issues, and QA acceptance criteria. No edits.


Two clipboard screenshots arrived minutes before this task spawned — reading them as the supplied screenshots.


Image tokens are not supported here — extracting the screenshot text via OCR instead.


`rapidocr-onnxruntime` is available — running OCR on both screenshots.


First shot is mostly the bot/dev pane. Getting the full OCR of the second screenshot (output was truncated).


OCR detected the dict-syntax leak (`{items: [], 'status': 'none'}`) in the second screenshot, but the Korean text in the Telegram pane got garbled. To improve the read results, I'll crop + upscale the Telegram pane and pinpoint the location of the generator code.


Korean OCR remains poor with RapidOCR. I'm trying tesseract with the kor traineddata, and in parallel identifying the summary generation code within the evidence wheel.


Found the generator code in evidence snapshots. Reading the latest candidate's copy logic.


This file explains all four defects. Currently identifying the callers and the synthesis fixtures so we can reconstruct the exact inputs.


What's running is the installed wheel (the `hermes-gateway-dualcoachtest.service` in the screenshot). To check the copy that was actually rendered, I'm comparing it against the reconciliation of installed-wheel-root.


The deployed presenter is consistent with the evidence snapshot. Since the screenshot also includes body stats / smoking / drinking lines, I'm looking for the more complete renderer within the snapshot.


No 흡연/음주 in the gateway code — those lines come from the LLM summary path. Recovering the exact synthetic inputs from fixtures/logs instead of guessing from noisy OCR.


Sibling tasks (H1/H2/schema mapping) exist — checking their results for the exact synthetic inputs.


I now have the exact synthetic inputs (`example_answer` set). One more targeted OCR pass on the message body to confirm the goal/activity lines before writing the target copy.


All evidence gathered — the screenshot defects are fully traceable. Composing the review.

---

## Read-only UX/copy review — Telegram customer summary card (task st_01a00a93)

### What I verified (no code edits, no live access)

- **Screenshots**: `/tmp/sshclip-clipboard-20260816-2131{15,22}*.png` (Telegram Desktop, `dual_coach_pilot_test_bot`, 21:31–21:32). Image tokens unsupported by this model, so I OCR'd them (rapidocr, multiple crop/upscale passes). Independently confirmed on the card, confidence ≥ 0.86: the dict leak `{'items': [], 'status': 'none'} · {'items': [], 'status': 'none'} · {'items': [], 'status': 'none'}` on the 알레르기 line, and bullets `- 키: 180cm`, `- 체중: 80kg`. (Two low-confidence lines resembling 흡연/음주 bullets could not be resolved; not relied on below.)
- **Exact synthetic inputs**: the card was generated from the canonical `example_answer` synthetic set — `profile-package/checkin_cli/nutrition_onboarding_contract.py:79-107` (DOB 1996-08-01, male, **height 180 / weight 80**, **`activity_category: moderate`**, rationale "주 3회 근력 운동", **`goal_type: maintain`**, all 7 structured fields `{"status":"none","items":[]}`, **`pregnancy_breastfeeding: False`**, **`eating_disorder_risk: False`**, 기본 조리 가능, 보통, 3끼, schedule "없음"). Matches screenshot values 1:1.
- **Causal chain** (evidence snapshots, `repository-runtime-closure/gateway/platforms/`):
  - `nutrition_onboarding_reconciliation.py:62-68` — `value()` coerces any stored value with `str()` → dict repr leaks (D1).
  - `:93, :96` — reads keys `goal` and `training_details` that **do not exist**; canonical keys are `goal_type` / `activity_rationale` (`QUESTION_FIELDS`, contract `:13-38`) → `목표: 미입력` (D2), activity ends `미입력` (D3).
  - `:70-81` — health gate checks nonexistent `medical_conditions`/`pregnancy_lactation` (real: `conditions`/`pregnancy_breastfeeding`) and compares stringified values against `{"없음","해당 없음","아니요"}`; `False` → `"False"` and dict → repr both trip it → false `검토할 입력이 있습니다` (D4).
  - `telegram_nutrition_onboarding_copy.py:100-147` — storage shapes that defeat the formatter: `{"status","items"}` dicts, English enum codes (`moderate`, `maintain`), booleans.
  - `nutrition_onboarding_reconciler.py:50-75` — the advisory copy model receives raw `json.dumps(answers)` with zero presentation rules.

### H3 verdict

**Confirmed.** There is no customer-facing presentation layer between stored answer shapes and rendered copy. Both authorities fail the same way: the deterministic formatter (`render_authoritative_customer_summary`) stringifies internal structures and reads wrong keys; the advisory model is handed raw JSON and only told "list fields contain Korean strings." The output is technically truthful about the *store* and false about the *customer*. A presentation layer that maps canonical fields → Korean labels is the missing component.

### Exact target summary (same synthetic inputs, Telegram-ready)

Follows the product's existing copy system — header, `- ` bullets, `·` separators, and the disclaimer sentences reused **verbatim** from `nutrition_onboarding_reconciliation.py:131,147-148` and `telegram_nutrition_onboarding_runtime_publication.py:140-143`:

```text
입력 내용을 정리했어요

- 목표: 유지
- 활동: 보통 · 주 3회 근력 운동
- 신체: 키 180cm · 체중 80kg
- 식사: 하루 3끼
- 조리·예산: 기본 조리 가능 · 보통
- 식사 시간: 특별한 제약 없음
- 알레르기·불내증·식품 제한: 해당 없음
- 건강 관련 입력: 별도 검토 입력이 없습니다.

이 요약은 참고용이며 안전 판정·승인·활성화를 대신하지 않습니다.
내용이 맞으면 아래 확인 버튼을 눌러 주세요.
```

Post-confirmation footnote (unchanged): `아래 버튼을 누르면 입력 확인이 저장되고 운영자 검토로 넘어갑니다. 이 단계에서 영양 코칭이 시작되거나 활성화되지는 않습니다.`

Copy rules encoded by this target: enum codes → customer labels (`maintain`→유지, `moderate`→보통); all-none structured fields collapse to one `해당 없음`; booleans → 아니오/해당 없음 semantics, never `False`; a genuinely empty optional field is **omitted or phrased `아직 입력되지 않았어요`** — the token `미입력` is banned customer-side.

### Blocking CJK/copy issues

1. **B1 — Internal dict repr in customer text.** `{'items': [], 'status': 'none'}` with ASCII braces/quotes inside Hangul sentences. Developer syntax visible to the customer; also mixes straight/curly quote glyphs. Blocking.
2. **B2 — `목표: 미입력` despite a maintain goal.** Factually wrong statement about the customer's own answer (root cause: key mismatch `goal` ≠ `goal_type`). Blocking.
3. **B3 — Activity line ends `미입력`** and leaks the English enum `moderate` (key mismatch `training_details` ≠ `activity_rationale`). Blocking.
4. **B4 — False safety signal.** `건강 관련 입력: 검토할 입력이 있습니다.` when every safety input is none/false. Safety-adjacent copy lying in the alarming direction; trains customers to ignore the warning when it is real. Blocking.
5. **B5 — Field-name contract drift (root cause of B2–B4).** Formatter keys (`goal`, `training_details`, `medical_conditions`, `pregnancy_lactation`, `religious_restrictions`) do not match `QUESTION_FIELDS` (`goal_type`, `activity_rationale`, `conditions`, `pregnancy_breastfeeding`, `religious_ethical_exclusions`). Every mismatched key silently renders as missing data. Blocking.
6. **B6 — English enum values in customer copy.** Any fix must map `sedentary/light/moderate/very_active/extra_active` and `loss/maintain/gain` to 좌식/가벼움/보통/매우 활동적/최고 활동 and 감량/유지/증량 (labels already exist in `telegram_nutrition_onboarding_copy.py:117-129`). Blocking for the customer surface.
7. **B7 — Stringified-value health gate.** The allowlist `{없음, 해당 없음, 아니요}` evaluated on `str(value)` misclassifies `False`, `None`, `[]`, and dict shapes. Must evaluate semantics (structured `status == "none"`, booleans false), not strings. Blocking.

Non-blocking: `없음 · 없음 · 없음 · 없음`-style repetition when multiple restriction fields are empty (collapse to one phrase); `- 식사 시간: 없음` reads curt (use `특별한 제약 없음`).

### Acceptance criteria for fresh screenshot QA

**Capture**: Telegram Desktop, customer DM topic, full card visible without "더 보기" truncation, bot name + timestamp in frame.

**Negative checks (must have zero occurrences in the card text):**
- `{`, `}`, `'items'`, `'status'`, `repr`-style quotes around Korean text
- `미입력` on any field that has a stored value; ideally no `미입력` at all
- standalone English enums: `moderate|sedentary|light|very_active|extra_active|maintain|loss|gain|False|True|None`
- `검토할 입력이 있습니다` when all safety fields are none/false
- keys or phrasing: `goal_type`, `training_details`, `medical_conditions`, `pregnancy_lactation`

**Positive checks for the all-none synthetic input (this case):**
- `- 목표: 유지` present; activity line contains `보통` and `주 3회 근력 운동`, ends without `미입력`
- restrictions line reads exactly `해당 없음`
- health line reads exactly `별도 검토 입력이 없습니다.`
- both disclaimer sentences verbatim; `- ` bullet style and `·` separators consistent; every line ≤ ~60 chars; total well under Telegram's 4096 limit

**Regression pairs (second screenshot each):**
- *provided-items case* (e.g., allergies `{"status":"provided","items":["유당"]}`, conditions provided): items render as comma-joined Korean text, no dict syntax, and the health line flips to `검토할 입력이 있습니다.` **only** when a safety field is genuinely provided/true.
- *genuinely-empty optional case*: empty optional renders as omitted line or `아직 입력되지 않았어요`, never `미입력`.

**Unit seam**: pin these as tests against `render_authoritative_customer_summary` with the `example_answer` set as input — the all-none fixture must produce the exact target card above (deterministic, no LLM in the loop).

No files were modified; no live system, service, or Telegram surface was touched.