ð æŠèŠ
ã¢ã¯ã·ã§ã³ãšäž»èŠ KPI ã®ã¹ãããã·ã§ããã詳现ã¯å·Šã¡ãã¥ãŒããåã¿ããžã
ðš ä»ããã¹ãããš â ã¢ã¯ã·ã§ã³ãå¿ èŠãïŒ
ð ã¹ãããã·ã§ãã
ð° ããžãã¹
æ¬ãªãªãŒã¹åãªã®ã§çŸç¶ã®æ°å€ã¯ãã¹ãããŒã¿ã§ããããã§ã¯ã·ããªãªèšç® (preset = æ²èг / æšæº / 楜芳) ã§èšç»å€ã確èªããŸãã
çŸç¶ã®å®çžŸã¯ æè¡ / ã³ã¹ã ã¿ãã§åç
§ã§ããŸãã
ð¯ ãã¡ãã« EV ã·ãã¥ã¬ãŒã¿ â Trial1 (14æ¥) â Paywall â Trial2 (14æ¥) â èªå課é
æ°ãã㌠(Free ãã©ã³ãªã): install â
Trial 1 (14æ¥ç¡æã»CCäžèŠ) â Paywall â
Trial 2 (14æ¥ç¡æã»CCãã) â
èªå課é â ææ¬¡ churn ã§æžè¡°
æä¹
ç¡æãŠãŒã¶ãŒã¯ååšããªããTrial 1 ã®ã³ã¹ãã¯å
š install åãèªç€Ÿè² æ
ã
ã¬ããŒ: äŸ¡æ Œèšèš (æé¡ / å¹Žé¡æ¯ç / 幎é¡å²åŒ) à Organic æ¯ç à Funnel å質 à ã¹ããŒãžå¥ CAC/installã
ð€ ã¹ããŒãžã¿ã€ã ã©ã€ã³ â Pre-Seed 6ã¶æ â Seed 6ã¶æ â Series-A 24ã¶æ (åèš 36ã¶æ)
ð ã¹ããŒãžå¥ ã¡ããªã¯ã¹ â åã funnel å質ã§ãã¹ããŒãžæ¯ã®çµæžæ§
| ã¹ããŒãž | CAC | install/æ | å®å¹ CAC / paying |
LTV / CAC | Payback | æ°èŠ paying / æ |
å®åžž ææ¬¡ OP |
|---|
ð 36ã¶æåçã·ãã¥ã¬ãŒã·ã§ã³ â ã¹ããŒãžé·ç§»ä»ããäœã¶æã§é»ååïŒ
åæ: ã¹ããŒãžããšã« install / CAC ãåãæ¿ãããTrial1 (14æ¥ å šå¡) + Trial2 (14æ¥ acceptors) ã®ç¿æãã課ééå§ãææ¬¡ churn ã§ active ãæžè¡°ã
ð ãããã¯ã
ãŠãŒã¶ãŒã®å¢æžãå®çãAI 粟床ããã£ãŒãããã¯ã®éçŽã
ð æé· â ãŠãŒã¶ãŒã¯å¢ããå®çããŠãããïŒ
ð çŽè¿14æ¥ã®æ°èŠç»é²
ð ãªãã³ã·ã§ã³ (Day1/7/30)
| ã³ããŒã | ãµã³ãã«æ° | æ®å | æ®åç |
|---|
N æ¥åã«ç»é²ãã人ã®ãã¡ãä»ã掻åããŠããå²åããµã³ãã«æ°ãå°ãªããšæ°å€ã¯ãã€ãºãå€ãã
ð¥ 補å KPI â AI 粟床ã»ã³ã¹ãæé©åã¯ã©ããïŒ
ð LLM åŒã³åºã caller æ¯ç (30d)
â ïž ã«ããªãŒèª€ãå ±å â ãŠãŒã¶ãŒããããããããããšæãã meal
| æå» | ãŠãŒã¶ãŒ | æçå | ç»é² kcal | lookup_key / meal_id |
|---|
ð¬ ãŠãŒã¶ãŒãã£ãŒããã㯠â ãŠãŒã¶ãŒã¯äœãæ±ããŠããïŒ
| æå» | ãŠãŒã¶ãŒ | å 容 |
|---|
âïž æè¡ / ã³ã¹ã
ã¬ã€ãã³ã·ããšã©ãŒãã³ã¹ãå èš³ã
𩺠æè¡ health â ãŠãŒã¶ãŒã¯äœç§åŸ ããããŠãããïŒ ãšã©ãŒã¯ïŒ
ð¯ 1次å¿çã¬ã€ãã³ã· (ãã€ã¯é¢ã â å¹ãåºã衚瀺)
ð¯ ã«ããªãŒåæ ã¬ã€ãã³ã· (å±¥æŽã«æçµkcal衚瀺ãŸã§)
ð 1次å¿çã®å èš³: ã©ãã«æéãããã£ãŠããã (7då¹³å)
â± è£å©: caller å¥ LLM åäœã¬ã€ãã³ã· (7d, OpenRouter API éšåã®ã¿)
| caller | n | p50 | p95 | p99 |
|---|
ð çŽè¿ã®ãšã©ãŒ (7d, æå€§20ä»¶)
| æå» | caller | error |
|---|
ðž ã³ã¹ã詳现 â ã©ãã«éãããã£ãŠããïŒ
ð æ¥æ¬¡ã³ã¹ãæšç§» (30d, OpenRouter ã®ã¿)
ð€ ã³ã¹ãäžäœãŠãŒã¶ãŒ (30d)
| user | plan | calls | cost |
|---|
ð caller à model å¥ (30d)
| caller | model | calls | cost |
|---|
ð€ Soniox æ¥æ¬¡ (30d, /v1/usage-logs ãã)
| æ¥ä» | åæ° | é³å£°ç§ | ã³ã¹ã |
|---|
ð LP αçãã¹ã Waitlist
ã¡ã¢ãç»é²ãæµå ¥å ãé ä¿¡åæ¢ç¶æ ãåå¥/äžæ¬ã¡ãŒã«éä¿¡ã
| æå» (JST) | Source | ç¶æ | éä¿¡å±¥æŽ | Referrer |
|---|
ð§ ã¡ãŒã«éä¿¡
ãã³ãã¬ãŒããéžã³ãå¿
èŠãªãã£ãŒã«ããå
¥åããŠéä¿¡ããŸããæ¬æã¯ç·šéã§ããŸãã(ãã³ãã¬ç®¡ç㯠functions/_lib/templates.ts)ã
ð ã¡ãŒã«ãã¬ãã¥ãŒ
å®éã«éä¿¡ãããå 容ã§ããéä¿¡åã®æçµç¢ºèªã«äœ¿ã£ãŠãã ãã (詊éã¯ãããŸãã)ã
ð ã¢ãŒããã¯ã㣠ã¬ãã¥ãŒ
7 芳ç¹ã§ shiwake ã®çŸç¶ãæŽçããèšèšãªãã¡ã¬ã³ã¹ã
åã»ã¯ã·ã§ã³ã¯ãð çŸç¶ / â
匷㿠/ â ïž æžå¿µ / ð TODOãã® 4 ãµãæ§é ã§çµ±äžã
æŽæ°ã¯ admin/index.html ã® data-tab="architecture" ã»ã¯ã·ã§ã³ã
ðº 1. ã·ã¹ãã å šäœããã â 1 æã§é ã«å ¥ããïŒ
ð çŸç¶
SwiftUI + Keychain"] Backend["âïž shiwake-daily-api
Hono on Workers"] D1B[("ðŸ D1: shiwake_daily")] R2[("𪣠R2: media")] Soniox{{"ð€ Soniox
STT realtime + async"}} OR{{"ð€ OpenRouter
Gemini 3 Flash / GPT-4o-mini"}} Apple{{"ð Apple
AppAttest + StoreKit 2"}} Weather{{"ð€ Open-Meteo
weather"}} LP["ð LP shiwake-lp(-stg)
Pages + Functions"] D1L[("ðŸ D1: shiwake-lp")] SMTP{{"ð§ ãåå.com SMTP"}} Admin["ð shiwake-admin
Pages SPA"] iOS -->|"Bearer access_token"| Backend iOS -.->|"WS stt-rt-v4 (temp key)"| Soniox iOS -->|"AppAttest assertion"| Backend iOS -->|"StoreKit JWS"| Backend Backend --> D1B Backend --> R2 Backend -->|"chat/completions"| OR Backend -->|"async stt"| Soniox Backend -->|"verify attest + JWS"| Apple Backend --> Weather Backend -->|"S2S notif"| Apple LP --> D1L LP -->|"SMTP 465"| SMTP Admin -->|"Basic auth"| Backend Admin -->|"Basic auth"| LP
â 匷ã¿
- Cloudflare ã«å¯ããã£ãåäžãã©ãããã©ãŒã (Workers + Pages + D1 + R2) ã§éçšé¢ã¯ã·ã³ãã«
- iOS â å€éš API ãžã®çŽæ¥åŒã³åºããæå°å (Soniox WS ã®ã¿ãtemp key çµç±)
- backend / lp / admin ã® 3 ãªãç圹å²ãæç¢º
â ïž æžå¿µ
- Backend åäžç°å¢: backend 㯠stg/prod ãåãããŠããªã (LP ã¯åãããŠãã)ãæ¬çªãããã€ã§å£ãããå³åœ±é¿
- Cloudflare é害æã®ä»£æ¿è·¯ãªã: D1 / Workers ãšãã«å瀟
ð TODO
- backend ã« shiwake-daily-api-stg ãåã (D1 ãå¥ instance)
- D1 ã®å®æã¹ãããã·ã§ããéçš
ð 2. äž»èŠ user flow â ãããŒã®åçŽæ§ã責åã®æç¢ºã
ð çŸç¶: ãçºè©± â ã«ããªãŒèšé²ã (äž»èŠ critical path)
â meal_nutrition_cache ã«ä¿å API->>D1: UPDATE meals.kcal (åŸè¿œã)
ð äž»èŠãããŒäžèЧ (æ¬ããŒãžã§å®æã¡ã³ããã¹ã)
- çºè©± â ã«ããªãŒèšé² (äžå³)
- äœéèšé²: POST /api/weights â D1 (LLM äžèŠ)
- å±¥æŽã®ä¿®æ£: PUT /api/meals/:id, /api/meals/:id/rename
- ãµããªè¡šç€º: GET /api/summary/weekly (éèšã¯ãšãª)
- ãµã€ã³ã¢ãã: AppAttest challenge â æ€èšŒ â users è¡äœæ â access_token è¿åŽ
- IAP 賌èª: StoreKit â JWS æœåº â /api/iap/verify â users.subscription_status æŽæ°
â 匷ã¿
- 1 次å¿ç㯠LLM 1 å (Gemini Flash) ã§åž°ããã«ããªãŒæ°å€ã¯ BG ã§åŸè¿œã (UI ã¯ä»®å€è¡šç€º â kcal 確å®ã§å·®ãæ¿ã)
- åå²ã®å°ãªã sequential flow ã§èŠ³æž¬ãããã
â ïž æžå¿µ
- BG ã¿ã¹ã¯å€±æã®å¯èŠåã匱ã: mealLookup 倱ææããŠãŒã¶ãŒã«ã¯èŠãã«ãã (kcal ããâãã®ãŸãŸæ®ã)
- WS ã®æ¥ç¶å€±æãã©ãŒã«ããã¯ãªã: Soniox WS ãèœã¡ãæãasync REST ãžã®ãã©ãŒã«ããã¯ã¯æªå®è£
ð TODO
- åãããŒã 1 ã»ã¯ã·ã§ã³ãã€å³åŒå (äœé/å±¥æŽç·šé/ãµããª/IAP)
- 倱æãã¹ã®è¿œå (Soniox åæãOpenRouter timeout)
ðŸ 3. ããŒã¿ã¢ãã« â æ£èŠåãš PII ã®å±åš
ð çŸç¶
D1 = SQLiteãJOIN ã¯äœ¿ããåæ£ã¯æèããªã (rows-per-account ãæ¥µå°)ã
| ããŒãã« | çšé | PII? |
|---|---|---|
users | account + profile + affinity + sub status + AppAttest signup | â åå/ã¡ã¢ã/èªçæ¥ |
meals | é£äºèšé² (transcript, name, kcal, lookup_key, photo_key) | â é£äº photo |
weights | äœéèšé² | â äœéå€ |
exercises | éåèšé² | â |
notes | ãã®ä»ã¡ã¢ | â |
meal_nutrition_cache | æ£èŠå kcal cache (30d TTLæ³å®) | â |
user_facts | assistant åŠç¿æžã¿äºå® (奜ã¿/å¶çŽ) | â |
conversation_log | çŽè¿ N ã¿ãŒã³ (ingest context) | â |
weather_cache | Open-Meteo 1h cache | â |
api_usage_log | OpenRouter/Soniox call ledger | â |
request_timing | per-request latency milestone | â |
feedback | é³å£°ãã£ãŒããã㯠transcript | â |
attest_challenges | çæ AppAttest nonce | â |
invitations | legacy æåŸ ã³ãŒã redeem | â |
waitlist (lp) | α ãã¹ã ã¡ã¢ã + unsub token | â ã¡ã¢ã |
email_log (lp) | éä¿¡å±¥æŽ | â ã¡ã¢ã |
â 匷ã¿
- PII ã
users+ åãã°ç³»ã«éäžãç©çåé€ 1 ãŠãŒã¶ãŒ = ã«ã©ã åé€ã§å®çµ (GDPR æ³å®å®¹æ) - cache ç³» (meal_nutrition_cache, weather_cache) 㯠user_id ãæããªãã®ã§åé€åœ±é¿ãªã
â ïž æžå¿µ
- åé€ã«ã¹ã±ãŒãæªå®è£ : ãŠãŒã¶ãŒå逿ã®åããŒãã«å逿é ãã³ãŒãåãããŠããªã
- conversation_log ã® TTL ãªã: ç¡å¶éã«äŒžã³ãå¯èœæ§ â ãµã€ãºç£èŠãå¿ èŠ
ð TODO
- åããŒãã«ã«ä¿ææéããªã·ãŒ (äŸ: meal_nutrition_cache=30d, conversation_log=ææ° 50 turns, request_timing=90d)
DELETE /api/meãšã³ããã€ã³ã + å šããŒãã« cleanup ã¹ã¯ãªãã
ð 4. ã»ãã¥ãªãã£ã»èªèšŒ â æ»æé¢ãææ¡ã§ããŠããã
ð çŸç¶
| ã¬ã€ã€ | æ¹åŒ | ã¬ãŒã |
|---|---|---|
| iOS â backend | Authorization: Bearer <access_token> | requireUser ã D1 ã® users.access_token ãçŽæ¥ lookup (opaque random) |
| ãµã€ã³ã¢ãã | Apple AppAttest | backend/src/appattest.ts: cert chain + nonce + bundle/team æ€èšŒ (node-app-attest) |
| iOS â Soniox WS | backend çµç±ã§ temp key çºè¡ | /api/auth/soniox-temp-key (èŠ user èªèšŒ) |
| admin /api/admin/* | HTTP Basic admin:ADMIN_PASSWORD | middleware over /api/admin/* |
| IAP S2S notif | JWS payload ã decode | â ïž çœ²åæ€èšŒã¯æªå®è£ (TODO) |
| LP /api/waitlist | ç¡èªèšŒ (å ¬éãã©ãŒã ) | email regex + ip hash + éè€æ€ç¥ã®ã¿ãrate limit ãªã |
| LP /api/unsubscribe | token çµç± (RFC 8058) | 16 byte ã©ã³ãã token |
â 匷ã¿
- iOS ãµã€ã³ã¢ããã« AppAttest ãæ¡çš â åœç«¯æ«ããã®èªåã¢ã«ãŠã³ã倧éäœæã鲿¢
- access_token 㯠opaque random (JWT 鵿ŒæŽ©ãªã¹ã¯ãªã)ãrevoke 㯠DB æŽæ° 1 è¡
- Soniox èªèšŒæ å ±ã iOS ãã³ãã«ã«åã蟌ãŸãªã (temp key æ¹åŒ)
â ïž æžå¿µ
- IAP JWS çœ²åæªæ€èšŒ â åœ receipt ã§ premium åãããå¯èœæ§ (èŽåœç)
- LP waitlist rate limit ãªã â SPAM / å«ãããç»é²ã«ãã D1 å§è¿« + ã¡ã¢ãæ±æ
- access_token ã®ããŒããŒã·ã§ã³ãªã â 端æ«çŽå€±æã«æåã§ DB æŽæ°ãå¿ èŠ
- admin Basic auth ãã·ã³ã°ã«ãã¹ã¯ãŒã â å ±æäºæ æã«ããŒãå¿ é
ð TODO
- iap.ts ã« x5c chain æ€èšŒ + issuer/bundleId/environment æ€èšŒãå ¥ãã (çŸç¶ skeleton)
- LP waitlist ã« Cloudflare Turnstile or IP-based rate limit
- access_token ã«æå¹æé + refresh ã®æ€èš
- æ°èŠãšã³ããã€ã³ãè¿œå æã®ã»ãã¥ãªã㣠ãã§ãã¯ãªã¹ãå
â¡ 5. ããã©ãŒãã³ã¹ & ã³ã¹ã â ããã«ããã¯ã¯ç¹å®æžã¿ã
ð çŸç¶: 1 次å¿çã¬ã€ãã³ã·äºç®
| step | ç®æš p95 | åè |
|---|---|---|
| Soniox WS ç¢ºå® (çºè©±çµäº â æçµ transcript) | ~1.5s | realtime stt-rt-v4 |
| ingest (Gemini 3 Flash) | ~1.5s | intent + entity æœåº |
| persona (GPT-4o-mini) | ~0.8s | å¿çã¡ãã»ãŒãžçæ |
| åèš (1 次å¿ç) | ~3-4s | å±¥æŽ UI ã«å¹ãåºã衚瀺 |
| mealLookup (BG, web search) | ~6-10s | UI 㯠kcal ãâãâ 確å®ã§å·®ãæ¿ã |
ð çŸç¶: 1 active user / æ¥ ã³ã¹ãåŒ
10 çºè©±/æ¥ Ã 10 ç§ Ã (Soniox $0.0025/ç§ + Gemini Flash + GPT-4o-mini) à cache HIT ç (mealLookup 㯠HIT ã§ 0)
= çŽ Â¥2-3/æ¥ (cache HIT 60%+ æ³å®) / Â¥4-5/æ¥ (HIT 30% æ³å®)
宿ž¬ã¯ æè¡ / ã³ã¹ã ã¿ãåç §ã
â 匷ã¿
- ã¯ãªãã£ã«ã«ãã¹ã¯ LLM 1 段 (Gemini Flash) â ã³ã¹ã/ã¬ã€ãã³ã·äºæž¬ãããã
- mealLookup 㯠web search ã䌎ãã®ã§ BG åãUI ãåŸ ãããªã
- meal_nutrition_cache ã§ååæçã¯ç§ã§ kcal ç¢ºå® (LLM ã³ã¹ã 0)
â ïž æžå¿µ
- cache HIT çãã³ã¹ãæ§é ãæ¯é ãHIT ãäžãããš OpenRouter ã³ã¹ããç·åœ¢ã«äžæ
- persona ã®å¿ççæãé ããªããš 1 次å¿çãäœææªå (çŸç¶ GPT-4o-mini ãçªç¶é ããªããªã¹ã¯)
- Soniox WS ã®ãããã¯ãŒã¯æ¡ä»¶äŸåæ§ã倧 (å°äžéç)
ð TODO
- cache HIT çã®èŠå ±éŸå€ (e.g. 7d å¹³å 50% 以äžã§ alert)
- persona ã Gemini Flash ã«çµ±äžããéžæè¢ã®è©äŸ¡
- ãªãã©ã€ã³é²é³ â åŸéä¿¡ã¢ãŒã (Soniox äžå®å®æ)
ð 6. å€éšäŸåãªã¹ã¯ â çè¶³ãæãã³ã¹ããèŠããã
ð çŸç¶
| äŸåå | çšé | ããã¯ã€ã³ | ä»£æ¿ | å€äžãèæ§ |
|---|---|---|---|---|
| Cloudflare | Workers + Pages + D1 + R2 | ðŽ é« (D1 ç§»è¡ = SQLite ãã³ã + åæ§ç¯) | Fly.io + libSQL, AWS Lambda + DynamoDB | ð¢ 倧 |
| Soniox | realtime + async STT | ð¡ äž (WS ãããã³ã« + temp key API) | Deepgram, AssemblyAI, OpenAI Whisper API | ð¡ äž |
| OpenRouter | Gemini 3 Flash + GPT-4o-mini ã«ãŒã¿ | ð¢ äœ (Anthropic/OpenAI/Google çŽå©ãã«åæ¿å®¹æ) | å LLM ãã³ããŒçŽæ¥ | ð¢ 倧 (ãã¹ã¹ã«ãŒ) |
| Apple AppAttest | ãµã€ã³ã¢ããé²åŸ¡ | ðŽ é« (iOS ã®éžæè¢ãšããŠã¯å¯äž) | ãªã | â |
| Apple StoreKit / IAP | 課é | ðŽ é« (iOS ã§ã¯å¿ é ) | ãªã | ð¡ Apple 30% â 15% (Small Biz) |
| ãåå.com SMTP | α æåŸ / é ä¿¡åæ¢ã¡ãŒã« | ð¢ äœ (Resend, SendGrid ã«ä¹ãæã容æ) | Resend, SES, SendGrid | ð¢ 倧 |
| Open-Meteo | å€©æ° | ð¢ äœ (ãªãã·ã§ãã«æ©èœ) | OpenWeatherMap | ð¢ 倧 |
â 匷ã¿
- LLM 㯠OpenRouter ã§æœè±¡å â ã¢ãã«å€æŽã§ 1 è¡ä¿®æ£
- SMTP / 倩æ°ã¯èãã©ããã§èŠãããŠããç§»è¡å®¹æ
â ïž æžå¿µ
- D1 ããã¯ã€ã³ãæå€§çŽããªã¬ãŒã·ã§ã³ + éèšã SQL ã«å¯ããŠãããã SQLite äºæ (Turso ç) 以å€ãžã®ç§»è¡ã¯ã³ã¹ã倧
- iOS 㯠Apple å šäŸå (åé¿äžå¯èœãããžãã¹ãªã¹ã¯ãšããŠç¹ã蟌ã)
ð TODO
- D1 ã®ã¹ããŒã â Turso (libSQL) ç§»è¡ã³ã¹ãã詊ç®ããŠãã
- Soniox é害æã« Whisper API ãžãã©ãŒã«ããã¯ããçµè·¯ã®è©Šäœ
𧪠7. æè¡è² åµ & æ¢ç¥ã® TODO â ãªãã¡ã¯ã¿åªå é äœ
ð çŸç¶: äžèЧ (grep ç±æ¥ + ã¬ãã¥ãŒæã«è¿œèš)
| åªå 床 | é ç® | å Žæ | åè |
|---|---|---|---|
| ðŽ é« | IAP JWS çœ²åæ€èšŒ | backend/src/iap.ts:12 | x5c chain + issuer/bundleId/env æ€èšŒãæªå®è£ ãæ¬ãªãªãŒã¹å å¿ é |
| ðŽ é« | backend stg ç°å¢ | â | shiwake-daily-api-stg ãåã |
| ð¡ äž | LP waitlist ã® rate limit | lp/functions/api/waitlist.ts | SPAM 察ç |
| ð¡ äž | conversation_log ã® TTL | backend/migrations | ç¡å¶éã«äŒžã³ã |
| ð¡ äž | åé€ã«ã¹ã±ãŒã (GDPR) | backend å šäœ | DELETE /api/me + å šããŒãã« cleanup |
| ð¢ äœ | ãã³ã㬠URL ã® XXXXXX | lp/functions/_lib/templates.ts:68,270 | UI ããå·®ãæ¿ãå¯èœãªã®ã§ã³ãŒã倿ŽäžèŠ |
| ð¢ äœ | legacy invitation redeem | backend/src/index.ts | 䜿ã£ãŠãªããã°åé€ |
ð éçšã«ãŒã«
- æ°èŠ TODO ã¯ããã«è¿œèš (該åœç®æã®ãœãŒã¹ã³ãŒã ã«
// TODOæ®ã + ããã«ãšã³ããªè¿œå ) - è§£æ¶æã¯ããŒãã«è¡ãåé€ (å±¥æŽã¯ git ã§è¿œãã)
- ã¬ãã¥ãŒäŒã¯ååæããšãåªå 床ãèŠçŽã
ð¡ ãã®ããŒãžã¯éçããã¥ã¡ã³ãã§ããããŒã¿ãœãŒã¹ã¯ admin/index.html å
ã®ããŒãã³ãŒãã
ã¢ãŒããã¯ãã£ã«å€æŽããã£ããšã㯠PR ã§æŽæ°ããŠãã ããã
ð 仿§æž
iOS å
š 11 ç»é¢ã®æ©èœèŠä»¶ (FR) / éæ©èœèŠä»¶ (NFR) / 飿º API / ããŒã¿ã¢ãã«ãç»é¢åäœã§æŽçã
暪æèŠä»¶ãš API ã«ã¿ãã°ãæ«å°Ÿã«äœµèŒãæŽæ°ã¯ admin/index.html ã® data-tab="docs" ã»ã¯ã·ã§ã³ã
â ïž å®è£ ã®ã£ãã (調æ»ã§çºèŠ)
POST /api/feedbackã®ãµãŒãå®è£ ãèŠåœãããªã â iOS ã®FeedbackButton.swiftãšAPIClient.submitFeedbackTextã¯åŒãã§ãããbackend/src/index.tsã«è©²åœãã³ãã©ç¡ããfeedbackè¡ã¯çŸç¶/api/meals/:id/flag-calorieçµç±ã§ã®ã¿äœãããã- Soniox é害æã« Apple SF ãžã®èªåãã©ãŒã«ããã¯ç¡ã â Premium ãŠãŒã¶ã¯ Soniox temp key ååŸå€±ææã«ãšã©ãŒæèšãåºãã ãã§é²é³ç¶ç¶äžèœã
â ãªã³ããŒãã£ã³ã° â ååèµ·åããæåã® 1 åã®èšé²ãŸã§
OnboardingView ios/Sources/Views/OnboardingView.swift
ããã¯ããŒã 1 é ç®ã ãã§ãµã€ã³ã¢ãããå®äºãããã¹ãã©ãã·ã¥çç»é¢ãèåŸã§ AppAttest signup (宿©) / æ§ invite redeem (ã·ãã¥ã¬ãŒã¿) ãèªåå®è¡ã
TRIGGERPrivacyConsent ééåŸ & token äžåšæ©èœèŠä»¶ (FR)
- FR-OB-01ããã¯ããŒã 1 ãã£ãŒã«ãã®ã¿è¡šç€º / 空çœä»¥å€ãå¿ é å
- FR-OB-02ãã¯ãããããã¿ã³ã§
autoSignUp(displayName)ãèµ·å - FR-OB-03éä¿¡äžã¯ ProgressView + ãã¿ã³ disabled + ã©ãã«ãç»é²äžâŠã
- FR-OB-04å
¥åãã£ãŒã«ãã« 0.2s åŸ auto-focusã
submitLabel=done - FR-OB-05AppAttest signup (宿©) â token/userId ã Keychain ä¿åãPhase B å®è£ æž
- FR-OB-06ãµã€ã³ã¢ãã確å®ã®ç¬éã«
needsProfileSetup=trueãå è¡ã»ãã (ãã©ã€ã鲿¢)
éæ©èœèŠä»¶ (NFR)
- NFR-OB-P1ãµã€ã³ã¢ãã㯠P50 < 2s (challenge + attest + signup ã® 3 åŸåŸ©)
- NFR-OB-S1access_token 㯠Keychain (kSecClassGenericPassword, service
jp.newmeta.shiwake.auth) ä¿å - NFR-OB-A1
preferredColorScheme(.dark)åºå®ããã¿ã³ã¯ 44pt 以äžã®ã¿ããé å - NFR-OB-X1æåŸ ã³ãŒãæŠå¿µããŠãŒã¶ãŒã«èŠããªã (UI ããå®å šæé€)
PrivacyConsentView ios/Sources/Views/PrivacyConsentView.swift
ååèµ·åæã«ãã©ã€ãã·ãŒèŠçŽ 4 é ç®ãšã«ã¡ã©åæåäœã®éžæãæç€ºãåæååŸã
TRIGGER@AppStorage("privacy_agreed_v1") == false ã®èµ·åæ©èœèŠä»¶ (FR)
- FR-PC-01ååŸæ å ± / 第äžè éä¿¡ / ä¿åæé / ã»ãã¥ãªã㣠㮠4 ãµããªãŒè¡šç€º
- FR-PC-02ã«ã¡ã©èµ·åèšå®ãã©ãžãªã§ 2 æ (ãã©ã€ãã·ãŒåªå
/ 峿®åœ±)ã
camera_instant_readyã«ä¿å - FR-PC-03ãå
šæãèªããã§
PrivacyPolicyFullViewã sheet 衚瀺 - FR-PC-04ãåæããŠã¯ããããã§
privacy_agreed_v1=true - FR-PC-05å šæã¯ 8 ç« (ååŸæ å ±, å©çšç®ç, 第äžè éä¿¡, ä¿åæé, ãŠãŒã¶ãŒæš©å©, ã»ãã¥ãªãã£, 倿Ž, é£çµ¡å )
- FR-PC-06é£çµ¡å
contact_app@newmeta.co.jpãæç€º
éæ©èœèŠä»¶ (NFR)
- NFR-PC-L1åæ æ³/GDPR æ³å®ã§ãååŸé ç®ã»å©çšç®çã»ç¬¬äžè æäŸã»ä¿åæéã»ååå ããå¿ é èšèŒ
- NFR-PC-L2åæãã©ã°ã¯ããŒãžã§ã³ä»ã (
privacy_agreed_v1) ã§æ¹å®æã«ååæå¯èœ - NFR-PC-A1Dynamic Type å¯Ÿå¿ (.subheadline),
fixedSizeã§æè¿ãä¿æ - NFR-PC-S1HTTPS/WSSãKeychain ä¿åãææå
ProfileSetupView ios/Sources/Views/ProfileSetupView.swift
7 ã¹ããããŠã£ã¶ãŒã (身é·âäœéâäœèèªâ掻åéâé£äºç®æšâç®æšäœéâéææ¥)ãé³å£°äžå¿ + æå ¥åãã©ãŒã«ããã¯ã
TRIGGERãµã€ã³ã¢ããçŽåŸ or auth.needsProfileSetup=trueæ©èœèŠä»¶ (FR)
- FR-PS-017 ã¹ãããã progress bar ã§å¯èŠå
- FR-PS-02ãã€ã¯ãã¿ã³é·æŒã (DragGesture) ã§é²é³ â é¢ããŠéä¿¡
- FR-PS-03STT äºåæµ: Premium=Soniox WebSocket / Free=Apple SFSpeechRecognizer
- FR-PS-04èªèããã¹ãã
/api/setup/parseãž â value_double/level/goal/date_iso/skipped ãæœåº - FR-PS-05çµæãå¹ãåºãã§ã身é·ã¯ 170.5cm ã§åã£ãŠã?ã圢åŒã§å確èª
- FR-PS-06æå ¥åã¢ãŒãã«: wheel picker / ãªã¹ã / DatePickerã芪æåã«å€§ããªæ±ºå®ãã¿ã³
- FR-PS-07bodyFat / activity 㯠"ã¹ããã" å¯ããã以å€ã¯åå ¥å匷å¶
- FR-PS-08ç¯å²ããªããŒã·ã§ã³: èº«é· 100-220cm, äœé 20-300kg, äœèèª 3-70%
- FR-PS-09ãã£ã©ã¯ã¿ãŒ (idle/listening/thinking) ã 160pt åºå®é«ã§è¡šç€º
- FR-PS-10å®äºæã« TDEE ãšç®æšã«ããªãŒã
Bmr.tdee + TargetCalories.deriveã§ã¯ã©ã€ã¢ã³ãç®åº - FR-PS-11
PUT /api/me/targetsã§èº«é·/äœèèª/ç®æšäœé/ææ¥/é£äºç®æš/ã«ããªãŒãäžæ¬ä¿å - FR-PS-12åæã«
POST /api/weightsã§åæäœéã 1 ä»¶èšé² - FR-PS-13ä¿åæåæã«å
šé¢ç·ãã§ã㯠overlay ã 1.8s 衚瀺ããŠãã
markProfileComplete() - FR-PS-14æªå ¥åé ç®ãããã°æåã®æªå ¥å step ã«èªåã§æ»ã
éæ©èœèŠä»¶ (NFR)
- NFR-PS-P1
/api/setup/parse㯠OpenAI strict-mode JSON è§£æãP50 < 1.5s - NFR-PS-A1è§ŠèŠ: é²é³éå§ 0.9 / 忢ãã« / å®äº success notification
- NFR-PS-X1声ãåºããªãå Žé¢ã®ãã©ãŒã«ãã㯠(æå ¥å wheel) ãå š step ã§æäŸ
- NFR-PS-X2é³å£°æ¥ç¶å€±ææã¯ errorMessage 衚瀺ã isRecording=false ã«æ»ã
PaywallView ios/Sources/Views/PaywallView.swift
ProfileSetup çŽåŸã«æ¿å ¥ããã 1 ã¶æç¡æ â Â¥490/æ ã®ãµãã¹ã¯ãªãã·ã§ã³å§èªãApple èŠçŽæºæ ã
TRIGGERprofileSetup å®äº & subscriptionStatus != "premium_active"æ©èœèŠä»¶ (FR)
- FR-PW-01ããããŒãShiwake Premium / æåã® 1 ã¶æã¯ç¡æã
- FR-PW-024 ãããã£ãã (é¢ä¿æ§æ·±å / é³å£°ç¡å¶é / èšæ¶ / é«ç²ŸåºŠã«ããªãŒ) ã icon+desc ã§èšŽæ±
- FR-PW-03äŸ¡æ Œã«ãŒã: Â¥0/æåã® 1 ã¶æ + ãã®åŸ
displayPriceèªåæŽæ° - FR-PW-04CTAã1 ã¶æç¡æã§ã¯ããããâ
StoreKitManager.purchase()â JWS ã/api/iap/verify - FR-PW-05ã賌å
¥ã埩å
ãã§
StoreKit.restore()+ refreshProfile + Premium ç¢ºèªæ onContinue - FR-PW-06èŠçŽ 3 é ç®ã caption ã§æç€º (è§£çŽæ¹æ³ / 24h å / Apple ID å ±æ)
- FR-PW-07product æªããŒã or è³Œå ¥åŠçäžã¯ãã¿ã³ disable
- FR-PW-08賌å
¥å€±æ (userCancelled é€ã) ã¯
store.lastErrorãããã¹ã衚瀺
éæ©èœèŠä»¶ (NFR)
- NFR-PW-L1App Store èŠçŽ: äŸ¡æ Œã»èª²éé »åºŠã»èªåæŽæ°ã»è§£çŽæ¹æ³ã»èŠçŽãªã³ã¯ãå¿ é æ²èŒ
- NFR-PW-S1ã¬ã·ãŒãæ€èšŒã¯ç«¯æ«ã§ãªãå¿
ã
/api/iap/verify(ãµãŒãåŽ JWS æ€èšŒ) - NFR-PW-X1StoreKit æªããŒãæã¯ Â¥490/æ ã®ãã©ãŒã«ããã¯è¡šèš
â¡ èšé²ã«ãŒã (ã³ã¢ããªã¥ãŒ) â èµ·åå³èšé²ã®å·®å¥ååºé
CaptureView ios/Sources/Views/CaptureView.swift
ã¡ã€ã³ç»é¢ãã«ã¡ã©åžžæèµ·å + å·Šäžãã€ã¯é·æŒã + å³äžã«ã¡ã©ã¿ãã + ã¢ã·ã¹ã¿ã³ãå¹ãåºã + çŽè¿ 6 ä»¶ãã«ã
TRIGGERéåžžèµ·å (èªèšŒæž & profileSetup å®äº)æ©èœèŠä»¶ (FR)
- FR-CAP-01å
šç»é¢
CameraPreviewãåžžæçšŒåãäžéš 34% çœè§äžžãäžååã¯å·Šå³ 50:50 ã®ãã€ã¯/ã«ã¡ã©å€å®ãšãªã¢ - FR-CAP-02
cameraInstantReady=falseãªãã«ã¡ã©ã« UltraThinMaterial overlayã1 ã¿ããã§è§£é€ â ããã¯ãªã³ 4 é ãã©ã±ããã2 ã¿ããç®ã§capturePhoto() - FR-CAP-03å·Šãã€ã¯ãšãªã¢ DragGesture: é·æŒã â startCapture â é¢ã㊠stop â
/api/ingestãž transcript - FR-CAP-04STT äºåæµ: useApple=!isPremium ã§ Apple SF / Soniox ãåæ¿
- FR-CAP-05äžéšãã£ã©ã¯ã¿ãŒ (idle/listening/thinking) + å¹ãåºã + 尻尟ãé²é³äžã¯å°»å°Ÿãé ã
- FR-CAP-06å¹ãåºãããã¹ãã¯ã©ã³ãã æœéžããŒã« (æé垯 à 芪å¯åºŠ Ã ç¶æ ã§ 50+ ãã¿ãŒã³)
- FR-CAP-07æéåž¯å¥æšæ¶ (æ/åå/æŒ/ååŸ/å€/å€/æ·±å€) ã Lv1-2=ãã / Lv3+=ãã ã§æœéž
- FR-CAP-08äžã¹ã¯ã€ã (> 40px) ã§ãã£ã©/å¹ãåºããæãããã¿ãpullDownTab ã§åŸ©åž°
- FR-CAP-09çŽè¿ 6 ä»¶ã® meal/weight ããã« (æçå+kcal / äœé+kg) ã§å³åŽçžŠäžŠã³ã仿¥/æšæ¥/äžæšæ¥ ã©ãã«ä»ã
- FR-CAP-10èšç®äž meal ãå«ãŸããé㯠3s ééã§ reloadRecentMeals ã polling
- FR-CAP-11
capturePhoto: AVCapturePhoto ã/api/meals(multipart) ã« POST - FR-CAP-12
/api/ingestã® advance_event ã¬ã¹ãã³ã¹ã§ AdvanceEventModal ã sheet 衚瀺 - FR-CAP-13persona_comment ãããã°åªå ãç¡ã count=0 ãªã "èšé²ãªã"ãcount>0 ãªã "N ä»¶èšé²ãã"
- FR-CAP-14èšé²æåæã¯ success haptic +
.shiwakeDataChangedã 0/4/8/15s ã§è€æ°å post - FR-CAP-15ååèµ·åã®ã¿ 0.6s é
å»¶ã㊠CaptureTutorialOverlay 衚瀺 (
hasSeenCaptureTutorial=trueã§ææ¢) - FR-CAP-16
scenePhase=.backgroundæã« camera.stop()ã埩垰æ start + ããã¹ãããåæå¹å + æšæ¶å·®æ¿
éæ©èœèŠä»¶ (NFR)
- NFR-CAP-P1ingest 1 次å¿ç P50 ç®æš (persona_comment å³è¿åŽ)ãmealLookup ã¯
waitUntilã§éåæ - NFR-CAP-P2ã«ã¡ã©èµ·åããæåã®ãã¬ãŒã ãŸã§ P50 < 500ms (camera.start ã task ã§äžŠåå®è¡)
- NFR-CAP-S1ãã©ã€ãã·ãŒ: èµ·åæããã¬ã©ã¹ãscenePhase å€åã§ç¢ºå®ã« stopããã¡ã€ã³ããŒè§£é€ã¯ã»ãã·ã§ã³æ¯ã«ãªã»ãã
- NFR-CAP-A1è§ŠèŠ: é²é³éå§ 1.0 / 忢 1.0 / æå .success / 倱æ .error / éè«æç« .selectionChanged
- NFR-CAP-A2ããã¯ãªã³æ 㯠cornerSize 44 / strokeWidth 5 ã§èŠèªæ§ç¢ºä¿
- NFR-CAP-O1ãªãã©ã€ã³: token åãã¯
bubblePhrase(.authExpired)ãé信倱æã¯.sendFailed - NFR-CAP-M1è§ŠèŠ
prepare()ã onAppear / onEnded æ¯ã«åŒãã§ã¬ã€ãã³ã·æå°å
CaptureTutorialOverlay ios/Sources/Views/CaptureTutorialOverlay.swift
åå CaptureView 衚瀺æã«åºã 3 ã¹ããã overlay (å°å ¥ / ãã€ã¯ / ã«ã¡ã©)ã
TRIGGERhasSeenCaptureTutorial=false ã§ CaptureView 衚瀺 0.6s åŸæ©èœèŠä»¶ (FR)
- FR-TUT-01é»å¹ 72% + äžå€®ã¡ãã»ãŒãžã«ãŒã + äžéšç¢å°ã§ 3 ã¹ãããæ¡å
- FR-TUT-02step1: ãã€ã¯é å (ç»é¢äž 25% x) ãž arrow.down + ããŠã³ã¹ã¢ãã¡
- FR-TUT-03step2: ã«ã¡ã©é å (ç»é¢äž 75% x) ãž arrow.down
- FR-TUT-04ãã¿ã³: æ»ã / 次㞠/ (æçµ)å§ãã
- FR-TUT-05ãå§ãããã§
hasSeenCaptureTutorial=trueä¿å â èªåæ¶å» - FR-TUT-06Settings ã®ããã¥ãŒããªã¢ã«ãå衚瀺ãã§ AppStorage ãåé€ããã°å衚瀺
éæ©èœèŠä»¶ (NFR)
- NFR-TUT-A1æå¹ã¿ããã¯ç¡èŠ (誀é·ç§»é²æ¢)ãã¹ãããã€ã³ãžã±ãŒã¿ããã衚瀺
- NFR-TUT-A2ç¢å°ã¯
ArrowBounceã§äžäž 10pt æ¯å (泚ç®èªå°)
AdvanceEventModal ios/Sources/Views/AdvanceEventModal.swift
é¢ä¿æ§ã¬ãã«ææ Œ (Lv N â N+1) ã®ç¥çŠã¢ãŒãã«ãAI ããã® opening line ãæŒåºè¡šç€ºã
TRIGGER/api/ingest ãŸã㯠/api/me ã advance_event ãè¿ããæãAuthStore.pendingAdvanceEvent çµç±
æ©èœèŠä»¶ (FR)
- FR-AEM-01ããŒã fill (84pt) ã
spring(0.6, 0.55)ã§ãã§ãŒãã€ã³ + æ¡å€§ - FR-AEM-02Lv ãããé·ç§» "Lv N â Lv N+1" ãç¢å°ã§è¡šç€º
- FR-AEM-03
event.openingLineã pink 8% èæ¯ + 16pt medium ã§äžå€®è¡šç€º - FR-AEM-04ãããããšãããã¿ã³ (pink ã°ã©ã Capsule) ã§
onDismiss - FR-AEM-05sheet detents [.medium, .large]
- FR-AEM-06衚瀺åŸ
AuthStore.pendingAdvanceEvent=nilã«å³ã¯ãªã¢ (äºéè¡šç€ºé²æ¢)
éæ©èœèŠä»¶ (NFR)
- NFR-AEM-X1one-shot é ä¿¡: ingest waitUntil ã§ advance ãæ±ºãŸã£ããæ¬¡ã® /api/me ã§ 1 åã ãå±ãå³ NULL
- NFR-AEM-A1è§ŠèŠé£åãªã (èŠèŠã¢ãã¡ã§ååãªæŒåºåŒ·åºŠ)
⢠æ¯ãè¿ã â ã°ã©ã / å±¥æŽ / ç·šéã® hub
SummaryView ios/Sources/Views/SummaryView.swift
ã°ã©ã (äœé / ã«ããªãŒ) + 7 æ¥ãµãŒã¯ã« + æ¥å¥å±¥æŽäžèЧãç·šé / åé€ / åèšç® / ã«ããªãŒèª€ãå ±åã® hubã
TRIGGERã¡ã€ã³äžéšãã°ã©ãããã¿ã³ã§ fullScreenCoveræ©èœèŠä»¶ (FR)
- FR-SUM-01ãµããªããã㌠(ã¿ã€ãã« + èšå®æ¯è»)
- FR-SUM-02todaySection: 7 æ¥ãµãŒã¯ã« (ç®æš vs å®çžŸ) + æå/æ¶è²»ãã°ã« + éžææ¥ã®é£äºã«ãŒãäžèЧ + ã«ããªãŒããŒ
- FR-SUM-03longTermSection: äœé/ã«ããªãŒã°ã©ãã®ãã°ã«ãæé day/week/month/6month/yearãã°ã©ãå ã¿ããã§ selectedDate åæ¿
- FR-SUM-04äœéã°ã©ãã¯
weight_kg + body_fat_percent(dual axis æ³å®) - FR-SUM-05å±¥æŽã»ã¯ã·ã§ã³: meal/weight/note ãæå»éé ããŒãžãæ¥ä»ã©ãã«åºåããæå€§ 50 ä»¶ãè¡å ãŽãç®± (1 ã¿ããã§ â/à 㮠2 段é確èª)
- FR-SUM-06é£äºåã®ã¿ããã§ editingMealSheet (rename) èµ·åãä¿åã§
POST /api/meals/:id/renameâ mealLookup åèšç® â æ¥œèŠ³çæŽæ°åŸ polling - FR-SUM-07é£äºè¡ã®ã!ãã§ flag-calorie alertããã¯ããã§
POST /api/meals/:id/flag-calorieâ äžéš toast 2.5s - FR-SUM-08äœéè¡ã¿ããã§ EditWeightSheetContentãä¿åã§
POST /api/weights/:id/update - FR-SUM-09åé€: meal/weight/note ã DELETEãæ¥œèгçã«ç»é¢ããæ¶ã
- FR-SUM-10èšç®äž meal ãããéã¯æ¥œèŠ³å€ (
optimisticMeals) ãä¿æãããµãŒãèšç®å®äºãŸã§äžæžããé²ã - FR-SUM-11
.shiwakeDataChangedéç¥ + refreshTimer ã§èªåãªããŒã - FR-SUM-12FeedbackButton ããªãŒããŒã¬ã€è¡šç€º (à ãã¿ã³å³é£)
- FR-SUM-13CloseButton (å·Šäž 78pt å) ã§ fullScreenCover dismiss
éæ©èœèŠä»¶ (NFR)
- NFR-SUM-P1
/api/meals(æéæå®) ã§ 1 åååŸãã¯ã©ã€ã¢ã³ãåŽã§æ¥å¥éèš - NFR-SUM-P2ç·šéæã¯
processingIdsã«è¿œå ããŠãä»åãä¿®æ£äžâŠãoverlay 衚瀺 - NFR-SUM-A1DynamicTypeãè¡ã¢ã¯ã»ã³ãã® heart ã¢ã€ã³ã³ã¯ 13pt ç³»
- NFR-SUM-O1ãšã©ãŒã¯ç»é¢äžéšã«
err:...monospace 9pt ã§éçºè åã衚瀺
FeedbackButton ios/Sources/Views/FeedbackButton.swift
SummaryView ã® Ã é£ã«åžžé§ãããã«ãé·æŒãã§ Apple SF ãæåèµ·ãããããã¹ãã /api/feedback ã«éä¿¡ã
æ©èœèŠä»¶ (FR)
- FR-FB-01暪é·ãã«ãéçºãã£ãŒããã㯠/ é·æŒãã§éä¿¡ããé·æŒãäžã¯èµ€è² + scale 1.05
- FR-FB-02LongPressGesture(0.2s) ã§ startRecordingãDragGesture.onEnded ã§ stopRecording â upload
- FR-FB-03é²é³äžã¯ç»é¢äžå€® 18% äœçœ®ã«å€§ã㪠transcript ã«ãŒã衚瀺 (鿬¡æŽæ°)
- FR-FB-04ã¢ããããŒãäžã¯ ProgressView ã¢ã€ã³ã³ããã« opacity 0.5ãdisable
- FR-FB-05çµæããŒã¹ã: æå (ç·) / èãåãã (æ©) / 倱æ (èµ€) ã 2.5s 衚瀺
- FR-FB-06Apple Speech (
LocalSpeechRecognizer) ã§å®å šãªã³ããã€ã¹æåèµ·ãã (ã³ã¹ããŒã)
éæ©èœèŠä»¶ (NFR)
- NFR-FB-P1ããã¹ãã®ã¿éä¿¡ (é³å£°ãã¡ã€ã«ç¡ã) ã§åž¯åã»ã³ã¹ãåæž
- NFR-FB-A1è§ŠèŠ: é²é³éå§ medium 1 å
- NFR-FB-O1空 transcript ã¯ããŒã¹ãã§ç¥ããéä¿¡ã¹ããã
⣠èšå® / ããåºç€ â ãããã£ãŒã«åç·šéãš Premium 管ç
SettingsView ios/Sources/Views/SettingsView.swift
ãããã£ãŒã«ç·šé / ç®æšåèšå® / ã«ã¡ã©èšå® / é¢ä¿æ§è¡šç€º / Premium 管ç / Facts é²èЧ / æåŸ çºè¡ / ãã°ã¢ãŠãã»ãªã»ããã
TRIGGERSummaryView å³äžã®æ¯è»ãã fullScreenCoveræ©èœèŠä»¶ (FR)
- FR-SET-01ãããã£ãŒã«: ããã¯ããŒã / èº«é· / äœèèªç / çŸåšäœé / BMR (ç®åº) / 掻åé / é£äºç®æš / TDEE (ç®åº)
- FR-SET-02ç®æš: ç®æšäœé / éæææ¥ (DatePicker, 仿¥ä»¥é) / ç®æšã«ããªãŒ (èªåç®åº)
- FR-SET-03ã«ã¡ã©èšå® Toggle:
camera_instant_readyãåæ - FR-SET-04é¢ä¿æ§: heart 5 段ãLv ã® Picker (ãããã°çš)ãåŒã³æ¹è¡šç€ºã奜æåºŠ pt 衚瀺
- FR-SET-05Premium ã¹ããŒã¿ã¹è¡šç€º + Apple ãµãã¹ã¯ç®¡çãªã³ã¯ + ãããã°åæ¿ Picker
- FR-SET-06ããã¥ãŒããªã¢ã«ãå衚瀺ãã§
hasSeenCaptureTutorialåé€ â dismiss - FR-SET-07ã¢ã·ã¹ã¿ã³ããèŠããŠããããš:
user_factsã 30+ ã«ããŽãªã©ãã«ã§è¡šç€ºãswipeActions ã§åé€å¯ - FR-SET-08æåŸ
ã³ãŒã: èªåã®ã³ãŒã衚瀺 + ãä»äººãæãã³ãŒããçºè¡ãã§
POST /api/inviteâ ShareLink - FR-SET-09ãã°ã¢ãŠã (Keychain ã¯ãªã¢) / åæèšå®ãããçŽã / ã¢ããªå®å šãªã»ãã ã® 3 段éç Žå£æäœ
- FR-SET-10FloatingSaveButton: ç·šéäž or hasUnsavedChanges ã®æã®ã¿è¡šç€º
- FR-SET-11ç·šéäž (focusedField != nil) 㯠CloseButton ãé ã (誀ã¿ãã鲿¢)
éæ©èœèŠä»¶ (NFR)
- NFR-SET-S1ç Žå£æäœã¯ ConfirmationDialog ã§å¿ ã確èª
- NFR-SET-X1å€å€åæ€ç¥ã¯
origXxxã¹ãããã·ã§ãããšã®æ¯èŒã§ hasUnsavedChanges ç®åº - NFR-SET-A1ããã¹ãç·šéäžã®ä¿åãã¿ã³åªå 床ãé«ãããà ãã¿ã³ãšæä»å¶åŸ¡
RootView ios/Sources/Views/RootView.swift
ããã²ãŒã·ã§ã³åºç€ãCaptureView ãå šç»é¢ã«çœ®ããäžéšãã°ã©ãããã«ãã SummaryView ã fullScreenCover ã§éãã
TRIGGERèªèšŒæž & profileSetup å®äº ã§è¡šç€ºæ©èœèŠä»¶ (FR)
- FR-RV-01ZStack(.bottom): CaptureView + äžéšã°ã©ããã¿ã³ (ultraThinMaterial Capsule)
- FR-RV-02sheet enum (settings, chart) ã fullScreenCover ã§è¡šç€º
- FR-RV-03ã·ãŒãé·ç§»ã¢ãã¡ãŒã·ã§ã³ã
transaction.disablesAnimations=trueã§æå¶ (ç¬æåæ¿) - FR-RV-04
scenePhase=.backgroundã§ã·ãŒããèªå close - FR-RV-05sheet nilânon-nil æ
.shiwakeLeftCaptureãéã§.shiwakeReturnedToCaptureã post (ã«ã¡ã© stop/start å¶åŸ¡) - FR-RV-06CloseButton (å·Šäž 78pt) ãš FloatingSaveButton (å³äž 78pt) ã® 2 åå©çšã³ã³ããŒãã³ãæäŸ
éæ©èœèŠä»¶ (NFR)
- NFR-RV-P1èšå® / ãµããªé·ç§»ã¯ã¢ãã¡ç¡å¹åã§ã峿ç»é¢åæ¿ãäœæ
- NFR-RV-S1ããã¯ã°ã©ãŠã³ãæã®ã·ãŒãèªå close ã§æå³ãã¬ç»é¢æ®çã鲿¢
†暪æèŠä»¶ â ç»é¢æšªæã§é©çšããã NFR
ð èªèšŒ / ã»ãã¥ãªãã£
- NFR-X-AUTH-01 Bearer ããŒã¯ã³æ¹åŒ (
Authorization: Bearer <hex32>)ãauth.ts ã®requireUserã§ SELECT id FROM users WHERE access_token=? - NFR-X-AUTH-02 access_token 㯠iOS Keychain (kSecClassGenericPassword, service
jp.newmeta.shiwake.auth) - NFR-X-AUTH-03 ãµã€ã³ã¢ãã㯠AppAttest çµç± (
/api/auth/challengeâ DCAppAttestService â/api/auth/signup)ãDEBUG ãã«ãã®ã¿æ§ invite redeem ãžãã©ãŒã«ãã㯠- NFR-X-AUTH-04 æ¢ãã°ã€ã³æã® invite URL 㯠iOS åŽã§æ©æ return (ç¡é§ãªåŸåŸ©åé¿)
- NFR-X-AUTH-05 IAP 㯠Apple JWS ã
/api/iap/verifyã§ãµãŒãåŽæ€èšŒ (端æ«åŽã¬ã·ãŒãæ€èšŒã¯ä¿¡çšããªã) â 眲å chain æ€èšŒã¯ TODO - NFR-X-AUTH-06 Apple Server-to-Server éç¥çšã«
/api/iap/notification(no auth, çœ²åæ€èšŒã¯ TODO) - NFR-X-AUTH-07 AppAttest ã® Apple å ¬ééµ + nonce æ€èšŒã§åœç«¯æ«ã®å€§éãµã€ã³ã¢ããã鲿¢ (Phase A å®äº)
- NFR-X-AUTH-08 ããã³ãåé¢: å
š D1 ã¯ãšãªã§
user_idã WHERE å¿ é (æŒããããã°å³ä¿®æ£å¯Ÿè±¡)
â¡ ããã©ãŒãã³ã¹
- NFR-X-PERF-01
/api/ingest1 次å¿ç (persona_comment) 㯠P50 < 2sãmealLookup ã¯ctx.waitUntilã§éåæ - NFR-X-PERF-02 OpenAI prompt cache ã
contextBundleã§æŽ»çšãã·ã¹ãã ããã³ãããå®å®å - NFR-X-PERF-03
meal_nutrition_cache(0007) ã§ååæçã®åèšç®ãåé¿ - NFR-X-PERF-04 CaptureView ã¯èšç®äž meal æ€åºæã®ã¿ 3s pollingãå®äºã§èªå忢
- NFR-X-PERF-05 Cloudflare Workers ãšããžå®è¡ + D1 prepared statement
- NFR-X-PERF-06
weather_cache(0011) ã§å€éš API åŒã³åºãã 1h ããŒã«ã«ãã£ãã·ã¥
ð¡ å¯çšæ§ / é害ææå
- NFR-X-AVAIL-01 OpenRouter/OpenAI é害æ: persona_comment ã¯ãã©ãŒã«ããã¯å®åæãmealLookup 㯠notes "èšç®äž" ã®ãŸãŸçã眮ã
- NFR-X-AVAIL-02 Soniox é害æ (Premium): temp key ååŸå€±æ â ãšã©ãŒæèšãApple SF ãžã®ãã©ãŒã«ããã¯ã¯æªå®è£ (å°æ¥èª²é¡)
- NFR-X-AVAIL-03 D1 é害æ: 5xx ãè¿ã iOS ã¯
bubblePhrase(.sendFailed)ã§å詊è¡èªå° - NFR-X-AVAIL-04 R2 é害æ:
image_r2_keyã null ã§ meal ãäœæ â åŸè¿œãåã¢ããæªå®è£ - NFR-X-AVAIL-05
/healthãšã³ããã€ã³ãã§çåç¢ºèª ({ ok, ts }) - NFR-X-AVAIL-06
/api/meãäžæå€±æããŠãã¯ã©ã€ã¢ã³ãã¯ååå€ãä¿æ (profileLoaded=falseã®é splash)
ð èšæž¬ / ãã° / ç£æ»
- NFR-X-OBS-01
api_usage_log(0012): OpenRouter/OpenAI/Soniox ã® tokenã»ã³ã¹ãã»ã¢ãã«å¥å©çšãã° - NFR-X-OBS-02
request_timing(0013): ãšã³ããã€ã³ãæ¯ã®ã¬ã€ãã³ã·ååž - NFR-X-OBS-03
conversation_log(0014): user çºè©± / assistant è¿çã®å¯Ÿè©±å±¥æŽ (persona æ¹åçš) - NFR-X-OBS-04
feedback(0018): éçºè åããã£ãŒãããã¯æ +[calorie_flag]ãã¬ãã£ãã¯ã¹ã§ã«ããªãŒèª€ãå ±å - NFR-X-OBS-05 Admin
/api/admin/stats: OpenRouter credits / å¹³åæ¥æ¬¡ã³ã¹ã / runway æ¥æ° / ãã£ãŒãããã¯çŽè¿äžèЧ - NFR-X-OBS-06 Admin 㯠Basic èªèšŒ (
app.use("/api/admin/*", basic auth))
ð ãã©ã€ãã·ãŒ / æ³å
- NFR-X-PRIV-01
privacy_agreed_v1åæãã©ã° + å šæããªã·ãŒ (8 ç« , å¶å® 2026-05-24) - NFR-X-PRIV-02 åæååŸé ç®: ããã¯ããŒã / èº«é· / äœé / äœèèª / é£äº (text/voice/photo) / å©çšæå» / ããã€ã¹æ å ±
- NFR-X-PRIV-03 第äžè éä¿¡: é³å£°èªè (Soniox) / AI è§£æ (OpenAI/OpenRouter) / ã€ã³ãã© (Cloudflare) ãæç€º
- NFR-X-PRIV-04 åé€äŸé Œã¯
contact_app@newmeta.co.jpããµãŒãåŽåé€ã¯æåãªãã¬ãŒã·ã§ã³ (DELETE /api/me 㯠TODO) - NFR-X-PRIV-05 ã«ã¡ã©ã¯ããã¯ã°ã©ãŠã³ãå³ stopãèµ·åæããã¬ã©ã¹ã default (ãã©ã€ãã·ãŒåªå )
- NFR-X-PRIV-06 feedback ã¢ããããŒãã¯ããã¹ãã®ã¿ (é³å£°éä¿å)
ðž ã³ã¹ã / ãªãœãŒã¹
- NFR-X-COST-01 OpenRouter æé¡äºç®ã¯ admin/stats ã®
runway_daysã§ç£èŠ - NFR-X-COST-02 Free ãŠãŒã¶ã¯ STT ã Apple SF (ã³ã¹ããŒã) ã«åŒ·å¶ãPremium ã®ã¿ Soniox
- NFR-X-COST-03
meal_nutrition_cacheã§éè€æçã® LLM åŒã³åºããåæž - NFR-X-COST-04 prompt cache 掻çšã§ OpenAI å ¥åããŒã¯ã³ã 50%+ åæžçã
- NFR-X-COST-05 R2 ã¯é£äºåçã®ã¿ä¿ç®¡ (é³å£°ã¯ä¿åããªã)
- NFR-X-COST-06 ãã£ãŒãããã¯ã¯ããã¹ãååŸéä¿¡ (é³å£°ãã¡ã€ã«éä¿¡ãªã)
â¿ åœéå / ã¢ã¯ã»ã·ããªãã£
- NFR-X-I18N-01 æ¥æ¬èªã®ã¿ (Locale ja_JP åºå®)ãAsia/Tokyo TZ ã§æ¥ä»åŠç
- NFR-X-A11Y-01 Dynamic Type ã semantic font (
.subheadline / .caption) ã§å¯Ÿå¿ - NFR-X-A11Y-02 VoiceOver: æå
¥åãã¿ã³ã«
accessibilityLabel("æå ¥åãã") - NFR-X-A11Y-03 ã¿ããé å 44pt 以äžãæè (CloseButton/SaveButton 㯠78pt å)
- NFR-X-A11Y-04 è§ŠèŠãã£ãŒããã㯠(impact/notification/selection) ã§èŠèŠã«äŸããªãç¶æ éç¥
ð é ä¿¡ / ãªãªãŒã¹
- NFR-X-REL-01 iOS: TestFlight â App Store Connect çµç±é åž (IAP 㯠In-App Subscription, StoreKit2)
- NFR-X-REL-02 ããã¯ãšã³ã: Cloudflare Workers (
wrangler deploy), D1 ãã€ã°ã¬ãŒã·ã§ã³wrangler d1 migrations apply - NFR-X-REL-03 LP: Cloudflare Pages,
lp/functions/api/waitlist.tsã§ Edge Function - NFR-X-REL-04 Admin: Cloudflare Pages
shiwake-admin.pages.devãSPA + Basic auth lookup via backend - NFR-X-REL-05 AASA:
/.well-known/apple-app-site-associationã§ Universal Link/invite/* - NFR-X-REL-06
privacy_agreed_v1ã®ããŒãžã§ã³çªå·ã§åæã®äžä»£ç®¡ç
⥠API ã«ã¿ãã° â ããã¯ãšã³ãå šãšã³ããã€ã³ã (37)
èªèšŒ / æåŸ
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/auth/challenge | none | AppAttest çš nonce çºè¡ (5min TTL) |
| POST | /api/auth/signup | none | AppAttest assertion ãæ€èšŒ â user äœæ + access_token è¿åŽ |
| POST | /api/invite | none | å¿å invite code çºè¡ (DEBUG ãã©ãŒã«ãã㯠/ Settings ä»è æåŸ ) |
| POST | /api/auth/redeem | none | invite code ã access_token ãšäº€æ (409=æ¶è²»æž) |
| GET | /invite/:code | none | Universal Link çšãã¢ããªæªã€ã³ã¹ããŒã«æã®ãã©ãŒã«ãã㯠|
| GET | /.well-known/apple-app-site-association | none | iOS Universal Link èšå® |
ãŠãŒã¶ãŒ / ãããã£ãŒã«
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| GET | /api/me | Bearer | ãããã£ãŒã« + streak + subscription + trial + pending_advance_event äžæ¬ååŸ |
| PUT | /api/me/targets | Bearer | 身é·/äœèèª/ç®æšäœé/ææ¥/ç®æšã«ããªãŒ/é£äºç®æš/ããã¯ããŒã äžæ¬æŽæ° |
| PUT | /api/me/affinity | Bearer | é¢ä¿æ§ points çŽæ¥ã»ãã (ãããã°) |
| PUT | /api/me/subscription | Bearer | ãµãã¹ã¯ç¶æ æååæ¿ (ãããã°) |
| POST | /api/setup/parse | Bearer | wizard çšé³å£° transcript â value/level/goal/date_iso æœåº |
| GET | /api/me/facts | Bearer | ã¢ã·ã¹ã¿ã³ããåŠç¿ãã user facts äžèЧ |
| DEL | /api/me/facts/:id | Bearer | fact åé€ |
é£äº (meals / ingest)
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/meals | Bearer | åçããŒã¹ meal ç»é² (multipart: image+transcript) |
| GET | /api/meals | Bearer | æéæå®ã§ meal äžèЧååŸ |
| DEL | /api/meals/:id | Bearer | meal åé€ |
| PUT | /api/meals/:id | Bearer | meal ç·šé (raw_text ç) |
| POST | /api/meals/:id/rename | Bearer | æçåå€æŽ â mealLookup åèšç® |
| POST | /api/meals/:id/flag-calorie | Bearer | ã«ããªãŒèª€ãå ±å â feedback ããŒãã«ã« [calorie_flag] æ¿å ¥ |
| POST | /api/ingest | Bearer | ããã¹ã transcript ã§é£äº/äœé/éè«ãä»åã (persona_comment + advance_event 忢±) |
| POST | /api/ingest/voice | Bearer | é³å£°ãã€ã㪠â STT â ingest çµ±å |
| POST | /api/auth/soniox-temp-key | Bearer | Premium çš Soniox WebSocket äžæããŒçºè¡ |
äœé / éå / ããŒã
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/weights | Bearer | äœéèšé² (kg + note + body_fat_percent) |
| POST | /api/weights/:id/update | Bearer | æ¢åäœéã®äžæžãç·šé |
| GET | /api/weights | Bearer | æéæå®ååŸ |
| DEL | /api/weights/:id | Bearer | åé€ |
| GET | /api/exercises | Bearer | éåèšé² æéæå®ååŸ |
| DEL | /api/exercises/:id | Bearer | éååé€ (POST 㯠ingest çµç±ã§èªåäœæ) |
| GET | /api/notes | Bearer | ããªãŒããŒãäžèЧ |
| DEL | /api/notes/:id | Bearer | ããŒãåé€ |
| GET | /api/summary/weekly | Bearer | éå» 7 æ¥ã®éèš (meal_count/total_kcal/avg_kcal + weights é å) |
ãã£ãŒãããã¯
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/feedback | Bearer | ããã¹ããã£ãŒãããã¯éä¿¡ â ïž ãµãŒãå®è£ æªç¢ºèª |
IAP / ãµãã¹ã¯ãªãã·ã§ã³
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| POST | /api/iap/verify | Bearer | StoreKit JWS ããµãŒãåŽæ€èšŒ â users.subscription_status æŽæ° |
| POST | /api/iap/notification | none (çœ²åæ€èšŒ) | Apple Server-to-Server éç¥åä¿¡ |
Admin / LP / ãã«ã¹
| METHOD | PATH | AUTH | çšé |
|---|---|---|---|
| GET | /api/admin/stats | Basic | OpenRouter credits / æ¥æ¬¡ã³ã¹ã / runway / ãã£ãŒãããã¯çŽè¿äžèЧ (/api/admin/* å šäœã Basic) |
| POST | /api/waitlist (LP) | none (IP hash) | ã¡ã¢ã + source + UA + referrer ã waitlist ããŒãã«ã«ä¿å |
| GET | /api/admin/waitlist (LP) | Basic | waitlist ååŸ (JSON or CSV ãšã¯ã¹ããŒã) |
| GET | / | none | ã«ãŒã (å°ç·è¿ã) |
| GET | /health | none | { ok: true, ts: now } |
ð¡ ãã®ããŒãžã¯éçããã¥ã¡ã³ãã§ããããŒã¿ãœãŒã¹ã¯ admin/index.html ã® data-tab="docs" ã»ã¯ã·ã§ã³ + admin/admin.js ã® docFilter* 颿°ã
ç»é¢è¿œå ã»API å€æŽæã« PR ã§æŽæ°ããŠãã ããã