구현·배포 보고서 · 2026-06-08 · 출발점: 반복 진단·재귀개선 감사가 가리킨 근본원인을 실제 프로덕션 수정으로. 시나리오 엔진의 후속.
planFromWeekly가 본 엔진의 buildCoachPlan 회전을 그대로 태움). 또한 06-11 복붙 사고 핫픽스로 구조 프레임(환경·자율성·식감)은 전용 무브 메뉴 SCEN_MOVES를 buildCoachPlan이 회전한다(시그니처 dedup 복원·포화 시 plateau). 본문 §1의 "어휘 가드는 의미 중복 미측정" 진단(지지 0.90)은 06-11 사고로 실증됨 — 상세는 주간 정본 부록.
healAnchor가 구버전 닻 보충, upsert 에러 시 컬럼 빼고 재시도.MOVE_MENU 음식 7종 / SCEN_MOVES 환경5·자율성4·식감4) + 시그니처 dedup(직전 3일) + 포화 시 plateau + 아크 단계(intro→how→obstacle→observe→reinforce·주 첫 편지만 진단 허용·reinforce 연속 금지).lib/coachFacts.compileFacts가 기존 컬럼(log_date 등·스키마 변경 0)만으로 시계열 라벨 사실 카드(끼니 커버리지·환경 통계+어제 사실·자주 오르는 음식 횟수·거부 단발/간헐/반복·메모 날짜 분류)와 동적 금지(detForbid)를 계산. 편지의 사실 주장은 카드 안에서만·라벨대로(단발을 패턴처럼 금지). 메모 2일 컷은 철회 — 자르지 않고 분류.polishKo(결정론 어법) + proofreadLetter(LLM). ⚠️ 스타일 전용: "내용·정보·길이·문장 수 불변, 새 정보 추가·삭제 금지" 계약 — 다양성·패러프레이징 담당이 아니다. 다양성은 2단계(계획 회전·아크)가 구조로 만든다.letterDeterministicBad: 시점 환각 / 입력에 없는 증상 단정 / NO_MIX 프레임 섞기·'으깨N섞'·'섞어 함께' / 김치에 섞기 / 까스·너겟·튀김·소시지·어묵 권유 / 'N가지' 가짓수 칭찬 / 해조류↔생선 혼동 / 단 과일+짠 재료 괴식 / 과일↔끼니 페어링 / 밀가루 날것 / 쌀 날것 / 짠맛=입맛 발달 칭찬 / 김치류 칭찬) + 동적 detForbid(그날 데이터로 계산: 점심 기록 3일+면 '점심 거르/건너/안먹/굶' 금지 · 단발 이벤트 단어[뷔페·외식·여행·파티·잔치·캠핑·행사]가 최근 2일 메모에 없으면 언급 금지) → 위반 시 재생성 ≤2회.context에 plan 시그니처·아크·simToPrev·repeatAlert·verify·model 기록 → 어드민 칩 + cron_runs.issues 경보. 발행 후에도 3일 연속 시그니처/유사도 0.6+면 반복경보.변경 이력(06-11 하루): 사고 진단 → SCEN_MOVES·아크 5단계·healAnchor·반복경보(52811ba) → 점심 사실+detForbid(c8a3834) → 검증자+intro Sonnet(186f015) → 메모 2일 컷(f981933, 이후 철회) → 간식 분리·허용 채널(13c8306) → 사실 컴파일러로 통합·2일 컷 철회(최종).
source_hash에 날짜 미포함(식단 반복 시 어제 편지로 동결).CoachPlan{frame,target,move,signature} + 결정론 타깃 회전 + 시그니처 중복회피 + 공유 작성기 composeLetter. plan 엔진 = 닫힌 하네스.편지 생성은 이미 다단 파이프라인이다: 신호추출 → 시나리오 라우터(13종) → 프롬프트 조립 → 생성(haiku) → 안전가드 재생성 → 어휘 유사도 재생성 → 퇴고. 느슨한 의미의 스캐폴드는 맞다. 그러나 다양성을 보장하는 관절이 약했다 — 9개 에이전트가 6개 가설을 코드에 대해 반증 시도한 결과:
| 근본원인 | 내용 | 판정 |
|---|---|---|
| 시나리오 수렴 | 만성 집공백 등원아는 home-daycare-gap/nutrient-gap만 점화 → 각도가 '집 공백'으로 수렴 | 지지 0.82 |
| 타깃 비대칭 | 코칭 무브는 결정론 회전인데 타깃(과일·콩류)은 LLM 재량 → 부족 1순위 고정이면 매일 같은 타깃 | 지지 0.92 |
| 유사도 가드가 어휘적 | 문자 3-gram 자카드 + 도입 첫 25자. "집…" vs "어린이집…"은 통과하나 사람 눈엔 같은 처방. 의미 중복 미측정 | 지지 0.90 |
| 상태 원장 부재 | '최근 다룬 (타깃·무브·프레임)'을 구조화 저장·환류하는 게 없음. 비중복은 과거 편지 본문을 LLM에 호소 | 지지 0.90 |
| 재생성 비탈출 | 임계 초과해도 같은 계획 재호출(무브 텍스트만 변경) → 근접 중복 발행 가능 | 지지 0.95 |
| 집-기관 프레임 | "어린이집은 충분히"는 home-daycare-gap 도입에서만. '충분'은 OCR 실측이지 추정 아님(부분 정정) | 부분 0.84 |
가장 근본: source_hash = 정렬식재료+거부+reds+notes에 날짜·무브·시나리오가 빠짐. 편식 식단은 며칠씩 비슷한 게 정상이라 해시가 자주 일치 → 어제 편지를 글자 그대로 재사용하며 모든 다양성 장치를 실행조차 안 함.
DB 확증: 6/8 nutrient-gap · 6/7 home-daycare-gap · 6/6 nutrient-gap — 시나리오 ID는 달랐는데 출력이 전부 '과일/어린이집 충분'으로 수렴.
핵심 전환: '무엇을 다룰지(계획)'를 LLM 손에서 빼내 코드가 결정론적으로 확정하고, LLM은 그 계획을 따뜻하게 풀어 쓰기만 한다. 계획은 구조화된 객체로 저장돼 다음날 자기 자신을 검증한다.
frame|target|move 중복회피(생성 전)context.plan 구조화 원장(다음날 읽음)주요 구성: CoachPlan{frame,target,move,signature} · buildCoachPlan(타깃 결정론 회전 + 시그니처 중복회피 + escalate) · planFor · composeLetter(공유 가드) · planTargetBlock(타깃 코드 확정) · varyOpener · srcHash에 today · ?date= QA 오버라이드.
| 피드백 | 구현 | 커밋 |
|---|---|---|
| 이틀 연속 같은 결핍 채근 금지 "다른 시나리오로 환기 — 엄마가 다음에 결심할 여유" | 어제와 같은 타깃을 또 짚게 되면(대체 결핍 없음) gap 시나리오를 빼고 환기 각도(정체기)로 전환. 단일 결핍이면 gap/plateau 교대. | 62d1b93 |
| 밥과 과일은 따로 · 과일 추천은 간식으로 | SNACK_CHANNEL={과일} — 무브를 snack으로 고정, 끼니에 곁들이지 말고 "간식으로 따로" 프레이밍. | 62d1b93 |
| 과일↔끼니 페어링 차단 | 결정론 가드 fruitMealPairing — 한 문장에 과일+끼니음식+곁들/옆에/섞 동사면 재생성(backstop). | 62d1b93 |
| 간식 멘트가 5일 중 4일 반복 "과자 대신 과일·요거트·계란" | ① 쿨다운(context.snackShown 원장, 최근 2일 노출 시 생략) ② 과일이 그날 타깃이면 중복 제거 ③ daySeed 예시 로테이션. 신호는 며칠 간격으로 재환기. | 8a6ada5 |
원인은 모두 동일했다 — 메인 코칭이든 간식이든 윈도우를 매일 재계산하면서 이력(원장)이 없어 같은 결론으로 수렴. 해법도 같다: 구조화 원장 + 결정론 회전 + 의미 중복회피.
?date= QA 오버라이드로 5일을 정방향 재생성(원장이 앞으로 쌓이도록). 정상 운영 모드는 byte-identical(무회귀).
| 날짜 | 프레임 | 타깃 | 무브 | 비고 |
|---|---|---|---|---|
| 6/4 | home-daycare-gap | 콩류 | cook-together | 간식 멘트 노출(예시 회전: 고구마·방울토마토) |
| 6/5 | plateau | — | — | 콩류 채근 대신 태도 칭찬(환기) · 간식 쿨다운 |
| 6/6 | home-daycare-gap | 생선·해산물 | sensory | 감각 사다리(향·만지기) |
| 6/7 | home-daycare-gap | 과일 | snack | "오후 간식에 딸기·귤 따로"(밥 옆 아님) |
| 6/8 | nutrient-gap | 콩류 | beside | 된장국에 두부 |
이틀 연속 같은 타깃 0건 · 과일↔끼니 페어링 위반 0건 · 5/5 고유 시그니처 · 간식 멘트 4/5 → 1/5.
Before(제보 이미지): 과일/과일/콩류 전부 "어린이집 충분 + 좋아하는 음식 옆에 놓아두기" → After: 4개 다른 타깃·각도, 채근 0. (검증: tsc 0 · next build 0 · 실함수 7일 시뮬 인접 3일 시그니처 충돌 0)
편지가 타깃 식재료를 어떤 음식·궁합으로 줄지를 LLM이 지어내던 것("콩밥·된장국")을, 검증된 네트워크 테이블에서 뽑아 사실로 주입하도록 바꿨다(plan 엔진과 같은 철학).
502816c·e065193) — 이사님 결정 반영: 추상 '조리법'을 추천하면 LLM이 조합을 지어내다 괴식 → 검증된 '인기 음식 이름'만 권한다(조합은 그 음식 안에 이미 들어 있음). lib/coachRecos.ts 신설(사촌 + 그 식재료의 가장 많이 쓰이는 실존 음식 + 궁합, 전부 테이블 근거·매운음식 제외). 아린 검증: "또래는 순두부찌개·유부맑은국으로 먹어요"(콩류)·"고등어무조림·구이"(생선) 본문 노출 — 괴식·환각·매운·목록 밖 추천 0건.| 동작 | 정본 테이블 | 접근자 | 상태 |
|---|---|---|---|
| ② 잘 먹는 식재료→사촌 | food-graph bridge(242) | neighborsOf().bridge | 이미 됨 |
| ②′ 궁합(같이 넣을 것) | food-graph pair(589) | neighborsOf().pair | 이미 됨 |
| ③ 조리법 → 인기 음식 이름으로 대체 | kit-dish-matrix(식재료×조리법 0~3) | dishesForIngredient() | 인기음식 폴백 |
| ③′ 조리법+안전 그램 | cooking-amounts·cuisine-guide | cookingGroups() | 보류(괴식가드 우선) |
| ① 식재료→또래 음식 | ingredient-recipes(식재료→또래 메뉴) | loadFreqRecipes() | ✅ 구현 |
| ① 음식→비슷한 음식 | (직접 자산 없음) | derive 필요 | 보류(우회) |
세 테이블 모두 식재료(노드/키) 단위 — "음식→비슷한 음식"을 직접 줄 자산은 없다(dish 노드 0개). 우회: 잘 먹는 식재료 기반 또래 음식/사촌(데이터 충분) 또는 빌더로 dish 유사도 생성(거침).
lib/coachRecos.ts 신설lib/coachRecos.ts + cron/api/프롬프트 (502816c·e065193)| 결정 | 결론 |
|---|---|
| 조리법 정책 완화? | 완화 불필요 — 추상 '조리법' 대신 검증된 인기 음식 이름으로 권하니 괴식 위험이 근원에서 제거됨. 상세 레시피 금지는 유지(부모 검색). |
| ① 음식→비슷한 음식 | (a) 우회 채택 — 잘 먹는 식재료 → 사촌 → 그 식재료의 인기 음식이 "비슷한 음식 추천"을 안전하게 대신. Phase 2 빌더 보류. |
main → Vercel mealfred-app. 스키마 변경 없음(기존 context jsonb에 plan·snackShown 키 추가). 오늘 밤 정기 크론부터 전 자녀 적용. 잔여: 며칠 모니터 · 간식 cadence(쿨다운=2) 체감 후 조정 · CRON_SECRET 미설정.