🧩 코칭 엔진 모듈 명세 · ⑧/⑧

📅 ⑧ 주간계획 (작전층 오케스트레이터)

일요일에 지난 4주를 의사처럼 종합해 7일치 구체 계획(dish 회전·macro·커리큘럼·거울)을 미리 굽는 작전 사령부 — 일간은 슬롯으로 소비

← 문서 허브 · 통합 아키텍처 지도 · 코칭 엔진 모듈 명세
🗺️ 이 문서는 통합 아키텍처 지도의 모듈 ⑧을 코드 단위로 펼친 명세입니다 — 데이터 파이프라인·입출력 예시·가드·천장. 코드 근거: lib/coachWeekly.ts · app/api/cron/coach/route.ts (2026-06-20 실측).
📅 한 줄 정의. 이 모듈은 일요일 1회 enrichWeeklyPlan(synth, ctx)(coachWeekly.ts:520)가 일요일 종합(① 신호선택기 runWeeklyPlanning·Sonnet)이 잡은 빈약한 닻(mission_target=식품군 1개) 위에서 다른 모듈을 전부 호출(오케스트레이션)해 7일치 구체 계획(PlanDetail)을 굽고 weekly_plans.plan_detail jsonb에 영속하는 작전층입니다. 추천엔진(②) popularDishesFor로 카테고리를 dish로 펼치고, 네트워크(⑦) verifiedCousinsOf로 도전 사촌을 붙이고, 영양평가(④) bmiBand/growthTracking으로 macro 트랙을 깔고, 진척(⑤) goals로 anti-stall을 가시화합니다. LLM 0콜·순수함수·throw 시 호출자(cron)가 null degrade. 월~토 일간(① 신호선택기)은 pickPlanSlot(coachWeekly.ts:576)으로 그날 결핍 데이터에 맞춰 슬롯을 소비합니다.

비개발자용 — 왜 필요했고 무엇을 하나

이 모듈은 '한 주 식단 작전을 미리 짜는 주방장'입니다. 식당에 비유하면, 일요일 밤에 주방장이 다음 한 주(월~토) 무엇을 어떤 순서로 낼지 메뉴판을 통째로 미리 짜두는 일입니다. 왜 필요했냐면, 예전 주간계획은 너무 빈약했습니다. 한 주의 목표가 '콩류'라는 식품군 카테고리 딱 하나였고, 그러니 월요일도 콩류→두부, 화요일도 콩류→두부… 몇 주 내내 '콩류 도돌이표'가 됐습니다. 이사님 지적의 핵심이 이것입니다.

그래서 이 모듈은 주간계획을 진짜 작전 사령부로 격상했습니다. 일요일 종합이 '이번 주는 콩류'라고 큰 방향만 잡으면, 그 직후 이 모듈이 다른 모든 엔진을 한꺼번에 불러 7일치 구체적인 음식 회전표를 미리 굽습니다. '콩류'를 두부조림→단호박죽→(사촌 도전) 검은콩→콩나물무침… 처럼 실제 음식 이름 7칸으로 펼쳐, 같은 식재료가 이틀 연속 나오지 않게 짭니다. 두 갈래를 번갈아 섞습니다 — '부족한 걸 보충하는 칸(supply)' 둘에, '잘 먹는 음식의 검증된 사촌으로 도전하는 칸(challenge)' 하나. 이 도전 칸이 '콩류 3주 도돌이표'를 구조적으로 끊습니다.

여기에 세 가지를 더 깝니다. ① 키·몸무게(BMI/성장곡선)를 봐서, 또래보다 가볍거나 성장이 더딘 아이면 '탄단지(단백·지방·열량) 보강 트랙'을 켜고 회전표 맨 앞에 고기·계란을 끼워넣습니다(격주). ② 부모 교육 '수업'이 정체되면 그게 보이게 표시해, 일간 편지가 '지난주 X에서 이번주 Y로'라고 진도를 말할 수 있게 합니다. ③ '오늘의 영양 거울 한 줄'도 7일치로 미리 짭니다 — 결핍이 콩류 하나뿐이면 매일 같은 콩류 클로징이 앵무새가 되니 격일로만 넣습니다. 마지막으로, 이 표는 어디까지나 가이드입니다. 월~토 그날 아침에 일간 엔진이 그날 기록 데이터로 다시 검수해서, 이미 채워진 칸은 건너뛰고 다음 유효한 칸으로 적응합니다(유연성 가드).

🥗 영양·BMI는 이렇게 처리합니다.
탄단지(macro) 트랙은 추측이 아니라 우리가 계산한 성장 수치만 씁니다. 영양평가(④)가 준 BMI 밴드(bmiBand)와 성장곡선 추종도(growthTracking)를 봅니다. 저체중이거나 성장곡선이 자기 채널에서 아래로 이탈(주의·경고)이면 '탄단지 보강군'(고기·계란·콩류·유제품)을 켜서 supply 회전표 앞에 고기류를 주입하고, 부모 편지엔 growthTrackToPhrase가 만든 한 구절(숫자·체중·살·BMI 단어 0)을 넣습니다. 과체중·비만이면 거꾸로 간식 절제로 안내합니다. 정상이면 트랙을 끕니다(불필요한 잔소리 0). 그것도 격주로만 노출합니다(arcWeek이 짝수인 주). 단, 솔직히: 대부분의 자녀는 키·몸무게·성별을 아직 입력하지 않아 이 트랙이 사실상 꺼져 있습니다(아래 ⑤ 천장3 참조).
🧒 운영 예시(아린·5세). 아린의 일요일 종합이 잡은 닻: 주력 타깃=콩류(집에서 단일 결핍), 비타민A채소도 약함, 잘 먹는 음식=소고기·계란·감자·미역, BMI 밴드=정상. 예전이라면 월~토 내내 '콩류→두부'였습니다. 이 모듈은 그 직후 7칸 회전표를 굽습니다: 두부(콩류·supply)→단호박(비타민A채소·supply)→검은콩(콩류·challenge·소고기의 검증 사촌)→콩나물(콩류·supply)→… 같은 식재료가 이틀 연속 없이 채워집니다. 월요일 아침, 일간 엔진은 슬롯 인덱스 (daySeed+cidHash)%n으로 한 칸을 고르되, 그 칸이 supply인데 그날 보니 콩류가 이미 채워졌다면 다음 유효 칸으로 회전합니다.

구현된 내용 — 실제 함수·타입·데이터흐름

전부 lib/coachWeekly.ts·app/api/cron/coach/route.ts 실측(2026-06-20). 1~6은 일요일 1회, 7은 월~토 매일.

🎬 산출 타입 — PlanDetail (coachWeekly.ts:79)

필드타입의미 · 운영 (file:line)
schemaVersion1신선도·하위호환 게이트 (:80)
targetRotationPlanSlot[≤7]슬롯별 구체 타깃. 각 슬롯: ingredient·cookedName·dishes[≤2]·group·track('supply'|'challenge')·via·pairLiked?·weeklyEstFreq?·level?·reason? (:57-69). supply 2 : challenge 1 인터리브·연속 동일 식재료 금지 (:550-557)
supplyPoolstring[]buildIngredientPool(②) 결핍군 대표 식재료 ≤8. macro active면 boostGroups 대표를 풀 에 주입 (:524-529)
challengePool{ingredient,cousinOf}[≤6]잘 먹는 식재료(≤8) 각각의 verifiedCousinsOf(⑦) 중 안 먹는·PLAN_MEAL_GROUPS 소속 사촌 (:531-542)
poolMode'supply'|'challenge'|'mixed'buildIngredientPool의 결핍 심각도 모드 (:567)
macroTrackMacroTrackactive·band·reason(lowWeight|growthLag|overweight)·boostGroups·boostDishes·snackRestraint·phrase·cadenceWeek (:71-78, 생성 buildMacroTrack:485)
curriculum객체focusUnit·focusLabel·standby[]·stalledOut·graduatedTo·lever — anti-stall 스냅샷 (:86, 산출 :560-564)
mirrorScheduleMirrorSlot[≤7]일자별 영양 거울 — deficitGroup·line·kind('deficit'|'covered'|'macro') (:70, 생성 buildMirrorSchedule:500)
deficitGroups / coveredGroupsstring[]결핍 식품군(missing∪homeMissing) / 충족군 — 거울·진단 근거 (:88-89)

🔧 핵심 함수

함수 (file:line)역할 — 실측
enrichWeeklyPlan(synth, ctx)
coachWeekly.ts:520
⭐모듈 핵심 오케스트레이터. ① buildMacroTrack(ctx) 먼저(고기류 주입 결정에 필요) → ② buildIngredientPool로 supplyPool, macro active면 boostGroups 대표를 앞에 주입 → ③ likedIngredients의 verifiedCousinsOf로 challengePool → ④ mkSupply/mkChallenge로 dish 붙여 슬롯화, slots.length%3===2면 challenge·연속 동일 식재료 skip로 7-슬롯 회전 → ⑤ goals에서 curriculum 산출 → ⑥ buildMirrorSchedule. 순수·LLM 0콜, throw 시 cron이 catch→null (route.ts:617-629)
pickPlanSlot(detail, p)
coachWeekly.ts:576
⭐일간 소비(유연성 가드). base=(daySeed+cidHash)%n에서 시작. valid()= supply 슬롯의 group이 deficitNow(그날 결핍)에 없으면 무효(challenge는 항상 유효). 2-pass: ①유효+최근 미추천(recentIngredients dedup·메추리알 수렴 차단) ②유효(최근 무시) ③폴백 base. PlanSlotPick{slot, slotIndex, mirror, macroPhrase} 반환. detail null/빈 회전이면 null
buildMacroTrack(ctx)
coachWeekly.ts:485
저체중(band==='저체중') OR 성장더딤(heightTrack/weightTrack.status==='주의'|'경고')→active:true·boostGroups['고기·계란','콩류','유제품']·boostDishes·phrase(growthTrackToPhrase)·reason. 과체중/비만→snackRestraint:true. 정상→active:false. cadenceWeek=arcWeek%2===0(격주). 입력 band·track 전부 growth_logs+sex 의존
buildMirrorSchedule(deficits, covered, macro, attendsDaycare, freqMap)
coachWeekly.ts:500
7-루프. macro 주면 i=3에 macro 슬롯. 결핍 ≥2 라운드로빈(같은 군 연속 금지). 결핍 1개=격일 쿨다운(K-04b: 짝수 슬롯만 결핍줄·홀수는 coveredLine). 결핍 0=covered. mirrorLineFor는 3변형×daySeed 회전·deficitDishFor로 dish 1개 주입(어린이집 톤 분기)
runWeeklyPlanning(i)
coachWeekly.ts:308
(① 모듈) 일요일 Sonnet 종합 → WeeklySynthesis(mission_target·target_pool·budget·goals·behaviorGoal). 후보 화이트리스트 강제(candidateTargets)·ARFID push=0·실패 시 coldSynth 폴백. enrich의 입력 thin 닻을 만든다
applyFocusFatigue(goals, focusHistory)
coachWeekly.ts:178
(⑤ 흡수) 같은 유닛 직전 2주(TH.focusMaxStallWeeks) 연속 focus·step 전진 0이면 standby 강등·차순위 승격. resolveGoals:288에서 호출 → goals가 이미 거친 채로 enrich에 들어옴 → curriculum.graduatedTo가 가시화
planFromWeekly(p)
coachWeekly.ts:365
월~토 닻 실행(슬롯 소비와 별개 경로). 안전 인터럽트 우선, 비-food 레버는 LEVER_SCENARIO 프레임, food는 mission_target 잠금+채근 캡(PUSH_MOVES·ledger). effectiveLever(F-16)로 유닛 피벗 추종. ⭐plan_detail 슬롯 소비는 이 함수가 아니라 route가 직접 pickPlanSlot 호출

🆕 이번 세션 신규가 이 모듈에 닿는 지점 (실측)

신규 기능이 모듈 내 위치 · 배선주의 / 한계
선호계량화(미상→liked 오판 차단)이 모듈 바깥(lib/preferenceQuantification.ts)에서 산출. route(:368-374)가 confidentLiked·dislikedInglikedSeed를 만들고, 그 likedSeedEnrichContext.likedIngredients로 enrich에 주입(route.ts:621)이 모듈은 likedIngredients를 받기만 함. challenge 풀(:531-542)이 이 시드의 사촌을 끈다. 아린 confidentLiked=0이면 challenge 풀 빈약(천장 참조)
A 슬롯 근거(어드민 '왜 타깃')⭐이 모듈 내부. mkSupply/mkChallenge(:546-547)가 PlanSlot.reason·weeklyEstFreq·level 산출 — supply=결핍군 X 보급(주 N회·red), challenge=잘 먹는 Y의 검증 사촌. gsMap(group→signal) 룩업EARNED. route(:956)가 context.planSlot로 어드민 전사
F-18 슬롯본문봉합소비 측(route :862-872). pickPlanSlot 슬롯의 dishes[0]slotDish·cookedNameslotFood를 작문기에 must-weave. 거울 음식=항상 슬롯 음식(deficitDishFor 두부 디폴트 제거)이 모듈은 슬롯을 제공, 봉합 로직은 route/작문기. environment 레버 주는 본문 음식 없음 → 거울에 슬롯 음식 소프트 노출
hardStall 피벗(18일 강제전환)이 모듈 (lib/curriculum.ts:222 advanceProgress·TH.hardStallDays:18). 이 모듈의 curriculum 블록은 stalledOut(:562)으로 그 결과를 읽어 가시화한다 — enum 정합 OK(Goal.status==='stopped', curriculum.ts:237이 set)⚠️ 룩업 enum은 정상. 한계는 enum이 아니라 goals에 'stopped' 상태가 실제로 도달하느냐(focusHistory/applyFocusFatigue 경유) — table-stage 졸업 채널 부재로 피벗 자체가 드묾(천장1/2·P0-D)
#5 거울 쿨다운(출현빈도 격일)소비 측(route :893-899 mirrorCooldownDue). 이 모듈의 buildMirrorSchedule K-04b(단일결핍 격일)와 2층 방어이 모듈은 7일치 라인을 굽고, 출현 자체의 격일화는 route 쿨다운이 담당
#6 도입회전·#7 macro#7 macro 입력 기아: route(:229-232) profileNudge가 키·몸무게 입력을 유도(매 3일)해 이 모듈 buildMacroTrack에 데이터를 공급하려 함. heightTrack/weightTrack은 격주 게이트와 무관하게 계산(route.ts:388,398) → EnrichContext로 전달⚠️ 데이터 미입력이면 track=null → macroTrack always off(천장3)
P0-D child_daily_state미구현. route.ts·coachWeekly.ts에 해당 테이블/읽기 없음(grep 0건). 환경신호 입력으로 table-stage step을 전진시키는 채널이 아직 없음이 모듈은 일요일 focus를 굽고 피벗만 가능, 졸업(step 전진)은 0 (천장1/2 근본)

설계 아키텍처 시각화 — 입력테이블 → 가공함수 → 출력컨텍스트키

일요일(굽기) 파이프라인과 월~토(소비) 파이프라인. 가공 함수가 어느 모듈을 호출하는지 화살표로.

┌─────────────────────────────────────── 일요일 1회 (dow===0, ?date QA는 차단) ───────────────────────────────────────┐
│                                                                                                                        │
│  [입력 테이블]                          [① 신호선택기]                       [EnrichContext 조립 · route.ts:619-626]   │
│  meal_logs ─┐                                                                                                          │
│  growth_logs┼─▶ computeGroupSignals ─▶  runWeeklyPlanning(Sonnet) ─▶ synth ──┐                                         │
│  children   │   computeFoodGroups        (thin 닻: mission_target=콩류,        │  groupSignals · likedIngredients(=    │
│  curriculum_│   bmiBandFor/growthTracking budget.lever, goals[focusFatigue])  │  likedSeed←선호계량화) · freqMap ·     │
│  progress   │   quantifyPreferences ─▶ likedSeed                              ├─ deficitGroups · coveredGroups ·     │
│             │   (confidentLiked·disliked)                                     │  band · heightTrack · weightTrack ·   │
│             ▼                                                                 │  goals · focusHistory · arcWeek ·     │
│         (식재료/거부/장소 집계)                                              ▼  attendsDaycare                       │
│                                                                  ╔══════════════════════════════════════╗            │
│                                                                  ║  enrichWeeklyPlan(synth, ctx)        ║  순수·LLM0 │
│                                                                  ║  coachWeekly.ts:520                   ║            │
│                                                                  ╚══════════════════════════════════════╝            │
│                                                                                  │                                     │
│              ┌─────────────────┬─────────────────┬──────────────┬──────────────┼──────────────┐                     │
│              ▼ 1) buildMacro   ▼ 2) supplyPool   ▼ 3) challenge ▼ 4) 7-슬롯    ▼ 5) curric.   ▼ 6) mirrorSchedule  │
│         buildMacroTrack    buildIngredientPool  verifiedCousins  회전(2:1·    goals→focus/   buildMirrorSchedule  │
│         ④ bmiBand·         ② (+macro 고기류    Of ⑦ (사촌)      연속금지)     standby/        (K-04b 격일·         │
│         growthTracking     앞 주입)                              mkSupply/      graduatedTo     라운드로빈·         │
│         (band·track)                                            mkChallenge    (⑤ 가시화)      macro i=3)          │
│              └─────────────────┴─────────────────┴──────────────┴──────────────┴──────────────┘                     │
│                                                                  ▼                                                     │
│                                                  PlanDetail{schemaVersion,targetRotation,supply/challengePool,        │
│                                                  poolMode,macroTrack,curriculum,mirrorSchedule,deficit/covered}       │
│                                                                  ▼                                                     │
│                                                  weekly_plans.plan_detail jsonb  (additive·SSOT=기존컬럼·throw→null)  │
└────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
                                                                  │ (read 월~토)
┌──────────────────────────────────────────── 월~토 매일 (route.ts:851-853) ────────────────────────────────────────────┐
│  weekly_plans.plan_detail ─▶ pickPlanSlot(detail, {daySeed, cidHash, deficitNow=그날 gMissing∪gHomeMissing,          │
│                              coachWeekly.ts:576           recentIngredients}) ─▶ PlanSlotPick{slot,slotIndex,         │
│                              (유연성 vetting: supply 슬롯 group∉deficitNow → 다음 유효 슬롯 회전·challenge 항상 유효)  │
│                                          │                                                                            │
│       ┌──────────────────┬──────────────┼──────────────────┬─────────────────────┐                                  │
│       ▼ recoIng=         ▼ recoMode=    ▼ slotDish/slotFood ▼ mirrorLine          ▼ growthMirror=                    │
│       slot.ingredient    slot.track     (F-18 본문 must-    (slot.mirror.line·    macroPhrase ?? growthMirrorCtx     │
│                                          weave)             출현 쿨다운)                                              │
│       └──────────────────┴──────────────┴──────────────────┴─────────────────────┘                                  │
│                                          ▼                                                                            │
│                              ⑥ 작문기(LetterInput) + context.planSlot(어드민 전사)                                   │
│                              · plan_detail null/!defMature → 옛 buildIngredientPool 회전 폴백(degrade)                │
└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
오케스트레이션 소스 요약 — 이 모듈은 직접 계산하지 않고 다른 모듈을 호출한다: ②추천엔진(popularDishesFor·buildIngredientPool·cookedName·groupOfIngredient·GROUP_INGREDIENTS) · ⑦네트워크(verifiedCousinsOf·strongPairsOf) · ④영양평가(bmiBand·growthTracking·growthTrackToPhrase) · ⑤진척(UNITS.label·leverForUnit·applyFocusFatigue). 입력은 전부 우리가 실측한 값(환각 0). 순환 의존 없음(이 모듈들은 coachWeekly를 import하지 않음).

작동 예시 — 아린(5세) before / after 한 케이스

실데이터 입력 → enrich 통과 → 출력. before=이 모듈 없던 시절, after=현재 라이브.

BEFORE (모듈 없음). 일요일 종합이 mission_target='콩류' 하나만 잡음. 월~토 일간이 매번 GROUP_INGREDIENTS['콩류'][0]=두부로 환원 → 본문 클로징 "콩류(두부)를 곁들여보세요"가 몇 주 내내 축자 반복. 거울도 매일 "어린이집 덕에…콩류" 동일 줄. 도전·다양성 0.
📥 INPUT — EnrichContext (아린·route.ts:619)
// synth.mission_target='콩류', budget.lever='food', goals 포함
{
  "groupSignals": [
    { "group": "콩류", "level": "red", "weeklyEst": 0 },
    { "group": "비타민A채소", "level": "yellow", "weeklyEst": 1 },
    { "group": "고기·계란", "level": "green", "weeklyEst": 6 }
  ],
  "likedIngredients": ["소고기","계란","감자","미역"],  // ⭐ likedSeed(선호계량화)
  "deficitGroups": ["콩류","비타민A채소"],
  "coveredGroups": ["곡류","고기·계란","생선·해산물"],
  "band": "정상",           // ⭐ 정상 → macro off
  "heightTrack": null,      // ⭐ growth_logs 없음 → null (실데이터에선 흔함)
  "weightTrack": null,
  "goals": [
    { "unit_id": "exposure-savings", "priority": 1, "status": "focus" },
    { "unit_id": "table-stage", "priority": 2, "status": "standby" }
  ],
  "focusHistory": [{ "unit_id": "table-stage", "stepAdvanced": false }],
  "arcWeek": 4, "attendsDaycare": true
}
📤 OUTPUT — PlanDetail (enrichWeeklyPlan 반환)
{
  "schemaVersion": 1,
  "targetRotation": [
    { "ingredient":"두부", "cookedName":"두부", "dishes":["두부조림","순두부찌개"],
      "group":"콩류", "track":"supply", "via":"deficit",
      "weeklyEstFreq":0, "level":"red", "reason":"결핍군 콩류 보급(주 0회·red)" },
    { "ingredient":"단호박", "cookedName":"찐 단호박", "dishes":["단호박죽"],
      "group":"비타민A채소", "track":"supply", "via":"deficit",
      "weeklyEstFreq":1, "level":"yellow", "reason":"결핍군 비타민A채소 보급(주 1회·yellow)" },
    { "ingredient":"검은콩", "cookedName":"삶은 검은콩", "dishes":["콩조림"],
      "group":"콩류", "track":"challenge", "via":"cousin", "pairLiked":"소고기",
      "reason":"잘 먹는 소고기의 검증 사촌(푸드체이닝)" },
    { "ingredient":"콩나물", "cookedName":"데친 콩나물", "dishes":["콩나물무침"],
      "group":"콩류", "track":"supply", "via":"deficit", "reason":"결핍군 콩류 보급(주 0회·red)" }
    // … (연속 동일 식재료 없이 ≤7칸)
  ],
  "macroTrack": { "active": false, "band":"정상", "reason":null, "boostGroups":[], "cadenceWeek":false, ... },
  "curriculum": { "focusUnit":"exposure-savings", "focusLabel":"새 음식 조금씩 노출",
    "standby":["table-stage"], "stalledOut":null,
    "graduatedTo":"exposure-savings", "lever":"food" },   // prevFocus=table-stage≠focus → 졸업 표시
  "mirrorSchedule": [ /* 콩류·비타민A채소 라운드로빈, macro 주 i=3 macro */ ],
  "deficitGroups":["콩류","비타민A채소"], "coveredGroups":["곡류","고기·계란","생선·해산물"]
}
// ── 월요일 일간 소비 (route.ts:852) ──
pickPlanSlot(detail, { daySeed:12, cidHash:5, deficitNow:Set("콩류","비타민A채소") })
//  base=(12+5)%4=1 → slot[1]=단호박(supply·콩류 아닌 비타민A채소·deficitNow에 있음=유효)
//  → recoIng="단호박" · recoMode="supply" · slotDish="단호박죽"(F-18 본문 must-weave)
AFTER (현재 라이브). 같은 '콩류' 닻에서 월=단호박죽, 화=콩조림(소고기 사촌 도전), 수=콩나물무침… 처럼 본문 dish가 매일 갈린다. 어드민엔 슬롯별 reason("결핍군 콩류 보급(주 0회·red)"·"잘 먹는 소고기의 검증 사촌")이 전사된다. 거울은 콩류↔비타민A채소 라운드로빈 + 출현 쿨다운으로 앵무새가 끊긴다.
단, 정직하게 — 같은 닻 W24·W25 비교. 위 예시는 한 주 안의 다양성은 살아난다. 그러나 주 단위로 보면 일요일마다 focus가 CORE_ORDER 다음(exposure-savings)으로 리셋되고 주중 table-stage 정체가 반복돼, W24와 W25의 plan_detail이 사실상 같은 진동을 그린다(focus 졸업이 진짜 step 전진이 아니라 순서 이동). 또 heightTrack=null(흔한 케이스)이라 macroTrack은 always off다. 아래 ⑤에서 다룬다.

남은 문제점 · 해야할 일 — 랄프위검 실측 천장 (연속성 47점, 2026-06-20)

이 모듈 관점에서 정직하게. 각 항목: 증상 → 근본원인 → 해야할일(owner). 미화 금지.

#증상근본원인 (이 모듈 위치)해야할일 · owner
천장 1·2 매주 동일 진동 — W24·W25가 같은 패턴. 한 주 안 다양성은 살았는데 주 단위로는 정체 일요일 종합이 focus를 CORE_ORDER 다음(exposure-savings)로 리셋 → 주중 table-stage 정체 시 일간이 hardStall 피벗 → 다음 일요일 또 리셋. 일요일 의도(plan_detail.curriculum.focus)와 주중 일간 피벗이 어긋나 정체로 귀결. stalledOut 룩업(g.status==='stopped'·coachWeekly.ts:562) 자체는 정상(Goal.status enum='stopped', curriculum.ts:237이 set·'pivoted'는 별개 타입 UnitStatus/ProgressRow라 Goal엔 안 들어옴). 진짜 한계는 goals에 'stopped' 상태가 실제로 도달하느냐다 — table-stage 졸업 채널 부재로 focusHistory에 정체가 누적되지 않아 피벗 자체가 드물고, 그래서 stalledOut이 비는 것. graduatedTo도 순서 이동(focus≠prevFocus)일 뿐 진짜 step 전진이 아님 ① 주간 focus를 '실제로 주중 이기는 유닛'에 맞춘다(일요일 리셋 대신 직전 주 일간이 전진시킨 유닛 우선). ② 피벗을 plan_detail에 확실히 반영: 룩업 enum은 그대로 두고, applyFocusFatigue/focusHistory가 정체를 누적해 goals에 'stopped'가 실제 도달하도록 배선(근본은 P0-D 졸업 채널). owner: 코칭엔진(이 모듈 + curriculum.ts 정합)
천장 3 macroTrack 항상 off(active:false) — 저체중·성장더딤 아이도 고기류 보강 트랙·탄단지 안내 미발동 buildMacroTrack(:485)이 band·heightTrack·weightTrack으로 판정하는데, 이 입력 3개가 전부 growth_logs + sex에 의존(route.ts:388-405 tk()는 첫 측정≠최신 측정 2행이 있어야 track 산출). 실데이터에선 대부분 키·몸무게·성별 미입력 → band=null·track=null → 무조건 정상 분기로 떨어져 트랙 dead. 이번 세션 #7 profileNudge(매 3일 입력 유도)가 입구이나 아직 데이터 유입 전 macroTrack 성장지표 입력 배선을 끝까지: ① profileNudge 효과 모니터링(hasGrowth 전환율) ② 측정 1행만 있어도(채널 추종 불가) BMI 밴드만으로 저체중→boost 부분 활성 허용 검토 ③ growth 미입력 상태를 어드민에 '트랙 입력대기'로 가시화. owner: 코칭엔진 + 데이터 입력 UX
천장 2(파생) 레버 주 본문 미직조 — 슬롯에 음식이 걸려도(특히 environment 레버 주) 본문에 슬롯 음식이 안 짜임 일간 본문 봉합이 레버 분기와 어긋남. environment 레버(_noFood) 주는 본문에 음식 액션이 없어 slotFood=null(route.ts:864-865)이 됨. F-18에서 거울에 슬롯 음식 소프트 노출(:886-887)로 일부 보완했으나, 음식-제안 시나리오(_foodScen)와 brain.useFood=false가 충돌하면 슬롯 강제가 풀려 결핍군 대표(두부)가 다시 이길 여지. 이 모듈은 슬롯을 정확히 제공하지만 봉합은 route/작문기 소관 environment 레버 주에도 슬롯음식 본문 보장: useFood↔scenario↔unit 정합을 단일 게이트로(별도 EPIC). 이 모듈 측 할일은 작음 — 슬롯 track·pairLiked가 봉합 로직에 충분히 전달되는지만 유지. owner: ① 신호선택기/⑥ 작문기 봉합(이 모듈은 슬롯 제공자)
P0-D 졸업 0 — table-stage가 피벗만 되고 step 전진(졸업)이 안 됨 child_daily_state 테이블 미구현(route.ts·coachWeekly.ts grep 0건). 환경신호(식탁 안착·화면 끔 등) 입력 채널이 없어 advanceProgress가 table-stage step을 전진시킬 hitDay를 못 받음 → hardStall 18일 후 강제 피벗만 가능, 졸업은 영영 0. 이게 천장1/2의 단일 뿌리 P0-D child_daily_state 구현: 환경신호 입력 → table-stage step 전진 가능하게. 그래야 주간 focus가 '이긴 유닛'을 잡을 거리가 생김. owner: 코칭엔진(다음 작업)
✅ 성과 (EARNED·사실). plan_detail 9필드 전사targetRotation·supply/challengePool·poolMode·macroTrack·curriculum·mirrorSchedule·deficitGroups·coveredGroups가 일요일 enrich에서 산출돼 weekly_plans.plan_detail에 영속되고, 일간 소비 시 context.planSlot(route.ts:956)으로 어드민 뷰에 전부 전사된다. A 슬롯 근거 정상 — 슬롯별 reason("결핍군 콩류 보급(주 0.8회·red)"·"잘 먹는 두부의 검증 사촌")·weeklyEstFreq·levelmkSupply/mkChallenge(coachWeekly.ts:546-547)에서 산출돼 어드민에 노출된다. 한 주 안 다양성 구조적 보장 — supply 2:challenge 1 인터리브 + 연속 동일 식재료 금지로 '콩류 도돌이표'가 한 주 단위에선 끊겼다. 유연성 가드pickPlanSlot이 그날 결핍으로 supply 슬롯을 vetting해 이미 채운 결핍 재추천을 차단한다.
요약(미화 없이). 작전층 '굽기·전사·근거'는 EARNED. 그러나 주 단위 정체(천장1/2)와 macro dead(천장3)는 미해결이며, 둘 다 단일 뿌리 = P0-D 미구현(졸업 채널 부재)이다. stalledOut 룩업(coachWeekly.ts:562)은 enum 정합 정상(Goal.status==='stopped') — 버그가 아니다. 비어 보이는 원인은 졸업 채널 부재로 goals에 'stopped'가 도달하지 못하는 것이며, P0-D가 풀리면 자연 해소된다.

가드 — 괴식·환각·안전 차단 (보존)

문헌 · 이론 근거 (보존)

이 모듈이 실제로 구현·적용하는 이론(코드 근거).

이론 · 문헌어떻게 적용하나
푸드 체이닝(food chaining)challenge 트랙 — 잘 먹는 음식의 검증 사촌(verifiedCousinsOf·food-graph bridge)을 끌어와 다리를 놓는다(소고기→검은콩). 도돌이표 차단 핵심
반복 노출(HabEat)·단순노출supply 트랙 — 결핍군 대표를 7칸 회전으로 격일~여러 번 재노출. 연속 동일 식재료 금지로 권태 없이 친숙도 축적
USDA·식품군 다양성PLAN_MEAL_GROUPS 8군을 회전·거울 대상으로 — '한 군 편중' 대신 두루
성장곡선 추종(growth channel)growthTracking이 첫 측정으로 백분위 채널을 잡고 z(SDS) 하향 이탈(주의·경고)을 본다. 절대 위치가 아니라 '자기 곡선 추종'으로 macro 트랙 점화 (⚠️ 입력 미충족 시 dead — 천장3)
탄단지 보강(저체중·성장더딤)저체중/성장더딤이면 boostGroups=['고기·계란','콩류','유제품']을 supply 회전 앞에 주입해 '고기류 타깃'화. P10 톤 안내
제한·잔소리 역효과(Satter)·무압력정상 밴드면 macro off(잔소리 0)·macro도 격주. 과체중도 금지가 아니라 간식 절제로 안내
주제 피로 캡(focus fatigue)curriculum 가시화에 applyFocusFatigue 거친 goals 사용 — 같은 유닛 2주 연속 focus·step 0이면 standby 강등 (⚠️ stalledOut 룩업 enum은 정상이나, 졸업 채널 부재로 정체가 goals에 누적되지 않아 노출이 드묾 — 천장1/2·P0-D)
괴식 차단(검증 조합만)dish=popularDishesFor, 사촌=verifiedCousinsOf(verified만), 궁합=strongPairsOf(strong만) → 지어낸 조합 0