🗺️ ENGINE · 8-MODULE ORCHESTRATION

코칭 엔진 통합 지도
모듈 8종이 하루 한 통의 편지를 만들기까지

새벽 크론 한 바퀴 안에서 신호 수집 → 주간 닻 → 커리큘럼 → 일간 두뇌 → 슬롯 → 작문 → 질문이 어떻게 맞물리는지 — route.ts 1,097줄을 실측한 시스템 전체 지도이자 모듈 명세로 가는 nav 허브.

비개발자용 — 편지 한 통이 만들어지는 과정

왜 8개로 나누나

매일 새벽 2시(0 17 * * * UTC), 활성 자녀 한 명마다 아래 흐름이 한 바퀴 돕니다. 한 명의 코치가 하는 일을 8개 부품이 나눠 맡습니다 — 코드를 몰라도 읽히게 정리했습니다.

비유 — 좋은 코치는 혼자 다 하지 않는다. 한 사람이 ⓐ아이 식단 기록을 읽고 ⓑ이번 주에 뭘 가르칠지 정하고 ⓒ어떤 음식으로 다리를 놓을지 고르고 ⓓ오늘 어떤 각도로 말할지 판단하고 ⓔ따뜻한 편지로 옮긴다면 매일 5가지를 동시에 합니다. 밀프레드는 이걸 8개 부품으로 쪼갰습니다. 부품마다 한 가지만 잘하면 되고, 한 부품이 고장 나도 나머지가 폴백으로 편지를 끝까지 발행합니다(try/catch degrade).

① 어제까지의 '아이 상태표' 모으기 신호 수집 · 선호 계량화 · 영양평가
집·기관 식단 기록을 모아 영양 신호등(부족/충분)·잘 먹는 음식·거부한 음식·새 음식 노출 진척을 한 장으로 계산합니다. ⭐'잘 먹는다'는 단정은 완식이 반복된 음식만 — 한 번 먹은 건 '아직 모름'으로 둬서 없는 사실을 지어내지 않습니다.
② 이번 주 작전 정하기 주간 닻 · 커리큘럼
일요일에 이번 주 타깃 식품군·교육 목표·주력 레버(음식이냐 식탁 분위기냐)를 잠급니다. 한 유닛이 18일째 진전이 없으면 강제로 다른 유닛으로 피벗합니다(잔소리 고착 방지).
③ 오늘의 각도 고르기 + 채울 음식 정하기 일간 두뇌 · 추천엔진 · 슬롯
13개 코칭 각도 중 오늘 가장 도움될 1개를 AI가 고르되, 이번 주 작전을 못 벗어나게 코드가 게이트합니다. 채울 음식은 검증된 데이터에서만(예: 콩류 결핍이면 '단호박찜·치즈스틱' 같은 구체 음식) — 매일 다른 음식으로 회전해 두부 도돌이표를 막습니다.
④ 따뜻한 편지로 쓰기 작문(손)
위 재료를 받아 한국어 편지로 옮깁니다. 강요·괴식·과한 소금은 안전가드가 거르고, 발행 전 검증자가 한 번 더 검수합니다.
⑤ 오늘의 질문 → 부모 답변 → 내일 ①로 질문 · 프로브
편지 끝에 오늘의 질문을 답니다. ⭐이번 주 가르치는 유닛의 '신호 프로브'를 던져, 부모 답이 내일 상태표의 양성신호로 흘러 코칭이 점점 그 아이에게 맞춰집니다.
두 가지 핵심 약속 — (가) '무엇을' 줄지는 코드가 결정론으로, '어떻게' 쓸지(문장)만 AI가 → 괴식·환각 0. (나) 판단은 하루 한 번 미리 계산해 저장, 편지는 그걸 읽어 글만 → 1만 명도 감당. 각 부품의 운영·문헌·테이블 상세는 🧩 모듈 명세에, 이 문서는 8개가 한 크론에서 어떻게 맞물리는지를 봅니다.

구현된 내용 — 코드에 실제로 있는 파이프라인

route.ts 실측

전부 web/app/api/cron/coach/route.ts(1,097줄)의 단일 GET 핸들러 안에서 자녀 루프(for (const cid of activeIds)·route.ts:264)로 순차 실행됩니다. 모듈은 @/lib/* 순수 함수로 import되어 결정론 재료를 채우고, LLM은 두뇌(pickActionByBrain)·작문(composeLetter)·질문(generateQuestion) 3곳뿐입니다.

8개 모듈 · 입력 → 함수 → 출력(컨텍스트 키)

#모듈 (lib)핵심 함수 · file:line📥 입력📤 출력 → context상태
0야간 백필
remapMenus
backfillUnmappedMenus()
route.ts:95
menus 있고 ingredients 빈 meal_logs(60일·LLM≤8콜)식재료 백필(영양계산 선행) → backfill 지표live
1신호 수집 + 선호 계량화
nutrition · preferenceQuantification
computeSignals/FoodGroups/Timeseries/GroupSignals·quantifyPreferences()·confidentLiked()·dislikedFoods()
route.ts:368-381
7일 meal_logs(집/기관 분해·place!=='daycare'acceptance_level(0~4)·catMapCoachSignals(reds/missing/refusedExposable)·confidentLiked[]·dislikedIng·coldStartlive⭐신규
2사실·재노출 컴파일러
coachFacts · reexposure
compileFacts()·reexposurePick()
route.ts:423,457
rows·거부→수용 전환(28일)·노출횟수/경과일fc.cards/noteCards/forbidParts·재노출 적기 사실(ts에 push)live
3주간 닻(작전층)
coachWeekly · curriculumUnits · curriculum · coachDaily
runWeeklyPlanning()·enrichWeeklyPlan()·planFromWeekly()·advanceProgress()·pickPlanSlot()
route.ts:596,627,740,680,852
reds·missing·refExposable·stalledTarget(3주)·progByChild·focusHistory·candSignalsweekly_plans 행(mission_target·target_pool·budget.lever·ledger·plan_detailcurriculumDecision·planSlotCtxlive⭐신규
4추천엔진 + 네트워크 조회
coachRecos · graphSource
buildRecoFacts()·buildIngredientPool()·coldStartSeed()·getRecipeFreq/IngredientsLight()
route.ts:760,842,867
likedSeed·target(결핍군)·freqMap·recentRecoIng(회전)recoPoolArr(5)·recoIng(회전)·bridgeFacts·slotFood/slotDishlive⭐신규
5영양 거울 + 간식 + 성장
snack · growth-reference · (mirror)
evaluateSnacks()·growthTracking()·bmiBand()·mirrorCooldownDue()
route.ts:385,401,894
byChild rows·snackBand(BMI)·firstGrowth/latestGrowth·recentMirrorShownsnackText·growthMirror·mirrorLine(쿨다운)·heightTrack/weightTracklive⭐신규
6일간 두뇌(판단)
coachBrain · coachScenarios
buildBrainContext()·pickActionByBrain()·anchorOverrideAllowed()
route.ts:761,771,782
signals·nutritionMirror·recoCandidates·weeklyEchoes·anchorLever·anchorTargetPool·recentUseFood·curriculumSummarybrainPick:BrainAction{scenarioId,useFood} → 닻 게이트 통과 시 precomputed 교체live·LLM
7작문기(손)
coach · coachChronic
composeLetter()·generateLetter/OnboardingLetter()·chronicGuidanceText()
route.ts:925
base(①~⑤ 재료 전부)+precomputed(plan/scenario)+detForbid+slotFood/slotDish{letter, oneliner}+verifyCtx(검증자)+simToPrev/repeatAlertlive·LLM
위 7행 + 질문/프로브 생성(generateQuestion()·pickUnitProbe()·icfqForDate()·route.ts:983-997)을 합쳐 오케스트레이션 단위 8종. 모든 모듈 호출이 try/catch로 감싸져 한 모듈이 throw해도 precomputed = planFor(...)(route.ts:557) 폴백으로 편지를 끝까지 발행합니다(안전 제1원칙).

⭐ 이번 세션(2026-06-19~20) 신규 — 이 모듈에 반영된 것

신규무엇 · file:line오케스트레이션 영향
선호 계량화
신호포착
quantifyPreferences(rs, today)·confidentLikedFrom·dislikedFoods (route.ts:368-370). acceptance_level 컬럼 select 추가(route.ts:112). 미상(null)을 liked로 오판 차단 — 확신=완식 반복+Wilson만.coldStart = confidentLiked.length < 2(372)로 콜드스타트 게이트·likedSeed(373)·dislikedIng 추천 제외(374). 아린: ate_well true 4건이 전부 다른 식재료 → confidentLiked=0 = 콜드스타트(구 누적기는 4로 오판).
targetAccepts 정량화acceptanceLevel(r) != null && lv >= 1 (route.ts:711). 구버전 ate_well !== false가 미상도 진척으로 세던 것을 만짐(≥1) 이상으로 제한.ledger.targetAccepts(743)→ 일요일 synth 스톨 판정 입력. 미상 80% 아동이 영영 '진전 중'으로 보여 두부 반복하던 근원 차단.
hardStall 피벗주간: stalledTarget(STALL_PIVOT_WEEKS=3)(route.ts:49,592)로 타깃 전환. 일간: advanceProgress({...coachedDays, pivotsThisWeek})(680)가 유닛 18일 정체 시 강제 피벗 → adv.goalsAfter 영속화(688-692).한 유닛 18일 진전0이면 주1회 캡 무시 강제전환. table↔exposure 교대.
F-18 슬롯본문봉합
rank1
slotDish/slotFood를 작문기에 must-weave(route.ts:864-865). suppressCousins:!!slotFood(867)·거울음식=항상 슬롯음식(870-889·deficitDishFor 디폴트 제거). _foodScen(859)으로 음식 시나리오엔 _noFood 끔.본문 두부 도돌이표 종결 — 메타=메추리알·본문=두부 이중장부 봉합. 실측 비-콩류 슬롯 12일 두부누수 0.
#5 거울 쿨다운mirrorCooldownDue(recentMirrorShown, {cooldownDays:2, severe}) (route.ts:894). 최근 2일 거울 나왔으면 오늘 생략·심한 결핍(≥2)이면 면제.'어린이집 덕에 영양 채워진다' 줄이 24통 중 17통이던 것 격일화. 무슬롯(plateau) 경로도 동일 적용(mirrorCooldown·920).
#6 도입/추천 회전식재료: recentRecoIng dedup(route.ts:848). 슬롯: pickPlanSlot({recentIngredients})(852). 거울 문장: daySeed 변주 3종(_posV·877-883).같은 식재료 연속 추천·축자 반복(템플릿 마모) 차단. (※도입 새 수렴은 §④ 천장 참조 — 풀 협소로 여전히 잔존.)
#7 macro 트랙growthTracking()·heightTrack/weightTrack(route.ts:398)·planSlotCtx.macroPhrase 우선(916). 성장 권유 매3일 강화(231-232) — 데이터 유입 유도.저체중→고기·계란 macro 슬롯. 단 성장데이터 없으면 트랙 전체 dead(input-starvation·§④ 참조).
A 추천 근거(어드민)context에 planSlot{group,track,via,mirror,macro}·recoIng·recoPool·recoMode 적재(route.ts:956,960). slot.track=supply/challenge·pairLiked(918).어드민이 '결핍군 콩류 보급(주0.8회·red)'·'잘먹는 두부의 검증사촌' 노출.
P0-D 프로브
다리 절반
pickUnitProbe(curriculumDecision?.unit, dayNum)(route.ts:990)·unitProbe를 질문 context에 적재(999). ICFQ 위험일 아닌 날 focus 유닛 1차 신호 프로브를 결정론 질문으로.'답해도 신호로 안 흐르던' 다리의 출력측은 이어짐(질문 발행). 입력측(답→envTablePct7d 등 양성신호 적립)이 미완 = §④ 단일 천장.

발행 게이트 · 안전 불변식 (실측)

가드코드지키는 약속
당일 입력 차단.lte('log_date', dAgo(1)) (route.ts:113)편지는 '어제까지' 확정 데이터로 — 자기참조·미래 차단
닻 종속 overrideanchorOverrideAllowed({anchorLever,sid,fov,triggerOk}) (route.ts:782)비-food 주·트리거 미충족·food override 캡 소진 시 두뇌 시나리오 무시 → 닻 잠금 보존
연속 food 캡recentBrainUseFood.slice(0,_need).filter(Boolean).length >= _need (route.ts:774-775)food주 3일·비food주 2일 연속 음식이면 useFood=false 강제(잔소리 쿨다운)
재사용 동결 방지srcHashtoday 포함(472)·재사용 시 context 전체 보존(498-509)날짜 바뀌면 식단 같아도 새 편지·firstOfWeek 오판 방지(S6)
검증자 + 반복경보verifyCtx(927)·simToPrev>=0.6 ‖ sigRun>=2 → repeatAlert(931)발행 전 의미 검증·자가 유사도 측정 → issues에 노출(사람 제보 의존 탈피)
비용 계측resetUsage()(269)·aggregateUsage→llm_usage upsert(finally·1020)자녀별 LLM 비용 실측 — ₩331/자녀·월

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

파이프라인

한 자녀 루프(route.ts:264~1029) 안의 데이터 파이프라인. 초록=입력 테이블/데이터보라=가공 모듈회색=두뇌(판단)주황=출력(편지).

📒 meal_logs (7일·집/기관)
ingredients·refused·place·acceptance_level·environment
📅 weekly_plans
mission_target·budget.lever·ledger·plan_detail
✉️ coach_letters (3일)
context: scenario·plan·recoIng·mirrorShown·brain.useFood
📈 curriculum_progress · growth_logs
unit step·status · 키/몸무게
▼ 자녀 루프 진입 (route.ts:264)
①신호+선호계량 L368
quantifyPreferences → confidentLiked·coldStart·dislikedIng · computeSignals → reds/missing/refExposable
②사실 컴파일러 L423
compileFacts → factCards·forbidParts · reexposurePick → 재노출 적기
▼ signals · likedSeed
③주간 닻(작전층) L560-754
일요일 runWeeklyPlanningenrichWeeklyPlan(plan_detail) · 평일 planFromWeekly→precomputed · advanceProgress→curriculumDecision(피벗) · pickPlanSlot→planSlotCtx(구체 dish 회전)
▼ precomputed(scenario/plan) · weekCtx{lever,targetPool,ledger} · planSlotCtx
④추천엔진+⑤거울/간식/성장 L760-899
buildIngredientPool→recoPool(5)·recoIng(recentRecoIng 회전) · buildRecoFacts→bridgeFacts(suppressCousins) · slotFood/slotDish · evaluateSnacks·growthTracking · mirrorCooldownDue→mirrorLine
▼ recoCand · nutritionMirror · curriculumSummary
⑥🧠 일간 두뇌 = 판단 L757-801
buildBrainContext(anchorLever·targetPool·recentUseFood 주입)→pickActionByBrain→brainPick{scenarioId,useFood}
anchorOverrideAllowed 게이트 통과 시에만 weeklyReplan(sid)로 precomputed 교체(닻 잠금 보존)
▼ 최종 precomputed · weekCtx.arc · base(재료 전부)
⑦✍️ 작문(손) = composeLetter L925
base + precomputed + detForbid + slotFood/slotDish(must-weave) → 생성 → 안전 재생성 → 검증자(verifyCtx) → 어휘 유사도 재생성 → {letter, oneliner}
▼ upsert
✉️ coach_letters.upsert L962
letter·oneliner·source_hash·context(scenario·plan·weekly·planSlot·brain·curriculum·recoIng·mirrorShown)
❓ daily_questions L1000
pickUnitProbe→unitProbe(P0-D) ‖ ICFQ ‖ generateQuestion
핵심 비대칭(읽는 법) — 화살표는 한 방향이지만 context(coach_letters)는 다음날 루프의 입력으로 되먹임됩니다(recentScenarios·recentPlans·recentRecoIng·recentBrainUseFood·recentMirrorShown·curriculumHist 모두 route.ts:174-188에서 어제 context를 읽어 복원). 즉 편지가 곧 상태 원장 — child_daily_state 테이블은 설계상 미구현이고, 실제로는 coach_letters.context가 그 역할을 대신합니다(§④ 참조).

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

실측

아린(table-stage·콩류 결핍·미상 80%) 한 명이 이번 세션 신규 기능을 통과하며 무엇이 바뀌었는지, F-18 슬롯본문봉합 + 선호계량화 두 케이스로 봅니다.

케이스 1 — 선호 계량화: 콜드스타트 오판 차단

BEFORE (구 누적기)
아린의 ate_well===true가 4건 — 그런데 전부 다른 식재료(각 1회). 구 누적기는 likedIng.length=4로 보고 콜드스타트 미발동. 추천 앵커가 빈약한데도 '잘 먹는 것 많음'으로 오판 → 결핍군 대표 디폴트(콩류→두부)로 무너짐.
AFTER (preferenceQuantification·route.ts:368-372)
quantifyPreferences(rs, today)confidentLiked()는 완식 반복+Wilson 통과만 liked. 아린은 단발 4건이라 confidentLiked = 0coldStart = true(<2). likedSeed = coldStartSeed(servedTop, 6)로 전환 — 자주 차려진 음식을 앵커로 안전 추천. 미상(null)은 더 이상 liked로 새지 않음.

케이스 2 — F-18: 메타·본문 이중장부 봉합

BEFORE (슬롯-본문 이혼)
슬롯이 정한 음식 = 단호박찜(콩류가 아닌 회전 dish). 그런데 작문 프롬프트의 거울 결핍줄이 '콩류'를 호명 → LLM이 결핍군 대표 두부를 본문에 박음.
슬롯(메타): 단호박찜 · 편지(본문): "콩류가 부족하니 두부를…" — 이중장부
AFTER (route.ts:864-889)
slotDish='단호박찜'를 작문기에 must-weave로 강제 + linter 재생성. suppressCousins:!!slotFood로 감자→두부 사촌 끄기. 거울음식=항상 슬롯음식(deficitDishFor 콩류 디폴트 제거) — 슬롯≠결핍군 날엔 거울이 콩류 단어를 빼고 generic-positive로.
슬롯=본문=거울: 단호박찜 단일화. 실측 비-콩류 슬롯 12일 두부누수 0
그런데도 졸업은 0 — 위 두 봉합으로 본문 품질은 회복됐지만, 아린의 table-stage 유닛은 9일째 step 1/2단 고착. 환경 양성신호(envTablePct7d)가 입력되지 않아 advanceProgress의 졸업 조건(passWhen)이 영영 안 풀립니다. 피벗(table↔exposure 교대)만 돌고 진짜 졸업은 0 — 이것이 §⑤ 단일 천장입니다.

남은 문제점 · 해야할 일 — 랄프위검 47점 천장

정직 감사

연속성 47점(2026-06-20). 추이 52→48→44→47. 6개 랭크천장이 전부 단일 뿌리 = P0-D 양성신호 미입력으로 수렴합니다. 이번 세션이 본문 품질(F-18·#5·#6)은 EARNED했으나, 시스템이 학습으로 진전하는 능력은 입력 기근에 막혀 있습니다. 미화하지 않습니다.

EARNED(사실인 성과) — F-18 슬롯본문봉합으로 비-콩류 슬롯 12일 두부누수 0 · 선호계량화로 미상→liked 오판 차단(콜드스타트 정상 발동) · #5 거울 17/24→격일화 · #6 식재료/거울 daySeed 회전. 괴식 0·환각 0·발행 보장(degrade-safe)은 견고. 천장은 '나쁜 편지'가 아니라 '학습이 안 도는' 데서 옵니다.
랭크천장 증상근본원인해야할 일 (owner)
① step 1/2단 19일 고착 + table↔exposure 핑퐁 — 피벗만 돌고 졸업 0 P0-D 단일 뿌리. advanceProgress 졸업(passWhen)은 envTablePct7d 등 양성신호 표본을 요구하는데, 프로브 답이 신호로 적립되는 입력측 다리가 미완. 출력측(질문 발행·route.ts:990)만 이어짐. P0-D 환경신호 입력 완성(코칭엔진) — daily_questions.context.unitProbe 답변을 읽어 child_daily_state(또는 progress.evidence)에 envTablePct7d 적립 → advanceProgress가 졸업 판정. 다음 P0=여기부터.
② F-16 environment 레버 슬롯음식 본문 증발 — 환경 코칭 날 음식 추천이 사라짐 environment 날 _noFood=true면 본문에 음식 액션 없음(route.ts:860). 거울에 슬롯음식 소프트 노출(886-887)로 완화했으나, 환경 양성신호가 안 들어와 environment 유닛 자체가 졸업 못 함 → 음식 트랙 복귀 지연. ①과 동근. 환경신호 입력되면 environment 졸업 → food 레버 자연 복귀. 그 전까지 거울 소프트 노출(현행) 유지로 음식 다양성 최소 보장.
③ macro/성장 트랙 dead — 저체중 탄단지 슬롯이 영영 비활성 input-starvationgrowthTrackingfirstGrowth/latestGrowth 2점이 필요한데(route.ts:394-398) 성장데이터 없으면 heightTrack/weightTrack=null. macro 슬롯(macroPhrase)이 안 켜짐. 이번 세션 #7로 성장 권유 매3일 강화(route.ts:231) 착수 — 데이터 유입 유도. owner=코칭엔진(현행 유지) + 앱 care 화면 키/몸무게 입력 UX(앱). 데이터 들어오면 자동 활성.
④ 추천풀·앵커 협소 — 콜드스타트 Tier2→Tier3, 앵커 단일(돼지불고기) youa 큐레이션 풀이 두부5·달걀6로 협소. coldStartSeed·buildIngredientPool이 뽑을 후보가 적어 회전(#6)을 해도 금세 수렴. P0-B/C 추천풀 확장(코칭엔진+콘텐츠) — Tier2→Tier3 youa 큐레이션 확대 · 앵커 다양화(돼지불고기 단일 탈피). /mealfred-food-mapping 코퍼스 보강과 연계.
⑤ 도입(온보딩) 새 수렴 — 새 음식 도입이 좁은 집합으로 수렴 ④와 동근(풀 협소) + 회전 dedup(recentRecoIng.slice(0,5)·route.ts:848)가 풀이 작으면 곧 소진. CORE_ORDER 확장트랙 4종 편입(C 온보딩3주차)은 됐으나 풀 자체가 얕음. ④ 풀 확장과 함께 해소. lever 태그 정합(디버그) 점검 — 슬롯 track(supply/challenge) 라벨이 어드민과 어긋나는지 확인(코칭엔진).
⑥ 본문 템플릿 회전 부재 — plateau/무브 카피 반복 거울줄은 daySeed 변주(#6·route.ts:877-883)했으나 무브·plateau 카피풀이 얕아 반복 인상 잔존. plateau는 trigger:()=>true 안전망이라 자주 도달. 무브/plateau 카피풀 확충(콘텐츠+코칭엔진) — daySeed 변주 슬롯을 거울 외 무브 문장으로 확대.
정직한 결론 — 6개 천장 중 ①②③⑤가 P0-D 양성신호 미입력 한 뿌리로 수렴하고, ④⑥은 풀/카피 협소(데이터·콘텐츠 작업). 대부분 care 5단계 실데이터가 쌓이면 자연 해소됩니다. 그래서 다음 작업 = P0-D 환경신호 입력측 완성(unitProbe 답 → progress.evidence → advanceProgress 졸업)이 단 하나의 최우선입니다. 이게 풀리기 전엔 추천풀을 넓혀도 '졸업 없는 회전'이 계속됩니다.
📎

부록 — 네이밍 규칙 · 연계 문서

참고

세 가지로만 부릅니다 — 산출물과 루프를 가지면 엔진, 데이터를 돌려주면 모듈, 읽고 쓰는 공용 데이터면 레이어.

⚙️ 엔진 (Engine)
사용자 향 산출물(편지·도감)을 만들고 자기 크론 루프를 가진 오케스트레이터. 시스템에 하나.
코칭 엔진 = 이 cron/coach/route.ts
🧩 모듈 (Module)
엔진이 호출해 구조화 데이터를 돌려주는 부품. 순수 함수 지향·사용자 향 산출 아님.
선호계량 · 주간닻 · 추천 · 거울 · 두뇌 · 작문
🗄️ 데이터 레이어
모듈들이 읽고 쓰는 공용 데이터. 판단 로직 없음.
meal_logs · weekly_plans · coach_letters.context(상태 원장 겸용)
📌 설계 문서가 그린 child_daily_state 단일 스냅샷 테이블은 아직 미구현이며, 실제 코드에선 coach_letters.context가 그 역할을 대신합니다(다음날 루프가 어제 context를 읽어 회전·중복회피 이력 복원). 1만 명 배치로 갈 때 머티리얼라이즈 테이블로 승격하는 것이 후속 과제입니다.