한눈 요약 — 엔진 계기판
Executive Dashboard엔진은 토대 위 3단으로 짓습니다 — ① 결정론이 2축 음식 네트워크·영양 신호로 후보(시나리오·추천 풀)를 깔고, ② 🧠 두뇌(Sonnet, ?brain 카나리아)가 시나리오를 고르고 음식 추천을 검수하고, ③ ✍️ 손(Haiku)이 작문합니다. "계획·판단은 코드와 두뇌가, 글만 손이" 원칙 위에 섭니다.
시스템 맥락 — 엔진은 누구와 대화하나
C4 Lv.1 · System Context엔진은 스스로 깨지 않습니다. Vercel Cron이 매일 새벽 깨우면, Supabase에서 식사 기록을 읽고, 필요할 때만 Claude에게 글을 맡긴 뒤, 결과를 다시 Supabase에 넣어 둡니다. 부모는 아침에 이미 만들어진 편지를 읽기만 합니다.
★ 핵심 설계 = 비동기 사전 생성. 부모가 앱을 여는 순간 LLM을 호출하지 않습니다 — 새벽에 미리 만들어 둔 편지를 read만 합니다(온디맨드 API는 폴백). 그래서 아침 화면은 즉시 뜨고, LLM 비용·지연은 새벽 배치에 격리됩니다.
계층 지도 — 9개 층, 37개 부품
Layered Architecture · 모듈 추상화위에서 아래로: 깨어남(진입점) → 지휘(오케스트레이션) → 전략(주간)·전술(일간) → 🧠 두뇌 선택·검수 → 사실·추천 → 데이터·영양 토대 → 검증 하네스 → 지원. 칩 하나가 모듈 하나입니다(파일명 병기).
두뇌 2파이프 — 결정론 후보 → 두뇌 선택·검수 → 손 작문
Container Diagram · 2-Stage Pipeline엔진은 v2(LLM 작문) 기반 위에 LLM 두뇌를 얹은 2파이프 구조입니다. ① 결정론이 토대(2축 네트워크·영양 신호)로 '오늘 쓸 수 있는 것'(시나리오·추천 식재료 풀)을 후보로 깔고 → ② 🧠 두뇌(Sonnet)가 그중 하나를 고르고 음식 추천을 켤지 끌지 검수(편지는 안 씀)하고 → ③ ✍️ 손(Haiku)이 영양 평가·어제 식단·음식 추천을 엮어 작문합니다. 두뇌가 꺼지거나(?brain 미설정·실패) 후보가 없으면 결정론 산출로 폴백 — 발행은 언제나 보장됩니다.
🧮 ① 결정론 후보 계산
🧠 ② 두뇌 선택·검수 + ✍️ ③ 손 작문
scenarioId·useFood·approvedRecs만 반환(편지 안 씀·후보 밖 금지)SonnetcookedName=찐 단호박) 엮어 작문Haiku★ 왜 두뇌인가 — 결정론만으로는 "오늘 진짜 문제가 음식이 아니라 환경인데도 음식 추천을 우겨넣는" 판단 오류를 못 막습니다. 그래서 두뇌가 useFood로 음식 추천 본문을 통째로 켜거나 끄고, 환경이 핵심인 날엔 음식을 뺍니다. 두뇌는 고르고 검수만, 글은 손이 — 생성자(손)와 판단자(두뇌)를 분리해, 비싼 머리(Sonnet)는 하루 1콜·주 1콜만 쓰고 작문은 싼 손(Haiku)이 맡습니다. 상태기계·영양·사실·간식·추천·괴식 매트릭스·검증 하네스는 전부 결정론 순수 함수(LLM 0콜)라 테스트·리플레이가 쉽습니다. (과거 v3 블록 조립·하이브리드 Letter B 비교는 플래그 뒤 도먼트로만 잔존 — 전 자녀 라이브 경로는 위 2파이프.)
시퀀스 — 하루치 편지가 만들어지는 56단계
Sequence / Swimlane Diagram새벽 코칭 크론이 한 번 깨어나 한 자녀의 편지·질문을 짓기까지. 레인(누가)·종류(무엇을)·갈래(공통/결정론/두뇌·손)로 색을 입혔습니다. 7개 국면으로 묶었습니다.
| # | 레인 | 행동 | 종류 |
|---|---|---|---|
| 국면 Ⅰ · 깨어남 · 인증 (1–6) | |||
| 1 | ⏰ Vercel Cron | 매일 KST 02시 트리거 GET /api/cron/coach — 부모가 아침에 열기 전 미리 생성 | 발행 |
| 2 | 크론 라우트 | 인증(CRON_SECRET) · QA 오버라이드 ?child ?force ?date 허용 | 가드 |
| 3 | 크론 라우트 | API 키 확인 · today 확정 — 편지는 '어제까지' 확정 데이터로만 평가 | 가드 |
| 4 | 🗄 DB | cron_runs INSERT(running) — 실행 로그 시작 | DB쓰기 |
| 5 | 상류 공급 | 야간 미매핑 보강 — menus만 있고 식재료 빈 행 백필(영양계산 전) | 계산 |
| 6 | 크론 라우트 | 카테고리맵·또래 인기음식맵 정적 로드(비면 '채소 없음' 단정 금지) | 읽기 |
| 국면 Ⅱ · 자녀 선별 · 라운드로빈 (7–14) | |||
| 7 | 🗄 DB | 최근 7일 meal_logs SELECT(어제까지) → 자녀별 그룹 | 읽기 |
| 8 | 크론 라우트 | 활성 자녀 = 최근 7일 중 기록일 ≥3일인 자녀 | 분기 |
| 9–10 | 🗄 DB | 최근 30일 기록으로 휴면 일수 계산 → 휴면(2~7일·이력≥3) 자녀 합류 | 읽기 |
| 11–13 | 크론 라우트 | 최근 3일 편지로 중복회피 원장 구축(과거 편지만) → '오래된 순' 정렬(라운드로빈) | 계산 |
| 14 | 🗄 DB | 자녀 메타·오늘 질문·등원·체위 일괄 SELECT(없으면 안전 degrade) | 읽기 |
| 국면 Ⅲ · [자녀 루프] 수집 · 사실 컴파일 (15–26) | |||
| 15 | 크론 라우트 | 경과 > 50초(시간예산)면 중단 → 남은 자녀는 다음 실행이 이어받음(SIGKILL 회피) | 가드 |
| 16–19 | 상태기계 | 휴면 복귀 분기 — 평일 포함 갭이면 죄책감 없는 복귀 편지 후 continue(주말만 갭은 제외) | 분기 |
| 20 | 크론 라우트 | 끼니 집계 — 식재료/거부/메모/장소, 집·기관 분해(부모 통제 기준 칭찬) | 계산 |
| 21 | 영양 계산기 | computeSignals/FoodGroups/Timeseries → 적신호·결핍군·시계열(집 끼니 별도) | 계산 |
| 22 | 간식 엔진 | BMI 밴드 → evaluateSnacks 4축(초가공·간섭·칼로리방향·좋은간식) | 계산 |
| 23 | 사실 원장 | compileFacts — 추세 라벨(단발/간헐/반복)·금지어(detForbid) 박제 | 계산 |
| 24–25 | 재노출 계산기 | 최근 28일로 '거부→수용 전환' 감지 + 재노출 적기 사실(숫자는 코드, LLM은 인용만) | 계산 |
| 26 | 크론 라우트 | 식단 지문(srcHash) 계산 — 식재료·거부·적신호·메모수+today | 계산 |
| 국면 Ⅳ · 재사용 판정 · 주간 닻 (27–32) | |||
| 27 | 크론 라우트 | 재사용? 직전 편지 지문 동일 또는 오늘 편지 있음 → LLM 스킵·내용·context 보존 | 분기 |
| 28–29 | 🗄 DB | (생성) 과거 편지 5통·ICFQ 위험 누적·구조화 입력 분포(식감·자율성·환경·시간) | 읽기 |
| 30 | 계획 수립기 | planFor — 결정론 CoachPlan(프레임·타깃·무브·시그니처)을 작문 전에 확정 | 계산 |
| 31 | 주간 사령부 | 주간 닻 로드/종합 — 일요일=차주 닻 Sonnet 종합(QA date 시뮬엔 금지) | Sonnet |
| 32 | 🗄 DB | weekly_plans upsert(닻) — error 검사, 신규 컬럼 거부 시 레거시로라도 영속 | DB쓰기 |
| 국면 Ⅴ-ⓐ · 두뇌 경로 (33–41) — ?brain 카나리아 | |||
| 33 | 두뇌 게이트 | ?brain 게이트 — qp.get('brain')==='1'(카나리아)면 두뇌 선택 진입, 아니면 결정론(planFor) 그대로 | 분기 |
| 34–35 | 🗄 DB · 추천 | 최근 3주 닻·과거 편지·영양 거울 + 추천 후보(buildIngredientPool 풀 5개) 로드 | 읽기 |
| 36 | 🧠 두뇌 | pickActionByBrain — 후보 시나리오 중 선택 + 음식 추천 검수(scenarioId·useFood·approvedRecs, 편지 안 씀) | Sonnet |
| 37 | 🧮 결정론 | planFor(forceScenarioId) — 두뇌가 고른 시나리오로 잠그고 계획(타깃·무브)은 결정론 산출 | 계산 |
| 38–39 | 🧮 추천 회전 | recoIng 풀에서 최근 추천 안 한 식재료 회전 → useFood면 buildRecoFacts(찐 단호박)로 음식 추천, 아니면 본문 제외 | 코드 |
| 40–41 | ✍️ 손 | 두뇌 선택·검수 결과를 composeLetter로 인계 → 손(Haiku) 작문(아래 Ⅴ-ⓑ와 동일 작문·가드) | Haiku |
| 국면 Ⅴ-ⓑ · v2 경로 (42–47) — 전 자녀 라이브 | |||
| 42 | 주간 사령부 | planFromWeekly — 타깃 잠금·채근 캡·행동지연·아크 단계(관측된 실행만 칭찬) | 계산 |
| 43–44 | 간식·추천 | 간식 멘트 게이팅(쿨다운·과일타깃·구조 프레임 제외) + buildRecoFacts 화이트리스트 | 계산 |
| 45 | 코칭 본부 | composeLetter — 계획 주입 → Haiku 작문(intro는 Sonnet) · 데드라인 강등 | Haiku |
| 46 | 가드 | 가드 3종 — 금지어 재생성 · 의미 검증자 · 어휘 유사도 재생성 | 가드 |
| 47 | 가드 | 자동 반복 경보 — 직전 편지 최대 유사도·동일 시그니처 2연속 → issues 기록 | 가드 |
| 국면 Ⅵ · 발행 · 질문 (48–52) | |||
| 48 | 크론 라우트 | 발행 컨텍스트 단일화 — 두뇌 산출 ?? 재사용 보존 ?? 결정론 폴백(어드민 검증용 근거 스냅샷) | 계산 |
| 49 | 🗄 DB | coach_letters UPSERT(child_id, letter_date=오늘) | DB쓰기 |
| 50 | 알림톡 | 신규 편지면 2줄 미리보기(부모당 1건) — 현재 도먼트 | 발행 |
| 51 | 코칭 본부 | 오늘의 질문 — ICFQ 주기 > generateQuestion(주제 로테이션·완식 수렴 방지) | Haiku |
| 52 | 🗄 DB | daily_questions UPSERT(child_id, q_date=오늘) [자녀 루프 끝] | DB쓰기 |
| 국면 Ⅶ · 롤업 · 마감 (53–56) | |||
| 53–54 | 영양·DB | 기간 요약 롤업 — 주·월 지표 재계산 → period_summaries 멱등 UPSERT | 계산 |
| 55 | 🗄 DB | cron_runs 마감 — 처리·오류·issues(반복경보·검증위반·폴백·닻 장애) ≤30건 | DB쓰기 |
| 56 | 크론 라우트 | tip-ranking 크론 self-fetch 체이닝 → JSON 응답(Hobby 플랜 2크론 한도 우회) | 발행 |
상태기계 3종 — 엔진의 의사결정 뇌
UML Statechart · 유한상태기계엔진은 "오늘 어떤 각도로 코칭할지"를 확률이 아니라 상태 전이·트리거로 결정합니다. ⓐ 시나리오 선택(현재 라이브·selectScenario+두뇌)과 ⓒ 한 주의 가르치는 결(주간 아크)은 라이브, ⓑ 유닛 생애주기(decideDailyV3)는 v3 도먼트입니다.
ⓐ 일간 전개 결정기 — decideDailyV3 + advanceProgress
전이 조건(코드 그대로): 전진=합격 연속이 holdWeeks 충족(step+1) · 심화=최근 신호 있고 진전 중 · 피벗=정체(무신호 6일+ & 충분히 코칭) 또는 절뚝거림(약신호로 정체 판정엔 안 걸리나 사다리 0진전)이고 주당 피벗 캡(1회) 미소진 · 유지=최종단 후 유지 주, 또는 정체+피벗 불가 → 강등 · maintain 3연속이면 deepen으로 강제 재개(위안만 반복 차단). 우선순위 1순위는 재발 신규(mastered→relapsed) 유닛 재개.
ⓑ 유닛 생애주기 — UnitStatus · evolveRow
ⓒ 가르치는 아크 5단계 — WeeklyArcStage
스크래치 블록 — 코드를 모르는 분도 읽는 엔진 로직
Block-based(Scratch) Pseudocode교육용 블록 언어(Scratch)처럼, 엔진의 두 핵심 로직을 색깔 블록으로 풀었습니다. 주황=제어(만약~), 파랑=동작, 초록=판정, 보라=나만의 블록(모듈), 노랑=시작(언제), 빨강=저장.
★ "만약~아니고~" 사다리가 곧 F-02 결정표입니다. 가장 급한 것(재발)부터 가장 한가한 것(관측)까지 우선순위가 고정돼 있어, 같은 데이터면 누가 돌려도 같은 결정이 나옵니다(결정론).
데이터 흐름도 — 한 끼니가 편지가 되기까지
Data Flow Diagram (DFD)모든 것의 원천은 부모가 찍은 끼니 기록 하나. 그것이 여러 처리기를 거쳐 편지·질문·진도·요약 네 개의 저장소로 흩어집니다. 사각=데이터 저장소, 둥근=처리기.
★ 방향성 — 진단·사실은 항상 왼쪽(데이터)→오른쪽(편지)으로만 흐릅니다. 편지(LLM 산출)가 사실을 거꾸로 만들지 못하게, 모든 숫자·라벨은 처리기(코드)가 확정하고 작문기는 인용만 합니다. 이 단방향성이 환각 차단의 뼈대입니다.
데이터 모델 — 12개 테이블, children 허브
Entity-Relationship Diagramchildren가 허브이고 모든 코칭 테이블이 child_id로 매답니다. 부모는 coach_letters·daily_questions만 읽고, 엔진 내부 상태(weekly_plans·curriculum_progress)는 RLS로 비노출(서버만 접근)입니다.
- nickname·age_band·sex 별명·연령대·성별
- chronic_conditions 만성질환→식이 가이드
- height/weight·daycare 체위·등원
- excluded_ingredients 집에있어요 제외
- ingredients 정규화 식재료(핵심)
- refused·ate_well 거부·잘먹음
- texture·autonomy·environment 씹기·자율·환경 메타
- place·meal_time 집/기관·끼니시각
- letter·oneliner 본문·한줄
- source_hash 식단 지문(재사용 키)
- context jsonb: plan·decision·blocks·weekly·verify
- mission_target·target_pool 초점 타깃(주 불변)
- budget·ledger 채근 캡·소비 추적
- goals 주간 목표 포트폴리오 2~3
- teaching_arc·impression 아크·의사 소견
- unit_id 12종 CHECK
- status·step 상태기계·사다리 단
- evidence 신호 카운터(jsonb)
- last_signal_at·relapse_count 정체·재발
- topic icfq·unit-probe·로테이션
- chips·answer 객관식·부모 답
- context unitProbe 적립 키
- metrics variety·refusalPct·reds·bmiPct…
- rating up · down · repeat
- height_cm·weight_kg 성장·간식 칼로리 방향
- status·meta issues·alerts·reused·skipped
context.weekly로 기록수업 커리큘럼 — 주간 작전층이 고르는 12 유닛
Domain Model · Capability Map유닛 하나가 "편식 해결의 한 과목"입니다. 각 유닛은 레버(어떤 지렛대)·최소 주차·2단 사다리(과제)·졸업 기준·관측 질문을 코드로 선언합니다(선언이 곧 명세). 색=레버.
② 압박 없는 식탁 분위기
② 식사·간식 2~3시간 간격
② 주 5끼+ 같은 자리·시간
② 티스푼 맛보기 초대
② 30분쯤 부담 없이 정리
② 가족 저녁 주 5회
② 디저트 지위 중립화
② 객체 중심 질문으로
② 일반식 비중 올리기
② 사슬 다음 칸(찍어먹기)
② 셀프 서빙·상차림 역할
② 격일 리듬으로 이어가기
★ 코어 8 → 확장 4 — pressure-off → hunger-rhythm → table-stage → exposure-savings → fullness-respect → parent-model → no-bargain → table-talk가 기초 순서(W1~W3 가능), 나머지 4종(감각·다리·자율성·연계)은 신호 발동 시 선택됩니다. 주간 사령부가 신호 강도(trigger 점수)로 후보를 올리고, 포트폴리오 2~3개로 묶어 한 주의 goals를 정합니다. 이 12 유닛은 학부모용 실라버스(coaching-syllabus.html)와 1:1로 정합 — 엔진이 실제로 가르치는 것만 적었습니다.
작문 = LLM(손) + 가드 — 블록 조립은 폐기
Generate-then-Verify현재 라이브 작문은 ✍️ 손(composeLetter, Haiku)이 LLM으로 직접 쓰고, 결정론 금지어 → 어휘 유사도 → 의미 검증자(별도 LLM 1콜) 3종 가드로 사후 검열·재생성합니다(§4·§11 방어 가드). "글맛은 LLM, 안전은 코드 가드" 원칙입니다.
★ 폐기된 접근 — 한때 "글을 쓰지 않고 검수된 블록을 집합 차감으로 조립(LLM 0콜)"하는 v3 경로를 실험했습니다(반복을 구조적으로 차단하려는 의도). 하지만 조립식은 따뜻함·다양성·구체성을 잃어(아린 A/B 실증에서 LLM 작문에 완패) 폐기했습니다. 관련 코드(assembleLetter·letterBlocks·decideDailyV3)는 카나리아 플래그 뒤 도먼트로만 남아 있고 전 자녀 라이브에선 가동되지 않습니다. 다양성은 조립이 아니라 두뇌의 시나리오 회전 + 추천 풀 일일 회전 + 유사도 가드로 확보합니다.
의존 그래프 — 누가 누구를 부르나
Dependency Graph · Call Hierarchy새벽 코칭 크론이 모든 것의 지휘자입니다. 아래로 갈수록 순수해집니다 — 잎(leaf) 모듈(영양·그래프·날짜)은 DB·LLM·시계를 모르는 순수 함수라 테스트·리플레이가 쉽습니다.
coachWeekly · coachDaily · assembleLetter · curriculum · coach · coachFacts · coachRecos · nutrition · snack · progress · reexposure · growth-reference · remapMenus · sens + DB 9개 테이블coachScenarios · coachFacts · coachRecos · coachChronic · snack · coach-tips · Claude SDK. v2 작문·가드의 중심.coachScenarios · coachRecos · coachFacts · Claude(Sonnet). 결정론 후보를 받아 시나리오 선택 + 음식 추천 검수(useFood)만 — 편지는 안 씀.curriculumUnits · curriculum · coachWeekly · coach(icfq). 순수 함수.curriculumUnits · curriculum · coachScenarios · Claude(Sonnet). 닻·레버·아크.nutrition · foodGraph · cookingMatrix · season · growth-reference · progress · reexposure · date · curriculumUnits — DB·LLM·시계 0. 리플레이·테스트의 토대.coachDaily · assembleLetter · curriculum · curriculumUnits만 호출(실 LLM·DB 0) → replayMetrics 게이트.★ 의존 방향이 단방향(상위→하위)이고 잎이 순수해서, 전체 파이프라인(결정론 후보 → 두뇌 선택·검수 → 손 작문)을 실제 DB·LLM 없이 인메모리로 14일 돌려볼 수 있습니다. "라이브에 내보내기 전에 미래를 시뮬레이션한다"가 가능한 이유.
방어 가드 38종 — 실패에서 자란 면역계
Defense-in-Depth · Incident → Guard 매핑가드 하나하나가 실제 사고의 흉터입니다. 5겹 방어로 묶었습니다 — 결정론(정규식) → 의미(LLM 검증자) → 구조(집합 차감) → 운영(시간·영속) → 안전(아동 보호). 프롬프트로 호소해선 못 막은 것을 코드 가드 + 리플레이로 잡았습니다.
🔵 결정론 가드 정규식·재생성
🟣 의미 가드 LLM 검증자·격리
🟢 검증 게이트 리플레이·prebuild
⚫ 운영 가드 시간·영속·일관성
🔴 안전 가드 아동 보호
⏱ 사고 연대기 — 가드가 자란 자리
엔진의 강건함은 설계가 아니라 사고 → 진단 → 가드 → 리플레이 박제의 반복에서 왔습니다.
지표 · 모델 사용 지도
Software Metrics · Cost Map모듈별 규모와 LLM 모델 사용처. 결정론 토대(LLM 0콜) 위에서 손(Haiku)·두뇌·주간 닻(Sonnet)만 모델을 씁니다.
모듈 규모(LOC) — 상위
LLM 모델 사용 지도 — "비싼 머리는 두 곳만"
composeLetter) 편지 작문 · LLM 퇴고 · 의미 검증자 · 온보딩·휴면 편지 · 오늘의 질문 · 궁합 LLM 폴백. 편지당 3~7콜.?brain 카나리아, 일 1콜). ③ 주 첫 진단(intro) 편지 승격(주 1회).범위 · 한계 · 현재 상태
Honest Status · 정직 진단이 문서는 2026-06-14 코드 기준 실재하는 것만 그렸습니다. 추측·로드맵은 배제했습니다.
✅ 지금 라이브 (전 자녀)
- 토대(2축 음식 네트워크·영양 신호) → 결정론 후보(
planFor시나리오·추천 풀 5개) → ✍️ 손(composeLetterHaiku) 작문 + 가드 3종이 모든 활성 자녀의 매일 편지·질문 생성 - 주간 작전층(Sonnet 닻·레버·아크·채근 캡)·사실 원장·간식 4축·추천 화이트리스트·괴식 매트릭스·휴면 회유 가동
- 온보딩(<3일) — 입력 칭찬 + 가벼운 영양 평가 + 안내 + 팁(결핍 단정·추천 없음)으로 신규 가입 침묵 갭 메움
- 음식 추천 풀 일일 회전(콩 연속 반복 방지) + 조리법 접두(
cookedName=찐 단호박)로 즉시 실행 가능한 추천 - 야간 자가진단 크론(반복·중복·거울 누락 모니터) + 검증 하네스(괴식 0·수렴 0·품질 0) prebuild 게이트, 835 테스트
🔬 카나리아 / 대기
- 🧠 두뇌(
?brain=1) — 결정론이 올린 시나리오·추천 후보를 Sonnet이 고르고 검수(scenarioId·useFood·approvedRecs). 아린 카나리아 가동, 실패 시 결정론 폴백 - 두뇌는 편지를 쓰지 않습니다 — 선택·검수만 결정론 산출에 얹고, 작문은 항상 손(Haiku, 생성자-검증자 분리)
- 모니터 통과 시 전 자녀 확대(env). 두뇌 OFF여도 결정론 경로로 동일하게 발행
- 폐기: v3 블록 조립(
assembleLetter·12 수업·decideDailyV3)·하이브리드 Letter B 비교 — 플래그 뒤 도먼트로만 잔존, 라이브 미가동
★ 관련 정본 문서 — 무엇을·언제(시나리오·간식) · 어떻게 정하고 만드나(plan 엔진) · 일·주·월 케이던스(주간 케이던스) · 학부모용(실라버스) · 피치용(엔진 다이어그램). 이 문서는 그 모두를 코드 단위로 합친 기술 해부도입니다.
음식 추천 2축 네트워크 — "실제 식단으로 판단한다"
Bipartite Evidence Graph · 동시출현 추론추천의 질문은 하나입니다: "이 음식·식재료가 정말 같이 먹는 조합인가?" 엔진은 이걸 의견이 아니라 실제 식단 데이터의 공통출현으로 판정합니다. 단일 카운트는 흔한 식재료(밥·달걀)를 우연히 '강함'으로 둔갑시키므로, 우리는 서로 다른 두 데이터에서 각자 lift(우연 보정)로 등급화하고, 둘 다 받쳐줄 때만 추천에 씁니다. 그래서 떡+달걀처럼 LLM은 "괜찮다" 했지만 두 축 모두 약신호인 조합이 자동으로 차단됩니다.
두 증거축 — 무엇이 다른가
축 ① 레시피 동시출현 (한 음식 안)
gen-food-graph.pyc — 같은 레시피에 함께 쓰인 횟수(MIN_CO≥4, 노드별 top-14)코드c·N / (na·nb) — base-rate 보정(1.0=우연, >1=실제 연관)코드축 ② NEIS 식판 공통출현 (같은 끼니)
gen-neis-tray-cooccur.pyc코드c·N / (na·nb) — 식판 총수 N으로 우연 보정코드★ 두 축은 다른 것을 본다 — 레시피축은 '한 그릇 안에 섞이는가', 식판축은 '같은 상에 같이 오르는가'. 둘 다 lift로 우연을 빼지만, 식판축은 메뉴명에 안 적히는 숨은 채소(잡채·볶음 속 당근)와 '같이 차려짐'이라는 부모 직관을 보강합니다.
세 번째 관계, bridge(사촌). 대체 사촌(명태↔대구, 고구마↔단호박)은 같은 레시피·식판에 거의 함께 안 나오므로 동시출현으로는 안 잡힙니다. 그래서 맛·식감·색이 닮은 고신뢰 수기 시드 175쌍(전부 verified:true·동일 식품군 근접)으로 따로 둡니다. 푸드체이닝 'chain' 추천(잘 먹는 것 → 닮은 새 음식)은 verifiedCousinsOf로 이것만 씁니다.
병합 규칙 — upgrade-only(신뢰는 올리기만)
식판축을 레시피축 그래프에 합칠 때 강등은 금지합니다. 메뉴명에 안 적힌 채소 누락이 '안 어울림'을 뜻하진 않기 때문 — 빠진 근거는 약점 증거가 아닙니다. 그래서 식판 strong 쌍은 ① 기존 엣지면 등급을 승급만(37쌍), ② 그래프에 없으면 새 pair 엣지로 추가(106쌍, src:'tray'·basis="같은 끼니 식단 N회").
| 식판 등급 | 그래프에 이미 있나 | 행동 | 결과 |
|---|---|---|---|
| strong | 있음 · 더 낮은 등급 | GRANK[tray] > GRANK[기존] → grade=tray (strength<2면 2로) | 승급 35 |
| strong | 있음 · 같거나 높은 등급 | e.tray만 기록(메타) · 등급 유지 | 보존 |
| strong | 없음 | 새 pair 엣지 추가(strength 3·src:'tray') | 신규 89 |
| strong · 과일 한쪽 | 무관 | 교차채널 차단 — 과일은 간식채널(끼니 곁들임 아님)이라 같은 끼니 디저트 동시출현을 궁합으로 안 봄 | 차단 116 |
| medium / weak | 무관 | 병합하지 않음(strong만 승격·신규) | 차단 |
| — | — | 강등 경로 자체가 없음(누락=약점 증거 아님) | 금지 |
추천 게이트 — 조합이 통과하는 경로 — comboMatrix.scoreCombo
최종적으로 '잘 먹는 음식 + 결핍 식재료' 조합을 띄울 때, 4단 폴백을 위에서부터 따집니다. 핵심은 borderline LLM 점수(2)는 실제 레시피 동시출현(cells)이 ≥8일 때만 인정한다는 게이트 — 떡+달걀(score 2·cells 4)을 여기서 한 번 더 거릅니다.
| 순위 | source | 판정 규칙 | 예시 |
|---|---|---|---|
| 1 | matrix | LLM 정성채점 0~3. 단 score 2 & cells < 8 → 1로 강등(차단), score 3·실증된 2는 그대로 | 볶음밥+당근=3 통과 · 미역국+당근=1 차단 · 떡+달걀=2·cells4→1 차단 |
| 2 | pair | strongPairsOf — grade='strong'만(약신호 s=1 일괄 차단). 강도를 0~3 클램프 | 된장+두부 등 강한 궁합 |
| 3 | cells | 레시피 동시출현 count>0이지만 임계 미만 = 약신호 → score 1(보수적 금지) | 드물게만 같이 쓰인 조합 |
| 4 | none | 어디에도 없음 → score 0. 미수록은 '통과'가 아니라 '금지' | 근거 없는 임의 조합 |
통과 임계는 기본 2(isComboOk) — score 1(미역국+당근·떡+달걀)은 막고 2·3만 통과. 약신호 차단 임계 PAIR_MIN_STRENGTH=2는 그래프(strongPairsOf)와 매트릭스(CELLS_MIN=8) 양쪽에서 통일됩니다 — 모든 소비자(coachRecos·coachMaterials·comboMatrix·comboGuard)가 같은 헬퍼로 일원화.
사례 추적 — 왜 '떡+달걀'이 차단되나(두 축 모두 약신호)
★ 핵심 — LLM 한 사람의 "괜찮을 것 같다"는 판단(score 2)이 아니라 실제로 같이 먹는 데이터가 최종 결정권을 갖습니다. 떡+달걀은 레시피에서도(lift 0.72) 식판에서도(strong 미달) 약신호라, 두 축의 교차 검증으로 자동 탈락합니다.
✍️ 작문 단계 곁들임 가드 Write-time Guard
serializeMaterials의 곁들임 규칙 줄 — 검증된(strong·게이트 통과) 조합이 없으면 작문 단계에서 식재료를 임의로 섞지 못하게 막습니다. 그래프가 통과시킨 조합만 글에 등장.★ 설계 한 줄 — 추천은 "이 음식 좋아요"라는 의견이 아니라, 두 데이터(레시피·식판)에서 우연을 뺀 뒤에도 함께 등장하는가라는 증거 질문입니다. 등급화·upgrade-only 병합·교차 게이트·작문 가드가 한 줄로 꿰여, 부모가 보는 문장 속 곁들임은 모두 실제 식단으로 검증된 것만 남습니다.