BRAIN_MODEL 역할·실패 시 결정론 폴백). 주간 목표 포트폴리오(커리큘럼 유닛) 선택은 일요일 runWeeklyPlanning가 하고, 유닛의 정의·사다리·진척 측정은 ⑤ 진척·커리큘럼이 담당합니다(선택=①, 정의·측정=⑤). P0-D 프로브 신호 입력 지점(pickUnitProbe)도 이 모듈에 있습니다.비개발자용 — 이 두뇌가 무엇을 왜 하나
이 모듈은 '오늘 부모에게 보낼 코칭 편지를 어떤 각도로 쓸까'를 고르는 두뇌입니다. 식당 셰프에 비유하면, 이 모듈은 '오늘은 콩류 얘기를 할까, 식사 분위기 얘기를 할까, 칭찬만 할까'를 정하는 주방장이고, 실제 요리(편지 문장 쓰기)는 다른 엔진이 합니다. 왜 필요하냐면, 같은 아이의 데이터로 매일 편지를 쓰면 LLM이 자꾸 같은 해법으로 수렴해 '매일 똑같은 잔소리'가 되기 때문입니다. 그래서 13개의 미리 짜둔 '각도(시나리오)'를 두고, 그날 데이터 신호로 딱 1개만 골라 그 각도로만 쓰게 합니다.
고르는 방식은 두 층입니다. 주간 닻(작전층): 일요일마다 더 똑똑한 모델이 지난 한 주를 의사처럼 종합해 '이번 주 주력 축(레버)'을 1개 정합니다 — 음식이냐, 식사 환경이냐, 자율성이냐, 질감이냐. 월~토는 그 축에 맞춰 일관되게 가되, 매일 표현만 바꿉니다. 일간 두뇌(전술층): 매일 두뇌(DeepSeek V4-Pro)가 '발동 가능한 시나리오 목록·최근 편지·영양 평가·이번 주 닻'을 한꺼번에 보고 오늘의 각도를 고릅니다. 두뇌의 제1원칙은 '다양성'과 '없는 문제 지어내지 않기'입니다. 그래서 두뇌가 고른 각도라도 '이번 주 닻이 음식이 아니면 음식 잔소리를 못 넣고', '데이터에 그 신호가 없으면 강제 못 하고', '며칠 연속 음식날이면 휴지'하도록 코드가 잠급니다(프롬프트 호소가 아니라 코드 게이트).
결핍 판정은 '추측'이 아니라 우리가 계산한 수치만 씁니다. 신호에는 부족 영양소·비는 식품군·'집 끼니에서만 비는 것(homeMissing)'·거부한 음식이 구분돼 들어옵니다. 어린이집 급식 덕에 전체 영양은 채워졌어도 집에서 비면 '집-기관 격차' 각도로 가서 '기관이 챙겨준 덕'이라고 솔직히 인정하고 집 끼니 한 가지에만 행동을 둡니다(기관 메뉴 변경 요청 금지).
반복 노출이 중심 철학입니다. 거부한 음식은 '실패'가 아니라 '아직 처음 보는 것'으로 재정의하고, 격일~주2~3회로 좋아하는 음식 옆에 작게 다시 올려두라고 안내합니다. 괴식·강요는 코드와 프롬프트 양쪽에서 차단합니다 — 음식 추천은 LLM이 새로 지어내지 못하고 'food-graph가 계산한 실제 후보' 안에서만 두뇌가 취사선택합니다(후보 밖 추가 금지 = 환각 0).
① 선호계량화 — 지금까지는 '아이가 잘 먹었다(ate_well)'만 봐서, 4번 다 다른 음식을 한 번씩 잘 먹어도 '4개를 좋아한다'고 오판했습니다. 이제 같은 음식을 2번 이상 완식 + 통계적으로 확실해야만 '확신하고 좋아함(liked)'으로 칩니다. 아린은 이 기준으로 '확신 liked = 0' → 그래서 '잘 먹는 것 위에 다리 놓기'가 아니라 '콜드스타트(자주 차려진 음식으로 출발)'로만 작동합니다.
② P0-D 프로브 — 코치가 매일 던지던 질문이 '답해도 신호로 흐르지 않던' 다리를 잇기 위해, 오늘 가르치는 유닛의 정식 질문(정규 칩)을 매일 던져 부모 답변을 신호 표본으로 쌓습니다(졸업 판정의 연료). 단, 출력(질문 내보내기) 다리만 깔렸고 입력(답변이 신호로 들어오는 흐름)은 아직 비어 있습니다 — ⑤ 남은 문제 참고.
구현된 내용 — 함수·데이터흐름·입출력 (실측)
현재 코드에 실제로 존재하는 함수·타입·file:line·데이터흐름입니다. 환각 없이 실측만.
핵심 함수
| 함수 · file:line | 역할 (실측) |
|---|---|
pickActionByBrain(ctx, candidates, model)coachBrain.ts:115 | DeepSeek V4-Pro 두뇌 호출 → BrainAction 반환. scenarioId는 SCEN_IDS 화이트리스트(coachBrain.ts:32) 검증 — 목록 밖이면 ''(빈값)으로 두고 호출측이 v2 폴백. useFood boolean·approvedRecs 방어 파싱. |
buildBrainContext(p)coachBrain.ts:48 | 13개 시나리오 메뉴(발동가능·최근사용 플래그) + 영양거울 + 추천후보 + 최근 3주 닻(weeklyEchoes) + 최근편지 + anchorBlock(coachBrain.ts:79 — 닻 lever가 비-food면 '음식 시나리오로 매일 갈아끼우지 마라' 지시) + useFoodBlock(coachBrain.ts:82 — 최근 음식날 시퀀스 '음식→비음식→…' + 직전 2일 음식이면 다른 각도로) + curriculumSummary를 JSON 지시 프롬프트로 조립. |
anchorOverrideAllowed({anchorLever,sid,fov,triggerOk})coachWeekly.ts:121 | ⭐ 두뇌 override 게이트(핵심). {allow, isFoodOverride} 반환. SAFE_INTERRUPT_SCENARIOS(progress-celebrate·neophobia-arfid-watch·low-data-gap)거나, food주에선 비구조 시나리오, 비-food주에선 그 레버의 LEVER_SCENARIO[lever] 시나리오면 호환. food주 아닌 식 override는 FOOD_OVERRIDE_CAP=2(coachWeekly.ts:110) 캡 내에서만. triggerOk=false(데이터에 신호 없음)면 불허. |
planFromWeekly(p) / weeklyReplan(sid)route.ts:740 / 750 | 월~토 닻 실행 — mission_target 잠금·채근캡·아크 단계 회전. weeklyReplan은 두뇌 override 허용 시 '닻 안에서 시나리오만 교체'(타깃·채근캡·effectiveLever 보존). |
quantifyPreferences(rows, asOf, {homeOnly})preferenceQuantification.ts:51 | ⭐ 선호계량화. 수용 5단계(0거부·1만짐·2한입·3조금·4완식)로 식재료별 FoodPref(state·score·confidence·trend) 산출. homeOnly 기본 true(기관 급식 제외). state: 신호<2면 unknown, 거부≥2&수용률<0.34면 disliked, 완식≥2&Wilson≥0.5면 liked, 나머지 exploring. |
acceptanceLevel(r) / wilsonLower(pos,n)preferenceQuantification.ts:32 / 40 | acceptance_level(5단계 컬럼) 우선, 없으면 ate_well 폴백(true→4·false→0·null→미상). wilsonLower=Wilson 95% 하한(표본 적을수록 보수적 — 1/1 완식을 '확실'로 단정 안 함). |
confidentLiked(prefs) / dislikedFoods(prefs)preferenceQuantification.ts:91 / 95 | state==='liked'만 추천 앵커·콜드스타트 게이트로(미상·탐색 제외). dislikedFoods=확실 거부 → 추천 후보에서 제외(거부한 걸 또 권하지 않기). |
pickUnitProbe(unitId, seed)coachDaily.ts:71 | ⭐ P0-D 유닛 프로브 회전. 오늘 focus 유닛의 정규 프로브(passWhen 신호) 1개를 seed % probes.length로 일별 회전 emit. 반환 {question, topic, chips, unitProbe:{unit_id,signal,probeId}}. 유닛/프로브 없으면 null(→ LLM 질문 폴백). |
parseProbeAnswers(qs) / buildCandSignals(rows,…)coachDaily.ts:52 / 17 | parseProbeAnswers=부모 답변 → evidence. context.unitProbe 있고 답이 정규 칩과 정확 일치할 때만 적립('잘 모르겠어요'·자유텍스트 미적립). buildCandSignals=주간 후보 신호(envBadPct·refusedCount 등) rows에서 계산. |
SCENARIOS / selectScenario(s,recentIds)coachScenarios.ts | 13개 시나리오 상수(라이브러리) + 결정론 폴백 셀렉터 — trigger 충족분을 priority 내림차순, 최근 2일 1차 제외, fresh→fired→plateau 폴백. 두뇌 실패·빈값 시 안전망. |
일간 두뇌 데이터흐름 (route.ts 실측 라인)
| 단계 · route.ts 라인 | 무엇을 하나 |
|---|---|
| 선호계량화 · route.ts:368-374 | prefs=quantifyPreferences(rs, today) → confidentLiked·dislikedIng. coldStart = confidentLiked.length < 2(route.ts:372). 콜드스타트면 coldStartSeed(servedTop,6)로 앵커 보충, 아니면 likedIng. dislikedIng는 앵커에서 제외(route.ts:374). |
| 신호 묶음 · route.ts:547-554 | CoachSignals 조립 — 1주차 미성숙(loggedDays<7)이면 결핍 신호(reds/missing/homeMissing) 비움(route.ts:542-546, 결핍 단정 금지). envBadPct=집 끼니만(route.ts:529). |
| 주간 닻 잠금 · route.ts:702-753 | anchor.mission_target·lever 로드 → targetAccepts(route.ts:711 — acceptanceLevel≥1만, 미상 null 배제 ⭐신호포착) → planFromWeekly(route.ts:740) → weekCtx에 lever·targetPool·ledger 노출(route.ts:745, 두뇌 게이트 입력). |
| 연속 food날 캡 · route.ts:774-775 | food주=3일·비-food주=2일 연속 useFood면 brainPick.useFood=false 강제(프롬프트 신뢰 대신 코드 보증). |
| override 게이트 · route.ts:776-799 | 두뇌 scenarioId 있으면 safeTrigger(sid)(route.ts:778, signals 트리거 충족) + anchorOverrideAllowed(route.ts:782) 통과 시에만 weeklyReplan(sid) 또는 planFor(forceScenarioId). isFoodOverride면 arc=null(잡탕 차단·route.ts:787) + foodOverrideUsed 카운트 적재(route.ts:792-796). |
| 슬롯-본문 봉합(F-18) · route.ts:857-872 | 음식 시나리오(home-daycare-gap·nutrient-gap·repeat-menu)는 _noFood를 끄고 슬롯 음식을 본문에 강제(route.ts:859-860, 콩류→두부 디폴트 차단). slotDish/slotFood(route.ts:864-865) must-weave. 거울 음식=항상 그날 슬롯 음식(route.ts:870-889, deficitDishFor 제거). |
| 거울 출현빈도 쿨다운(#5) · route.ts:890-899 | mirrorCooldownDue(route.ts:894) — '어린이집 덕에…' 거울줄이 최근 2일 안에 나왔으면 오늘 생략(전체 결핍 2개+면 면제). 무슬롯 경로도 동일 쿨다운(골든완화). |
| 도입 회전(#6) · route.ts:876-883 | 거울 positive 문장을 daySeed 변주 3종(_posV·_foodClause)으로 회전 — 템플릿 축자 반복(4~11통) 차단. |
| macro input-starvation(#7) · route.ts:229-232 | 성장데이터(키·몸무게·성별) 없으면 macro 트랙(저체중→고기↑)이 영영 비활성 → 성장 권유를 매 3일·효과 명시로 데이터 유입 유도, 들어오면 자동 중단. |
| P0-D 프로브 emit · route.ts:989-1002 | ICFQ 스크리너 아닌 날 pickUnitProbe(curriculumDecision?.unit, day-number)(route.ts:990) → 질문 + context.unitProbe 저장(route.ts:999-1002). 출력 다리. |
A 추천 근거 — 어드민 노출(이번 세션)
coach_letters.context에 brain(scenarioId·useFood·why) + weekly(lever·effLever·missionTarget·targetPool·ledger) + planSlot(slotIndex·ingredient·dishes·group·track·via·macro) + recoIng/recoPool/recoMode + curriculum(unit·step·mode·pivotTo)로 적재(route.ts:947-960) — 어드민이 "결핍군 콩류 보급(주N회)"·"잘 먹는 두부의 검증 사촌" 같은 추천 근거를 검증.어느 테이블에서 무엇을 뽑나
| 테이블 · 데이터 | 뽑는 값 | 운영 의미 |
|---|---|---|
| meal_logs | ingredients·refused·note·place·ate_well·acceptance_level(5단계)·environment·autonomy·texture·meal_time | 아이 '오늘 상태표'. acceptance_level 컬럼이 선호계량화 granular 입력(없으면 ate_well 폴백) |
| weekly_plans | mission_target·budget(lever·push)·ledger(targetAccepts·foodOverrideUsed)·goals·impression | 이번 주 '나침반'. 두뇌 override 게이트가 이 lever·ledger를 읽고 잠금 |
| coach_letters.context | 최근 scenarioId·plan·brain.useFood·weekly·mirrorShown — 중복 회피 이력 | '어제 뭐라 보냈는지'로 같은 각도·연속 food날 반복 차단(다양성 장치) |
| daily_questions | answer·context.unitProbe(P0-D)·icfq | 프로브 답변→신호 적립(parseProbeAnswers). ICFQ 위험 누적은 ARFID 신호 |
| curriculum_progress | unit_id·status·step·evidence | 오늘 focus 유닛 결정(pickUnitProbe·effectiveLever 입력) — 정의·측정은 ⑤ |
| SCENARIOS (코드 상수) | 13개 각도의 label·priority·trigger·promptHint·avoid | DB 아닌 코드 라이브러리 — 두뇌가 이 안에서만 골라 환각 0 |
설계 아키텍처 시각화 — 입력테이블 → 가공함수 → 출력키
데이터 파이프라인을 '그림'으로. 좌 입력테이블 → 중 가공함수 → 우 출력 컨텍스트키.
일간 두뇌 결정 트리 (route.ts:755-799) ─ '뇌는 작문 안 함, 각도/타깃/useFood/lever 회전만 결정론 보증'
signals + weekCtx(lever·ledger) ──▶ pickActionByBrain ──▶ brainPick{scenarioId, useFood, why}
│
┌──────────────────────────────────────┤
▼ ▼
연속 food날 캡(774-775) scenarioId 있나?
food주 3일/비-food 2일 │
─▶ useFood=false 강제 ┌────────┴────────┐
▼ ▼
safeTrigger(sid)? (빈값/실패)
signals에 신호 있나 │
│ ▼
┌────────┴───────┐ 결정론 selectScenario 폴백
▼ ▼ (planFor precomputed 유지)
anchorOverrideAllowed(782) 미충족
food주│레버호환│안전인터럽트│cap내 │
│ ▼
┌────────┴────────┐ override 무시 ─ 닻 프레임 유지
▼ allow=true ▼ allow=false (비-food주 음식잔소리 차단)
weeklyReplan(sid) 닻 lever 프레임 유지
타깃·채근캡 보존 (precomputed=닻)
isFoodOverride면 arc=null + foodOverrideUsed++
EARNED: 게이트는 코드에 박혀 프롬프트 신뢰가 아님. 트리거 미충족·비-food주·연속food = 결정론으로 막힘.
천장: 게이트 입구의 신호(targetAccepts·envTablePct)가 거의 0 ─ 두뇌가 mealtime-atmosphere에 고착(⑤ 참조).작동 예시 — 아린 데이터 before/after
실제 입력(아린·5세) → 이 모듈 통과 → 출력. 핵심 변화 = 선호계량화로 콜드스타트가 켜지고, 닻 게이트로 음식 override가 막힌다.
// meal_logs(집 끼니만, 최근 7일):
// 미역국·소고기·계란·감자 각 1회 ate_well=true (서로 다른 식재료)
// acceptance_level 대부분 null(미상 80%)
// environment: 5끼 중 3끼 'screen' → envBadPct 0.55
// note: "저녁마다 유튜브 보며 먹어요"
signals = {
homeMissing: ["콩류","비타민A채소"], missing: [], // 기관 급식 덕 전체 OK
homeRefused: ["당근"], daycareRefused: [],
notes: ["저녁마다 유튜브 보면서 먹어요"],
favoriteFoods: ["소고기","계란","감자","미역국"],
attendsDaycare: true, envBadPct: 0.55, icfqRiskCount: 0
}
weekCtx = { lever: "environment", missionTarget: "콩류",
ledger: { targetAccepts: 0, foodOverrideUsed: 0 } }// ① 선호계량화(quantifyPreferences):
// 미역국·소고기·계란·감자 = 각 1회 → signal<2 → state="unknown"
confidentLiked = [] // ⭐ 0개! (구 누적기는 4개로 오판)
coldStart = true // confidentLiked.length < 2
likedSeed = coldStartSeed(servedTop,6) // 자주 차려진 음식으로 출발
// ② 두뇌 brainPick:
{ scenarioId: "nutrient-gap", useFood: true,
why: "콩류가 집에서 비어 음식 각도 제안" }
// ③ override 게이트(anchorOverrideAllowed):
// anchorLever="environment"(비-food) · sid="nutrient-gap"(음식)
// → LEVER_SCENARIO[environment]="mealtime-atmosphere" ≠ sid
// → 안전인터럽트 아님 · foodOverrideUsed(0)<CAP(2)
// → isFoodOverride=true, allow=true(캡 내 1회 허용) ...단
// recentUseFood 직전 2일 모두 음식이었으면 774-775서 useFood=false 선차단
// ④ 최종 CoachContext:
scenarioId: "mealtime-atmosphere" // 환경 닻 프레임 유지(override 캡 소진 시)
plan.target: "콩류"(배경) · weekCtx.effLever: "environment"
slotFood: 슬롯 dish(콩류 supply 날엔 두부 아닌 구체 dish)
mirrorLine: 쿨다운이면 null문헌 · 이론 근거 (이 모듈이 적용하는 이론)
| 이론 · 문헌 | 어떻게 적용하나 |
|---|---|
| 반복 노출(HabEat)·단순노출 | 거부 음식을 격일~주2~3회 작게 다시 올려 친숙도 축적(진전축하·새거부·재노출 시나리오 근거) |
| ARFID 구분·감각적 음식 거부 | 식사 적신호가 60일 내 2회+ 누적되면 진단 없이 전문가 안내 1회 + 채근 0(neophobia-arfid-watch·push=0) |
| 자율성 지지(SDT)·Satter DOR | 어린 연령 광범위 거부를 발달 과업으로 보고 결과는 같되 작은 선택권(자율성·집기관격차 시나리오) |
| 압박의 역효과·무압력 식사 | 환경에 압박·영상 신호가 있으면 단일 레버 1개만(mealtime-atmosphere) |
| 보상 역효과(과잉정당화) | 보상·디저트 협상이 장기 선호를 떨어뜨림을 알리고 거래를 끊게 함(reward-bribe-backfire) |
| 푸드 체이닝·맛-맛 연합 | 부족 식품군을 좋아하는 음식에 다리 놓기(nutrient-gap·repeat-menu) — 슬롯 dish로 구체화 |
| Wilson 점수 하한(통계) | ⭐ 선호계량화 — 1/1 완식을 '확실히 좋아함'으로 단정 안 함(표본 적을수록 보수적, likedScore≥0.5 게이트) |
| 편식 궤적(자연 완화)·신뢰 모델 | 뚜렷한 신호 없으면 행동 빼고 칭찬만(plateau·low-data-gap) |
| Fogg 행동설계 · Gollwitzer 실행의도 | 주간 행동목표를 '하루 한 끼'의 가장 작은 행동 + 구체 트리거 1구('저녁 6시 TV 끄고')로 |
남은 문제점 · 해야할 일 (랄프위검 47점·2026-06-20)
랄프위검(연속성 감사) 47점. 이 모듈 관점에서 정직하게 — 증상 → 근본원인 → 해야할일. 미화 없음. 단 사실인 성과는 EARNED로 명시.
| 증상 | 근본원인 | 해야할일 (owner) |
|---|---|---|
| [천장1·뿌리] P0-D 환경신호가 step 전진 트리거 0 — 졸업 0, 피벗만 됨 | child_daily_state·envTablePct가 '다리만 깔리고' 실데이터 미입력. pickUnitProbe가 context.unitProbe로 질문은 내보내나(출력 다리·route.ts:990 EARNED), 답변 파이프(parseProbeAnswers→advanceProgress)에 양성신호가 안 들어옴 — 입력 다리 공백. targetAccepts(route.ts:711)·focus 유닛 passWhen이 표본 0이라 영구 보류 | 환경신호 '입력 UI/경로'까지 정의(현재 출력 다리만) — care 5단계/환경칩 입력이 child_daily_state·envTablePct로 흘러 parseProbeAnswers 양성표본을 쌓게 배선. owner: route.ts + ⑤ 진척(advanceProgress passWhen) + care UI |
| [천장5] 시나리오 mealtime-atmosphere 고착 — 9일 중 7일 동일 각도 | focusFatigue(정체 유닛/시나리오 강등)가 environment 레버를 강등 후에도 즉시 재선택 → 강등 면역. 강등 기준이 '직전 주 비교'라 일간 누적빈도를 못 봄. 시나리오 다양성이 도입 다양성과 직결 | focusFatigue 강등 기준을 '연속 정체일/시나리오 누적빈도'로 교체(직전 주 비교 → 최근 N일 시나리오 카운트). recentScenarios 윈도를 두뇌 입력에 누적빈도로 주입해 7일 중 5일+ 동일이면 강제 차순위. owner: route.ts:584-590 focusHistory + buildBrainContext |
| 선호계량화 confidentLiked=0 → 콜드스타트만 — 영원히 콜드스타트 | 아린 양성신호 없음(완식 반복 2회+ 없음). 미상 null을 liked 오판 차단은 정상(EARNED). 그러나 신호 입력이 0이면 콜드스타트 탈출 불가 — 천장1과 같은 뿌리(신호 0 구간) | 신호 0 구간 식별 후 시나리오 강제 다변화 — confidentLiked=0 + targetAccepts=0이 N일+ 지속이면 '신호 수집 모드'로 진입해 시나리오를 강제 회전(같은 각도 고착 방지)하고 프로브를 우선 emit. acceptance_level 입력 유도(care 5단계)로 양성표본을 적극 수집. owner: route.ts:372 coldStart 분기 + pickUnitProbe 우선순위 |
| 추천 풀 협소 — 콜드스타트 후보가 두부5·달걀6에 수렴 | 콜드스타트 Tier2→Tier3(youa 큐레이션)까지 못 가 풀이 좁음 · 앵커 다양화 부족(돼지불고기 단일) | P0-B/C — 추천 풀 확장(Tier3 큐레이션 배선) · 앵커 다양화. owner: ② 추천엔진(coachRecos) — 본 모듈은 confidentLiked/likedSeed 제공만 |
| lever 태그 정합·plateau 카피풀 | effLever 디버그 표시와 본문 레버가 어긋나는 케이스 산발 · plateau/무브 카피 풀 빈약(반복 톤) | lever 태그 정합 디버그 · plateau·무브 카피풀 확충. owner: route.ts weekCtx.effLever + ⑥ 작문기 카피풀 |
가드 — 괴식·환각·안전 차단 (실측)
- scenarioId 화이트리스트(
SCEN_IDS·coachBrain.ts:32) — 목록 밖이면 빈값 → 결정론 selectScenario 폴백(환각 시나리오 차단) - approvedRecs는 food-graph 후보의 부분집합만·useFood=false면 빈 배열 — 후보 밖 음식·조합 추가 불가(괴식/환각 0)
- 닻 종속 override 게이트(
anchorOverrideAllowed) — 비-food주·트리거 미충족·캡 초과 시 두뇌 override 불허(코드 보증, 프롬프트 신뢰 아님) - 연속 food날 캡(route.ts:774-775) — food주 3일·비-food주 2일 연속 useFood면 강제 휴지
- 선호 오판 차단 —
acceptanceLevel이 null(미상)을 liked/disliked 어디에도 안 셈. confidentLiked=완식≥2+Wilson만(미상 80% 아이를 '잘 먹는다' 오판 차단) - targetAccepts 미상 배제(route.ts:711) —
acceptanceLevel≥1만 진척으로(구ate_well!==false는 미상도 진척으로 세 같은 타깃 영구 반복했음) - 프로브 적립 보수(parseProbeAnswers) — context.unitProbe + 정규 칩 정확 일치만(자유텍스트·'잘 모르겠어요' 미적립)
- ARFID 적신호(icfqRiskCount≥2) → push=0 선잠금·진단/'장애' 단어 금지·1회만 안내
- 안전 제1원칙 — 주간 닻은 throw 안 함(cold 폴백·null degrade) → 닻 실패해도 일간 엔진 발행 보장