case 3/4 의 setup 이 EXPLANATION_MAX_CHARS (1200) 보다 작은 text 를 만들어
assert 실패. 한글 chunk 반복 횟수 늘려 1200 자 이상 보장.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
운영 데이터에서 ready 박힌 풀이가 793/838/866자 — 권장 200~400 대비 큰 편.
1차 운영 후 결과 화면 가독성 + 토큰 사용량 통제 위해 prompt 강화 + 저장 전 cap.
Prompt (study_explanation_envelope.txt):
- explanation_md 권장 300~600자, 최대 900자 명시
- 핵심 개념 + 정답 근거 + 헷갈리는 1~2개 오답만 — 모든 오답 풀이 X
- explanation_md 안 줄바꿈 최소화 (parse_json fix 와 결합 — invalid escape 줄임)
- LaTeX 수식 자제 — \\circ/\\text/\\, 매크로 가능하면 평문 ('0°C', 'C')
- 출력은 raw JSON 한 객체만 — 코드 펜스/thinking/메타 X 강조
Worker (study_explanation_worker.py):
- _cap_explanation_md(text, max_chars=1200) 헬퍼 신규
· 1200자 이하 passthrough
· 초과 시 마지막 200자 안에서 \\n\\n / \\n / '. ' / '다.' / '요.' 경계 탐색
· 경계에서 자르기 + '…' (단어 중간 자르기 회피)
· 경계 못 찾으면 단순 자르기 + '…'
- save 전 cap 적용. ai_explanation_status='ready' 유지 (cap 됐다고 failed X)
- payload 에 운영 분석 metadata: explanation_len_original / _saved / capped 플래그
검증:
- tests/test_explanation_cap.py (6 케이스)
· short passthrough / exact at limit / paragraph boundary / sentence boundary
· no boundary fallback / empty input
- scripts/phase4_health.sql 섹션 8/9 추가
· ai_explanation 길이 p50/p95/max (study_questions.ready)
· cap 작동 빈도 (job.payload 의 explanation_capped/_original/_saved)
cap 1200 = 800 (4-B summary_md) 보다 여유 — 기사시험 풀이는 공식+오답+개념 묶이면
800 빡빡함. 운영 후 800~1000 으로 조정 검토.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 4-B v1 첫 검증 결과 자료 부족 토픽인데도 모델이 confidence='high'
박는 케이스 발견. 정의 (high = 자료 + 다른 ai_explanation 으로 패턴 명확)
보다 과신 — UX 신뢰도 위험. 자동 cap 보정 + 운영 관찰 SQL 추가.
confidence calibration (services/study/session_summary_guard):
- calibrate_confidence(c, ctx_docs_count, ready_explanation_count) 신규
· ctx_docs_count == 0 AND ready_explanation_count == 0 → 'low' cap
· ctx_docs_count == 0 (ready 만 있음) → 'medium' cap
· ctx_docs_count >= 1 → 모델 값 그대로
- 모델이 정의보다 더 보수적인 값 박은 경우 (모델 'low' + cap 'medium') 는
보존 — 더 보수적인 값을 절대 올리지 않음
worker 적용 (study_session_analysis_worker):
- ctx_docs_count = len(ctx_docs)
- ready_explanation_count = sum(1 for a in prompt_attempts if a.get('ai_explanation'))
- calibrate_confidence 호출 → study_quiz_session_analysis.confidence 박힘
- job.payload 에 운영 분석 metadata 보존:
· ctx_docs_count / ready_explanation_count
· model_confidence_raw (모델 응답) vs calibrated_confidence (cap 후)
· prompt_attempts / valid_attempts_total / summary_len
→ SQL 4 번 쿼리가 cap 작동 빈도 측정
scripts/phase4_health.sql (신규 운영 점검 SQL 7 섹션):
1. 4-A study_question_jobs status × error_code 분포
2. 4-B study_quiz_session_jobs status × error_code 분포
3. 4-B confidence 분포 (calibrated)
4. 4-B model_confidence_raw vs calibrated 차이 (cap 작동 빈도)
5. 4-A/4-B 최근 7일 처리 지연 p50/p95/max/avg
6. 4-A/4-B skipped 사유 분포
7. 4-B guard_fail / parse_fail / llm_timeout 비율
ship gate (단위 테스트):
- test_calibrate_confidence_no_evidence_caps_to_low (3 케이스)
- test_calibrate_confidence_only_explanations_caps_to_medium (3 케이스)
- test_calibrate_confidence_with_documents_passthrough (3 케이스)
- test_calibrate_confidence_normalizes_invalid_first (2 케이스)
Plan: ~/.claude/plans/nifty-sparking-spindle.md (Phase 4-B v1 후속)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
프로덕션 컨테이너는 /app 을 cwd 로 실행하고 import 는 `from api...`,
`from core...`, `from workers...` 처럼 무접두 스타일을 사용한다.
PR-A 내부 import 가 `from app.policy...`, `from app.ai.envelope` 로
되어 있어서 컨테이너에서 ModuleNotFoundError 발생.
변경:
- app/policy/*.py: `from app.policy.X` → `from policy.X`
- app/services/prompt_versions.py: lazy import 도 `from policy.prompt_render`
- app/ai/envelope.py: 영향 없음 (내부 import 없음)
- tests/policy/*.py: 모두 `from policy.X` / `from ai.envelope` 로 통일
- tests/policy/conftest.py: 로컬 pytest 용 sys.path.insert(app/) 추가
(MacBook 에서 repo-root 기준 실행 시 app/ 를 package root 로 취급)
CI: pytest tests/policy/ -q → 98 passed (로컬, 동일 결과)
프로덕션: docker exec fastapi python -c "from policy.loader import load_policy" → OK
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VERIFIER_NUMERIC_PROMOTE 환경변수로 numeric_conflict severity 승격 실험.
verifier_service.py:
- _NUMERIC_PROMOTE = os.getenv('VERIFIER_NUMERIC_PROMOTE', '0') == '1'
(import time 평가 — env 변경 시 process restart 필수)
- _SEVERITY_MAP['numeric_conflict']: env=1 → critical=strong / minor=medium,
env=0 (기본) → 둘 다 medium (기존 동작 유지)
- direct_negation 은 env 무관 항상 strong (안전장치)
verifier.txt:
- numeric_conflict 정의에 critical/minor 분리 명시 (core quantity vs peripheral)
- "Range values satisfy any answer within range" rule 추가
- severity mapping 갱신: numeric_conflict 분기 명시
search.py re-gate (Tier 1~7 재번호, B2 신규 Tier 4):
- v_strong_numeric = sum(1 for f in v_strong
if f.startswith('verifier_numeric_conflict'))
- Tier 4 (신규): g_strong + v_strong_numeric >= 1 + low_conf → refuse
re_gate value: 'refuse(grounding+verifier_numeric)'
- 원칙 유지: verifier strong 단독 refuse 금지 — g_strong 교차 필수
- 호환성: 기존 re_gate string literals 그대로 유지, 신규 1개만 추가
credentials.env.example: VERIFIER_NUMERIC_PROMOTE=0 (off, B3 통과 후 production 전환)
tests/test_verifier_numeric_promote.py: 4 케이스 (env off / on / explicit 0 /
direct_negation invariant). monkeypatch.setenv + importlib.reload 패턴.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Codex adversarial review (no-ship) 반영:
fix1: unit-aware numeric clearing
- _extract_numeric_corpus(): 단위별 bucket dict (exact_by_unit) +
ranges_by_unit (양방향 + 단방향 bound 통합)
- _within_unit_range / _close_to_unit_pool: 같은 unit 안에서만 매칭
bare answer 는 보수적으로 range/tolerance 패스 X
- 2-pass cleared_pairs (unit, digits): cross-unit cleared 절대 skip 안 함.
bare(None) 답변은 unit-anchored cleared 시 duplicate 로 skip
(콤마 normalize 부산물 보호 — Codex 케이스는 그대로 flag)
fix3: 최대/최소 bound semantics
- _APPROX_PREFIX_RE 에서 최대/최소 제거 (약/대략/거의/얼추 만 strip)
- _BOUND_PATTERN_RE: 최대 N → range (0, N-1), 최소 N → range (N+1, 1e18)
- 경계값 자체는 cleared 대상 아님 ("최대 100명" + answer "100명" → flag)
- bound span 내 숫자는 exact pool 에서 제외
기존 prefix strip / 콤마 / 부터 separator / 단위 동의어 / tolerance 4자리+ /
식별자성 단위 1자리 flag 동작 모두 유지.
tests/test_grounding_fabricated_number.py: 25 케이스 — 기존 17 + Codex
unit-mismatch 3 (won_vs_myeong_range/tol, pct_vs_myeong_range) + bound 5
(최대/최소 boundary/inner/outer).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## 배경
1차 Phase 2.2 eval에서 발견: 23개 쿼리가 순차 호출되지만 각 request의
background analyzer task는 모두 동시에 MLX에 요청 날림 → MLX single-inference
서버 queue 폭발 → 22개가 15초 timeout. cache 채워지지 않음.
## 수정
### query_analyzer.py
- LLM_CONCURRENCY = 1 상수 추가
- _LLM_SEMAPHORE: lazy init asyncio.Semaphore (event loop 바인딩)
- analyze() 내부: semaphore → timeout(실제 LLM 호출만) 이중 래핑
semaphore 대기 시간이 timeout에 포함되지 않도록 주의
### run_eval.py
- --analyze true|false 파라미터 추가 (Phase 2.1+ 측정용)
- call_search / evaluate 시그니처에 analyze 전달
## 기대 효과
- prewarm/background/동기 호출 모두 1개씩 순차 MLX 호출
- 23개 대기 시 최악 230초 소요, 단 모두 성공해서 cache 채움
- MLX 서버 부하 안정
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
tests/scripts/reindex_all_chunks.py — 전체 documents chunk 재인덱싱 도구.
핵심 요건 (사용자 정의):
- asyncio.Semaphore(N) — 동시 처리 수 제한 (기본 3, Ollama bge-m3 부하 조절)
- checkpoint resume — JSON 파일 atomic swap, 중간 실패/중단 후 재시작 가능
- rate limiting — 작업 간 sleep 0.1초 (Ollama API 보호)
- 진행 로그 — [REINDEX] N/total (P%) ETA: ... fails: N (~2% 단위)
CLI:
- --concurrency, --checkpoint, --rate-limit, --limit (dry-run), --skip-existing
야간 배치 (00:00~06:00):
PYTHONPATH=app .venv/bin/python tests/scripts/reindex_all_chunks.py \
--concurrency 3 --checkpoint checkpoints/reindex.json \
> logs/reindex.log 2>&1 &
22개 쿼리(6개 카테고리)와 Recall/MRR/NDCG@10 + latency p50/p95
측정 스크립트 추가. wiggly-weaving-puppy 플랜 Phase 0.2 산출물.
- queries.yaml: 정확키워드/한국어자연어/crosslingual/뉴스/실패 케이스
실제 코퍼스(2026-04-07, 753 docs) 기반 정답 doc_id 매핑
- run_eval.py: 단일 평가 + A/B 비교 모드, CSV 저장
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- scripts/pkm_utils.py: 공통 유틸 (로거, dotenv, osascript 래퍼)
- scripts/prompts/classify_document.txt: Ollama 분류 프롬프트
- applescript/auto_classify.scpt: Inbox → AI 분류 → DB 이동
- applescript/omnifocus_sync.scpt: Projects → OmniFocus 작업 생성
- scripts/law_monitor.py: 법령 변경 모니터링 + DEVONthink 임포트
- scripts/mailplus_archive.py: MailPlus IMAP → Archive DB
- scripts/pkm_daily_digest.py: 일일 다이제스트 + OmniFocus 액션
- scripts/embed_to_chroma.py: GPU 서버 벡터 임베딩 → ChromaDB
- launchd/*.plist: 3개 스케줄 (07:00, 07:00+18:00, 20:00)
- docs/deploy.md: Mac mini 배포 가이드
- docs/devonagent-setup.md: 검색 세트 9종 설정 가이드
- tests/test_classify.py: 5종 문서 분류 테스트
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>