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

🕸️ ⑦ 네트워크 조회 헬퍼

음식↔식재료 궁합·검증 사촌·음식적합도·급식 흔함도를 결정론 그래프로 조회해 challenge 추천의 '다리'를 놓는 질의·근거화 층

← 문서 허브 · 통합 아키텍처 지도 · 코칭 엔진 모듈 명세
🗺️ 이 문서는 통합 아키텍처 지도의 모듈 ⑦를 코드 단위로 펼친 명세입니다 — 비개발자 설명 · 구현 실측 · 파이프라인 시각화 · 작동 예시 · 남은 문제. 코드 근거: lib/coachRecos.ts · lib/foodGraph.ts · lib/comboGuard.ts · lib/comboMatrix.ts · lib/kitGuide.ts (2026-06-20 실측).

비개발자용 — 이 모듈이 무엇을, 왜 하나

이 모듈은 코칭 엔진이 "이 식재료, 어디에 어떻게 넣으면 아이가 잘 먹을까?"를 물었을 때 대답해 주는 '음식 인맥 검색대'입니다. 직접 추천을 결정하지는 않습니다. 다른 코칭 모듈이 편지를 쓸 때 필요한 '사실' 네 가지를 결정론(매번 같은 답)으로 찾아 줍니다. ① 궁합(같이 차리면 자연스러운 짝, 예: 두부+다시마) ② 검증된 사촌(맛·식감이 닮아 한 걸음 갈아탈 만한 대체 음식, 예: 감자→고구마) ③ 음식 적합도(이 식재료를 넣기 좋은 요리, 예: 두부→순두부·계란찜) ④ 급식 흔함도(이 식재료가 또래 급식 표준식단에 얼마나 자주 오르나). 이 넷은 우리가 모은 실제 레시피·급식 데이터에서 미리 계산해 둔 표를 그냥 '읽어 오는' 것뿐입니다.

왜 필요하냐면, 코칭의 핵심 전략이 "잘 먹는 음식에 결핍 식재료를 슬쩍 섞기" 또는 "잘 먹는 음식의 닮은 사촌으로 한 걸음 갈아타기(푸드체이닝)"인데, 아무 조합이나 섞으면 '미역국에 당근'처럼 아이가 질색하는 괴식이 나옵니다. 그래서 추천하기 전에 반드시 이 검색대에서 "이 짝이 실제 레시피에 충분히 같이 나왔나? 닮은 사촌이 사람이 검증한 것인가? 이 요리에 넣어도 자연스럽나?"를 확인합니다. 즉 코치가 상상으로 조합을 지어내지 못하게 막는 '현실 검문소'입니다.

🥗 이 모듈이 실제로 거는 편식·괴식 안전장치 (코드 실측)
약한 궁합 차단: 궁합(pair)은 등급 'strong'(없으면 강도 ≥ 2)만 추천에 씁니다. '떡+달걀'(lift 0.72, 강도 1) 같은 약한 짝이 괴식 사고를 낸 적이 있어, strongPairsOf가 약신호를 걸러냅니다. 이 임계값(2)은 추천을 쓰는 모든 모듈이 같은 문을 통과하도록 통일돼 있습니다. ② 검증된 사촌만 갈아타기: 푸드체이닝에 쓰는 사촌(bridge)은 verified 표시가 붙은 것만 씁니다(verifiedCousinsOf). ③ 곁들임 2차 필터: 부모에게 "잘 먹는 X 곁들이면 좋아요"로 나가는 짝은 식판 동시출현(tray)을 뺀 garnishPairsOf 위에, 매운/김치류·날곡물·교차괴식(단 과일·우유 ↔ 짠 생선·콩)을 한 번 더 거릅니다(safeGarnishOf). ④ 급식 흔함은 정직하게: 급식 표준식단(youa-freq)에 충분히 자주 오르는 재료만 '익숙하다'고 안심시키고, 미수록은 빈 문자열로 둡니다(0회를 '흔함'으로 위장 금지). 이 모듈 자체는 결핍 판정·강요 금지 같은 '판단'은 하지 않습니다 — 그건 상위 모듈의 몫이고, ⑦은 그 판단에 필요한 '사실'만 안전하게 공급합니다.
🧒 운영 예시(아린·5세) — 아린(콩류·채소 결핍, 감자·계란은 잘 먹음)에게 콩류를 늘려 주고 싶다고 합시다. 입력: 엔진이 "두부(콩류)를 어디에 곁들일까?"를 검색대에 물어봅니다. 처리: ① 궁합 조회 → 두부의 강한 짝(다시마·계란·멸치·무). 아린이 계란을 잘 먹으니 '두부+계란'이 안전한 곁들임. ② 음식 적합도 → 두부는 순두부·계란찜·국에 자연스러움. ③ 사촌 조회 → 감자의 검증된 사촌으로 고구마·단호박이 나옴(감자에서 한 걸음 갈아타는 길). ④ 급식 흔함 → 두부·계란은 급식에 매우 흔해 '또래 급식에도 자주 오르는 익숙한 재료'로 안심 톤. 결과: 엔진은 "잘 먹는 계란에 두부를 섞은 계란찜" 또는 "감자 잘 먹으니 닮은 단호박"을 검증된 음식 이름으로 추천하고, '미역국+당근' 같은 괴식이나 약한 조합은 검문소에서 걸러집니다.

구현된 내용 — 실제 함수·데이터흐름·입출력 (코드 실측)

이 모듈의 코드 본체는 lib/coachRecos.ts(추천 사실 조립)와 그 하위의 그래프 질의 헬퍼(lib/foodGraph.ts·lib/kitGuide.ts), 그리고 휴면 중인 괴식 게이트(lib/comboGuard.ts·lib/comboMatrix.ts)입니다.

그래프 질의 헬퍼 (lib/foodGraph.ts — 결정론 조회의 뿌리)

함수 (file:line)역할 · 반환 (실측)
neighborsOf(nm) foodGraph.ts:36한 식재료의 모든 이웃을 Neighbor[]로 반환(foodGraph.ts:16 {nm,kind,strength,basis,count?,lift?,grade?,verified?,tray?,src?}). 정렬 = bridge(사촌) 먼저 → grade(strong>medium>weak) → strength → count.
strongPairsOf(nm) foodGraph.ts:49곁들임용 강한 궁합만 — kind==='pair' && (grade ? grade==='strong' : strength>=PAIR_MIN_STRENGTH). PAIR_MIN_STRENGTH=2(line 19). 모든 소비자(coachRecos·coachMaterials·comboMatrix·comboGuard) 일원화.
verifiedCousinsOf(nm) foodGraph.ts:54푸드체이닝 chain용 검증 사촌만 — kind==='bridge' && (verified===undefined || verified===true). 이 모듈의 challenge-다리 핵심.
garnishPairsOf(nm) foodGraph.ts:65strongPairsOf에서 식판 동시출현(src!=='tray')만 — tray(김치+요구르트류)는 맛 궁합이 아니라 '식판 공출현'이라 곁들임 권유에서 제외. strongPairsOf(TR-02 골든)는 byte 무변경 보존.

곁들임 안전 필터 + 조리 표기 (lib/coachRecos.ts)

함수 / 상수 (file:line)역할 (실측)
safeGarnishOf(nm) coachRecos.ts:165곁들임 추천 전용 — garnishPairsOf 위에 ① 매운/김치류(isSpicyIngredient·KIMCHI_EXTRA) ② 날곡물(STAPLE_FORMS) ③ 교차괴식(garnishConflict: 단 과일·우유류 ↔ 짠 생선·콩/두부)을 추가 차단. 부모 "곁들이면 좋아요" 경로 전용.
garnishConflict(a,b) coachRecos.ts:160무방향 교차괴식 판정 — isSweetBland(과일·유제품) ↔ isSavoryProtein(생선·해산물·콩). 짠↔짠(두부+멸치)은 허용.
cookedName(nm) · stapleDisplay(nm) coachRecos.ts:60·38조리법+식재료 표기. COOK_ADJ(coachRecos.ts:42)로 익혀야 하는 재료에 접두('찐 단호박'·'삶은 검은콩'), 주식 곡물은 먹는 형태(밥·빵·면·떡), 그 외는 원래 이름. 생물 오인 방지.
cleanDishName(raw) coachRecos.ts:64NEIS 원본명 정제 — 접두 분류태그'(간식)'·접미 부재료'&쌈장'·용량 괄호'(200ml)' 제거. inappropriateDish(line 76)로 매운·짠지·견과·튀김 차단.
popularDishesFor(ing, freqMap?) coachRecos.ts:80식재료의 '가장 많이 쓰이는 실존 음식' 최대 2개. freqMap(또래 급식 빈도, freq≥4) 우선 → dishesForIngredient(ing,2)(count>0) 폴백. 주식=먹는 형태, 과일=빈 배열(간식채널).

급식 흔함도 — youa-freq (이번 세션 ⑤거울/A어드민근거의 안심 톤 소스)

이 모듈의 '급식 빈도' 소스는 youa-freq.json(dietary4u 영유아 표준식단 등장률%)입니다. coachRecos.ts:176~216에 인덱싱·조회가 박혀 있습니다.

함수 / 상수 (file:line)역할 (실측)
YOUA_RANK (IIFE) coachRecos.ts:194youa 전 항목으로 동률 안전 순위 산출 — rank = (등장률 더 큰 항목 수)+1, topPct = max(1, round(rank/total*100)). 모집단=전체(분모 인위 축소 금지).
youaRankOf(ing) coachRecos.ts:207YOUA_RANK[ing] ?? YOUA_RANK[ING_YOUA_ALIAS[ing]] ?? null. ING_YOUA_ALIAS(line 184)로 표기 봉합(요거트→요구르트·검은콩→콩(대두)·현미→멥쌀). 미수록=null(0을 꼴등으로 위장 금지). 연어는 의도적 미매핑.
youaReassuranceFor(ing) coachRecos.ts:213부모 안심 문구 — pct >= YOUA_COMMON_PCT(50)이면 '또래 급식에도 자주 오르는 익숙한 재료', 아니면 빈 문자열(서열·등수·% 노출 금지). buildRecoFacts (a) 줄의 evid 절에 직조됨.
coldStartSeed(servedIngredients, max=6) coachRecos.ts:218콜드스타트 앵커 사다리 — Tier2(자주 차려진=servedIngredients) → Tier3(YOUA_TOP 급식 고빈도). 식품군 인식 + 과일(간식채널) 제외. 확신 liked가 적을 때 challenge-다리의 출발점(앵커)을 보강(cron이 confidentLiked와 합쳐 likedSeed로 주입).

추천 사실 조립 — buildRecoFacts / buildIngredientPool (진입점)

함수 (file:line) · 시그니처역할 (실측)
buildRecoFacts({likedIngredients, target?, targetIngredient?, freqMap?, suppressCousins?}) coachRecos.ts:276RecoFacts{target,cousins[],lines[],text}네트워크 헬퍼를 묶어 호출하는 진입점. (a) 타깃 줄(line 285~298): targetIngredient(주간 풀에서 일일 회전) 우선, 그 인기 음식 + 잘 먹는 것과의 safeGarnishOf 곁들임 + youaReassuranceFor 안심 절. (b) 사촌 줄(line 303~318): 잘 먹는 식재료의 verifiedCousinsOf(+그 사촌 인기 음식)·궁합. ⭐ F-18(line 301~303): suppressCousins=true면 (b) 전체 생략 — 슬롯이 이미 음식 타깃을 정한 날 part(b)의 사촌(감자→두부)이 슬롯 음식과 경쟁해 본문이 두부로 회귀하던 근원(랄프위검 rank1) 봉합.
buildIngredientPool({signals, likedIngredients, freqMap?, max=5}) coachRecos.ts:237IngredientPool{pool[],mode,reason}주간 추천 식재료 풀 5개. 결핍 상위 3군만(line 244~246 심각도순·채소 가점). mode: red≥2 → supply(결핍군 대표 라운드로빈) / red0·결핍≤1 → challenge(cousinsOfLiked() 푸드체이닝 + 안 먹는 도전) / 그 사이 → mixed. cousinsOfLiked(line 250)이 verifiedCousinsOf로 사촌 풀을 만듦.
pickFoodReco({target, likedIngredients, freqMap?, seed?}) coachRecos.ts:103FoodReco{group,food,via,...}③거울용 구체 추천. 대표 식재료를 seed로 회전 → liked → pair(safeGarnishOf) → chain(verifiedCousinsOf) → dish → plain 우선순위. via로 프레이밍 결정.
weeklyExposureTarget(signals, liked, seed) coachRecos.ts:128주간 노출 1개 고정(푸드체이닝 일관성). 끼니 채널·채소 우선·도전(안 먹는 것) 우선.

괴식 게이트 (lib/comboGuard.ts · comboMatrix.ts — ⚠️ 라이브 미배선·휴면)

함수 (file:line)역할 · 상태 (실측)
dishIngredientFit(dish,ing) · validCombos(dishes,ings) comboGuard.ts:27·51음식×식재료 게이트 — dishesForIngredient(ing,0) 조회 후 score>=COMBO_THRESHOLD(2)만 ok. 미수록=금지(보수적). validCombos=LLM 후보 화이트리스트. ⚠️ 외부 호출 0건(휴면).
ingredientPairFit(a,b) comboGuard.ts:41식재료×식재료 — strongPairsOf(a).find(b). dish는 노드 아니라 ok=false(시그니처로 경계 강제). ⚠️ 휴면.
scoreCombo(dish,ing) · isComboOk(dish,ing,2) comboMatrix.ts:31·47matrix(정성채점 0~3) → pair → cells(약신호) → none. borderline 게이트: m===2 && cells<CELLS_MIN(8)이면 score 1로 강등(떡+달걀 차단). score 3·실증된 2·국+당근(cells49)은 유지. ⚠️ 라이브 추천 경로에 미배선(휴면).
이번 세션 신규가 이 모듈에 닿는 지점 (실측)
선호계량화 → ⑦ 직접 함수 아님(lib/preferenceQuantification.ts·route 소관). 단 cron이 confidentLiked를 산출해 ⑦의 buildRecoFacts/buildIngredientPool 앵커(likedIngredients)로 주입 — ⑦의 출력 다양성이 이 앵커 폭에 종속(천장4의 뿌리).
F-18 슬롯본문봉합buildRecoFacts suppressCousins 인자(coachRecos.ts:276·303)로 ⑦에 직접 구현됨. cron(route)이 slotFoodsuppressCousins:!!slotFood 전달(cron/coach/route.ts:867).
콜드스타트 사다리coldStartSeed(coachRecos.ts:218) ⑦ 본체. cron이 likedSeed=[...confidentLiked, ...coldStartSeed(servedTop,6)]로 합성(route.ts:373).
A 어드민 근거youaReassuranceFor의 '익숙한 재료' 안심 절이 (a) 줄 근거 텍스트로 흐름. buildIngredientPool.reason('supply(red 2·결핍군 3)')이 어드민 노출.
hardStall 피벗 · #5거울쿨다운 · #6도입회전 · #7macro · P0-D 프로브 → ⑦에 함수 없음(⑤커리큘럼·③거울·⑧주간계획·route 소관). 이 문서에서 ⑦ 기능으로 기술하지 않음.

설계 아키텍처 — 데이터 파이프라인 시각화

입력 테이블(JSON 스냅샷) → 그래프 질의 헬퍼 → 추천 사실 조립 → CoachContext 키. 결정론(매번 같은 답)이고 순수 함수(fs/HTTP 불사용).

📦 입력 테이블
food-graph.json
(edges: pair·bridge·tray)
kit-dish-matrix.json
(scores 0~3·cells)
youa-freq.json
(급식 등장률%)
ingredient-recipes
(freqMap·호출자 주입)
🔍 질의 헬퍼
neighborsOf
strongPairsOf (s≥2)
verifiedCousinsOf
garnishPairsOf (¬tray)
dishesForIngredient
youaRankOf / youaReassuranceFor
🧱 안전 필터
safeGarnishOf
매운·날곡물·교차괴식 차단
cookedName
찐 단호박·삶은 검은콩
cleanDishName
NEIS 원본명 정제
🧩 사실 조립
buildRecoFacts
(a)타깃줄 (b)사촌줄
buildIngredientPool
supply/challenge/mixed
coldStartSeed
📤 CoachContext
bridgeFacts
recoIng · recoPool
recoMode
recoCandidates
buildRecoFacts({ likedIngredients, target, targetIngredient, freqMap, suppressCousins })
│
├─(a) 타깃 줄 ──────────────────────────────────────────────────────────────────
│     targetIngredient (주간 풀 일일 회전)  ──▶ cookedName()         "삶은 검은콩"
│     popularDishesFor(ing, freqMap)        ──▶ "또래 인기: 콩자반·검은콩밥"
│     safeGarnishOf(ing) ∩ likedSet          ──▶ "잘 먹는 두부 곁들이면 좋아요"
│     youaReassuranceFor(ing)  (pct≥50)      ──▶ "또래 급식에도 자주 오르는 익숙한 재료"
│        └─ break (오늘 추천 식재료 1개만)
│
└─(b) 사촌 줄 ──  if (suppressCousins) → [] ◀── ⭐F-18: 슬롯이 음식 타깃 정한 날 생략
      for ing of liked (김치류 제외):
        verifiedCousinsOf(ing) \ likedSet  ──▶ "사촌 찐 단호박(또래 인기: 단호박죽)"   ◀── challenge 다리
        safeGarnishOf(ing)     \ likedSet  ──▶ "궁합 계란·당근"
        └─ lines ≥ 4 면 중단
                                            │
                                            ▼
            RecoFacts{ target, cousins[], lines[], text }
                                            │
   cron: bridgeFacts = _noFood ? '' : buildRecoFacts(...).text   (두뇌 useFood=false면 '')
                                            ▼
                              CoachContext.bridgeFacts  →  ⑥ 작문기(손) 그라운딩
⚠️ 괴식 게이트는 이 그림에 점선comboGuard.validCombos·comboMatrix.isComboOk(dish×ingredient 임계 2)는 존재하지만 위 라이브 파이프라인에 연결돼 있지 않습니다(외부 호출 0건). 현재 괴식 방어는 safeGarnishOf의 식품군 휴리스틱 + strongPairsOf 임계뿐이고, dish×ingredient 정합 게이트는 휴면입니다(⑤ 참고).

작동 예시 — 아린(5세) 실데이터 before/after

아린: 콩류·채소 결핍, 확신 liked = 감자·계란(2개), 콜드스타트 보강 후 likedSeed에 미역·두부·쌀 등 합류. 오늘 슬롯이 콩류 회전 식재료 '검은콩'을 잡은 날.

📥 INPUT (cron이 buildRecoFacts에 주입)
buildRecoFacts({
  likedIngredients: ["계란","감자","미역","두부","쌀"],
  target: "콩류",
  targetIngredient: "검은콩",   // 주간 풀에서 일일 회전
  freqMap: { "검은콩":[{name:"콩자반",freq:31}],
             "단호박":[{name:"단호박죽",freq:18}] },
  suppressCousins: true          // ⭐ F-18: 오늘 슬롯이 음식 타깃을 이미 정함
})

// 내부 단위 헬퍼 호출:
// verifiedCousinsOf("감자") → [{nm:"고구마",kind:"bridge",verified:true},
//                              {nm:"단호박",kind:"bridge",verified:true}]
// safeGarnishOf("검은콩") ∩ liked → [{nm:"두부"}]
// youaReassuranceFor("검은콩") → "또래 급식에도 자주 오르는 익숙한 재료"
📤 OUTPUT (RecoFacts → bridgeFacts)
{
  "target": "콩류",
  "cousins": [],            // suppressCousins로 (b) 생략
  "lines": [
    "[오늘 타깃 콩류] 삶은 검은콩 (또래 인기 음식: 콩자반 · 잘 먹는 두부 곁들이면 좋아요 · 또래 급식에도 자주 오르는 익숙한 재료)"
  ],
  "text": "[오늘 타깃 콩류] 삶은 검은콩 (또래 인기 음식: 콩자반 · 잘 먹는 두부 곁들이면 좋아요 · 또래 급식에도 자주 오르는 익숙한 재료)"
}

// suppressCousins=false 였다면 (b)에 추가됐을 줄(천장4 단조성):
//   "감자 → 사촌 찐 단호박(또래 인기: 단호박죽) / 궁합 계란"
//   "계란 → 사촌 삶은 메추리알 / 궁합 ..."
before/after 한 줄before(F-18 이전): 슬롯이 '검은콩'을 잡았어도 part(b)의 감자 → 사촌 두부 줄이 본문에서 검은콩과 경쟁해, 작문기가 결국 두부로 회귀(랄프위검 rank1 '두부 도돌이표'). after: suppressCousins:true로 (b) 생략 → 본문이 슬롯 음식(검은콩) 단일 푸드체이닝으로 봉합. 실측 비-콩류 슬롯 12일 두부 누수 0. (b)가 살아 있는 날엔 사촌 줄이 항상 감자→단호박·계란→메추리알처럼 감자·계란 단일 앵커에서만 갈려 나옴(천장4 — ⑤ 참고).

남은 문제점 · 해야 할 일 (랄프위검 47점 실측 천장 · 미화 없음)

연속성 47점(2026-06-20) 실측 천장을 이 모듈 관점에서 정직하게. 각 항목 증상 → 근본원인 → 해야할일(owner).

1. challenge 사촌 앵커가 단조 — 감자·계란 2개에 묶임 P0 · 천장4
증상 — (b) 사촌 줄이 매번 단일 앵커에 의존: '감자 잘 먹으니 단호박/고구마', '계란 잘 먹으니 메추리알/달걀팟국'. 사촌만 갈아끼우는 동일 논리가 반복돼 challenge 추천의 다양성에 상한.
근본원인verifiedCousinsOf·cousinsOfLiked(coachRecos.ts:250) 그래프 조회 자체는 결정론·정상(EARNED). 문제는 입력 앵커(잘 먹는 음식 = likedIngredients/confidentLiked)가 감자·계란 2개로 협소해 출력 다양성에 상한이 걸리는 것. 이 협소함의 뿌리는 천장1(아린 확신 liked 0개에 가까움 — P0-D child_daily_state 환경신호 미입력)이라, ⑦ 단독으로는 못 푼다.
해야할일(a) 앵커 다양화: buildRecoFacts (b)가 liked.slice(0,8) 순서대로만 쓰지 말고, 최근 회전 이력(recentRecoIng)을 받아 같은 앵커 연속 사용을 강등. (b) 사촌 다리 카피 변주: '사촌 X' 단일 패턴 → '닮은 맛', '식감이 비슷한', '한 걸음 옆' 등 변주 풀(⑥ 작문기와 합의). (c) 의존관계 명시: confidentLiked 확장이 P0-D 신호 입력에 종속(천장1)임을 막아두기 전엔 앵커가 2개로 묶임을 코드 주석·어드민에 박는다. owner: ⑦(앵커 다양화 정렬) + route/P0-D(앵커 폭 확장 — 선행) + ⑥(카피 변주).
2. 추천 풀 협소 — Tier3 youa 큐레이션이 두부5·달걀6 수준 좁은 풀 P0-B/C
증상 — 콜드스타트가 coldStartSeed Tier2→Tier3로 올라가도 YOUA_TOP 큐레이션 풀이 협소해, challenge 후보가 두부·달걀 등 소수로 수렴. 앵커 다양화(1)의 공급원도 같이 좁아짐.
근본원인YOUA_TOP(coachRecos.ts:177)이 groupOfIngredient 인식되는 표준 식재료로 한 번 더 걸러지고, verifiedCousinsOf 진입 가능한 노드만 남아 실효 풀이 좁다. food-graph bridge 자체도 두부·계란 주변에 집중.
해야할일YOUA_TOP 큐레이션 확장(/mealfred-food-mapping Part B 매트릭스 재생성 시 bridge 다양화) · ING_YOUA_ALIAS 봉합 누락 보강. owner: 데이터(food-mapping 스킬) + ⑦(풀 소비).
3. 진짜 괴식 게이트 휴면 — comboGuard/comboMatrix 미배선 CODE_BUG
증상 — dish×ingredient 정합 게이트(validCombos·isComboOk, 임계 2)가 구현돼 있으나 라이브 추천 경로에서 호출 0건. 현재 괴식 방어는 safeGarnishOf 식품군 휴리스틱 + 궁합 임계뿐.
근본원인buildRecoFacts의 곁들임 산출('잘 먹는 음식 × 추천 식재료')이 safeGarnishOf까지만 통과하고, comboGuard.validCombos로 dish×ing 정합 재검을 안 거침. 두 게이트(comboGuard·comboMatrix)가 동일 임계·동일 사상인데 병존·휴면.
해야할일 — 휴면 게이트를 라이브에 단일 게이트로 배선(comboGuard.validCombos 권장 — dish×ing/ing×ing 경계가 시그니처로 강제). 통과분만 본문 노출(미통과=제외, 빈 결과 허용=조합 강요 금지). 중복 게이트 정리. ⚠️ 호출 트리거 위치(cron buildRecoFacts 호출부)는 통합/cron과 1줄 합의 필요. owner: ⑦(게이트 함수) + cron(호출부).
4. 사촌 곁들임 누수 — 같은 식품군 bridge가 곁들임으로 샐 소지 DESIGN_GAP
증상 — '사촌'은 본질상 갈아타기(교체) 축인데(돼지→닭은 같이 차리는 게 아니라 갈아타는 것), bridge 엣지에 '곁들임 부적합' 표식이 없어 같은 단백질군 사촌이 곁들임 후보로 오용될 소지.
근본원인cousinsOfLiked(coachRecos.ts:250)이 verifiedCousinsOf 결과를 'MEAL_GROUPS 소속'으로만 게이트하고 '결핍군 + 앵커와 다른 군' 게이트가 없음. buildRecoFacts (b)는 사촌을 '사촌 ~' 교체 프레이밍으로만 쓰므로 현재 곁들임 누수는 제한적이나, 풀(buildIngredientPool)에는 같은 군 사촌이 들어갈 수 있음.
해야할일cousinsOfLiked에 '결핍군 + 앵커와 다른 군' 정렬·필터 추가, bridge 엣지에 곁들임 부적합 표식 명문화(장기). 단 — 사촌을 교체 축으로만 쓰는 한 P0보다 후순위. owner: ⑦ + 데이터(food-graph.json).
EARNED — 정직하게 잘 된 것 사실
• 그래프 조회 자체는 결정론·환각 0: strongPairsOf 임계 통일, verifiedCousinsOf verified-only, popularDishesFor 테이블 근거(목록 밖 음식 못 지어냄).
F-18 봉합 실측 EARNED: suppressCousins로 비-콩류 슬롯 12일 두부 누수 0(랄프위검 rank1 해소).
youa 정직 null: 미수록·연어 의도적 미매핑으로 '흔함 위장' 0.
곁들임 교차괴식 가드: 단 과일·우유 ↔ 짠 생선·콩 차단(garnishConflict)이 라이브 동작.
이 모듈의 한 줄 진단 — ⑦의 그래프 조회는 건강하다(결정론·환각 0). 천장은 조회 로직이 아니라 입력 앵커의 협소함(감자·계란 2개)에 있고, 그 뿌리는 P0-D child_daily_state 미입력(천장1)이다. ⑦이 단독으로 할 수 있는 건 앵커 다양화 정렬·카피 변주·휴면 괴식 게이트 배선이고, 앵커 폭 자체의 확장은 route/P0-D 선행에 종속된다 — 이 의존관계를 미화 없이 명시하는 것이 ⑦의 정직한 현 위치다.