Plan 본래 의도: 근거 선별은 4B, 합성은 26B.
- evidence_service: LLM 호출을 primary(26B MLX) → triage(4B Ollama) 로 전환.
Ollama concurrent 가능하므로 get_mlx_gate() 제거. synthesis 는 여전히
llm_gate Semaphore(1) 경유로 MLX 보호.
- prompt_version v3-evidence-triage bump (synthesis 프롬프트 자체는 v2-600char
그대로, evidence LLM 경로 변경을 분리 추적).
- migrations 161/162: analyze_events 에 answerability / partial_basis /
suggested_query_count 컬럼 + partial index. /ask 는 이미 ask_events 에
completeness (full/partial/insufficient) 기록 운영 중이므로, analyze_events
쪽은 향후 문서 분석에서 answerability 개념 도입 시 활용 예비.
- telemetry record_analyze_event 에 answerability / partial_basis /
suggested_query_count 파라미터 확장.
기존 /ask 3-state completeness 로직 (classifier_service + 7-tier gate) 은
그대로 유지 — 이미 Phase 3.5a 에서 완성된 상태. B-2 는 LLM 부하 재분배와
관측성 확장에 집중.
MLX 부하 감소 효과: 이전엔 쿼리 1건당 evidence(26B) + synthesis(26B) 2번
MLX 호출. 이제는 evidence(4B Ollama) + synthesis(26B MLX) 로 MLX 호출 절반.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- docker-compose.yml stt-service 를 profiles:[legacy] 로 이동. GPU 의
stt-service 는 더 이상 기동하지 않고, fastapi STT_ENDPOINT 가
Mac mini (기본 100.76.254.116:8804 Tailscale, MAC_MINI_HOST env 로
LAN IP 주입) 를 바라보도록 변경. 복원 필요 시
`docker compose --profile legacy up -d stt-service`.
- config.yaml: classifier 섹션을 gemma4:e4b-it-q8_0 으로 복원. 이전
B-0 커밋이 classifier 를 주석 처리했는데, 실제로는 classifier_service
가 쓰고 있어 gate 유효. exaone 은 이미 제거됐으니 모델만 gemma4 로
통일. classifier_service 의 hasattr 체크는 유지되어 fallback 안전.
D13 (STT 이전) drift 를 main 으로 승격. inventory 갱신은 B-3 마감
단계에서 3-tier + STT 경로 묶어서 일괄.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- config.yaml: ai.models 에 triage (gemma4:e4b-it-q8_0, GPU Ollama,
context_char_limit=120k, timeout 30s) 신규. primary (MLX gemma-4-26b)
는 에스컬레이션 전용 역할 명시. fallback 을 gemma4:e4b 로 통일
(exaone 제거 이미 반영). classifier/verifier 는 optional 유지,
vision 은 optional 로 완화 (미사용 정리 준비).
- core/config.py: AIConfig 에 triage 필드 추가, vision 은 Optional 로
전환. AIModelConfig.context_char_limit + DeepSummaryBacklogConfig
(R2 backlog guard 임계치 ratio 0.3 / pending 5 / window 30min)
스키마 신설. load_settings 가 models.get("vision") graceful.
- ai/client.py: call_triage / call_primary / call_fallback 3-tier
진입점 신규. primary 는 caller 가 get_mlx_gate() 블록 안에서 호출
해야 한다는 계약 docstring. classify/summarize 는 DEPRECATED 주석
만 추가, 기존 호출부 (eval runner 등) 를 위해 유지.
PR-B B-0 Day 1. 기존 primary 경로 변경 없음 — 회귀 0 기대.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
배포 검증 중 발견: domain_policy.yaml 이 repo root 에 있지만 fastapi
컨테이너의 build context 는 ./app 이라 COPY 가 포함하지 못함. 결과
load_policy() 가 FileNotFoundError.
1. docker-compose.yml: config.yaml 과 동일 패턴으로 읽기전용 bind mount
- ./domain_policy.yaml:/app/domain_policy.yaml:ro
2. app/policy/loader.py: _resolve_path 에 4 개 후보 검색 추가 —
cwd / /app / /app/.. / <this>.parent.parent.parent 순으로 파일 존재
확인. 첫 매칭 반환. 로컬/컨테이너/다른 배포 환경 모두 호환.
CI: pytest tests/policy/ -q → 98 passed.
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>
asyncpg exec_driver_sql 이 prepared statement 로 multiple commands 를
허용하지 않아 배포 시 PostgresSyntaxError: cannot insert multiple commands
into a prepared statement 로 init_db() 실패.
153 를 단일 ALTER TABLE (10 ADD COLUMN) 로 축소하고 2 partial index 를
154/155 로 분리:
- 153_analyze_events_shadow.sql: ALTER TABLE ADD COLUMN (단일 statement)
- 154_analyze_events_shadow_idx_ts.sql: idx_analyze_events_shadow_ts
- 155_analyze_events_policy_violation_idx.sql: idx_analyze_events_policy_violation
배포 test: GPU fastapi 컨테이너 재빌드 후 init_db 가 153/154/155 세 파일을
순차 적용 (asyncpg prepared statement 1 파일 1 문).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ALTER TABLE analyze_events ADD COLUMN IF NOT EXISTS 로 10개 shadow 컬럼:
subject_domain, risk_flags[], high_impact_task, escalated_to_26b,
escalation_reasons[], confidence, policy_violation, policy_violation_ids[],
shadow_would_route_to, policy_version.
+ 2 partial index:
- idx_analyze_events_shadow_ts (shadow_would_route_to IS NOT NULL)
- idx_analyze_events_policy_violation (policy_violation=true)
전부 nullable, 기본값 NULL. 아무도 쓰지 않음 — PR-B 의 DBShadowLogger 가
writer 추가 예정.
번호 153: 152 는 `feat(category): law` 가 점유 (e88640d).
BEGIN/COMMIT 없음 (CLAUDE.md: _run_migrations 단일 outer 트랜잭션).
answerability / new_facts_count 는 PR-B 의 migration 154+ 가 소유.
plan: ~/.claude/plans/wise-gliding-hippo.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ShadowLogger (runtime_checkable Protocol) — PR-B 가 DBShadowLogger 구현
시 준수해야 할 인터페이스. record_would_route(*, doc_id, decision,
actual_model_used, prompt_version, policy_version, extra=None) → None.
InMemoryShadowLogger — 테스트 전용 in-memory 구현. records/count/clear
inspection helpers. Protocol 호환 (isinstance 통과).
PR-B 책임: app/services/policy_shadow_writer.py::DBShadowLogger(ShadowLogger)
구현 — analyze_events 에 INSERT. DB write 실패 시 WARN 로그만, 본 파이프라인
중단 금지 (shadow 기간 제품 영향 0).
plan: ~/.claude/plans/wise-gliding-hippo.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
check_4b_output_violations(text, subject_domain) → list[str]. Python re.search
기반 (Postgres regex 아님). forbidden_for_4b 에서 해당 subject 에 적용되는
rule 만 선택 후 detection_patterns 순회.
컴파일된 패턴 lru_cache 로 반복 호출 비용 감소. escalate_to_26b=False 인
event 에만 호출하여 policy_violation=true 기록 + under_escalation 재처리
후보로 포획.
plan: ~/.claude/plans/wise-gliding-hippo.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frozen dataclass with from_stage / escalation_reasons / risk_flags /
distilled_context / original_pointers / synthesis_directives / user_intent /
draft_hint. JSON round-trip (to_json/from_json). to_system_injection() 으로
26B system prompt 에 주입할 텍스트 블록 생성 (risk_flags + directives +
distilled_context 순).
from_stage 는 whitelist 검증 (triage/classify/summarize_short/advice_trigger/
night_sweep/ask_pre/unknown). tuple 타입 강제 (mutability 방지).
PR-B 의 escalation_service 가 이 계약을 사용. PR-A 는 계약만 정의.
plan: ~/.claude/plans/wise-gliding-hippo.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`/documents?category=law` 같은 URL 이 프론트에서 무시되던 버그 — `+page.svelte` 의 filter state 에 `category` 가 빠져 있어 API 호출 시 `?category=` 가 서버로 전달 안 됐음. 결과적으로 default 목록 (news/law 만 제외한 전체) 이 반환됐다.
Sidebar '법령 알림' 버튼 (e88640d) + API `category` 필터 (§§2A) 는 이미 반영됨 — 프론트 middleware 만 추가.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- migrations/152: ALTER TYPE doc_category ADD VALUE 'law' (DDL only; PG16 단일-트랜잭션 제약상 backfill 은 별도)
- models/document.py: Enum 에 'law' 추가 (7 활성 + 3 유보)
- workers/law_monitor.py: Document(..., category='law') — 신규 유입부터 세팅
- workers/classify_worker.py: source_channel='law_monitor' early-return + 최소 필드 (ai_domain='법령', ai_tags=['법령'], importance='medium'). AI classify skip — 법령 구조 고정/외부 source of truth/자동 재수집
- scripts/backfill_category.py: law 분기 + WHERE re-target ((source_channel='law_monitor' AND category='document')) + VERIFY cat_law/law_source_count + fail 조건
- api/documents.py: default 목록 제외에 law_monitor 추가 (news 와 동일 패턴)
- api/dashboard.py: documents count FILTER 에 law_monitor 제외 (category_counts.law 는 기존 GROUP BY category 로 자동 노출)
- frontend/Sidebar.svelte: '법령 알림' 버튼 ?source=law_monitor → ?category=law (explicit category 경로가 default exclusion 을 skip)
plan: ~/.claude/plans/stateless-churning-raccoon.md
axis 원칙: category=UI 축, policy/telemetry=source_channel+ai_domain 축 (feedback_category_vs_ai_domain_axis.md)
배포 순서: push → GPU pull → compose up --build fastapi frontend → backfill --dry-run → --apply.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
document-caddy 가 home-caddy 로부터 받은 X-Forwarded-Proto: https 를
신뢰하지 않고 incoming scheme (http) 로 덮어써 FastAPI 가 받은 proto 가
http 로 인식 → /api/documents 307 Location 헤더가 http:// 로 나가
HTTPS 페이지에서 mixed-content block.
private_ranges 를 trusted_proxies 로 설정해 docker bridge 내부의
home-caddy 가 전달한 X-Forwarded-* 를 보존.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- +layout.svelte 상단 nav 에 오디오/비디오 추가 (문서/자료실 옆,
카테고리 계열 그룹). Sidebar 는 §2 에서 추가했던 카테고리
블록 제거하고 기존 도메인 트리 전용으로 복구 — 상단 nav 와
중복되고, 사이드바가 카테고리 탐색 1차 진입점으로 적합하지
않다는 피드백 반영.
- app/Dockerfile uvicorn 에 --proxy-headers --forwarded-allow-ips=*
추가. FastAPI 의 trailing-slash 307 리다이렉트가 X-Forwarded-Proto
를 무시해 Location 헤더를 http:// 로 생성 → HTTPS 페이지에서
mixed-content block (/video 에서 목격). home-caddy → document-caddy
→ fastapi 체인에서 scheme 복구.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- accept-suggestion: documents.updated_at != expected stale 검사 제거.
classify_worker 가 source_updated_at 을 pre-commit 값으로 저장하는데
SQLAlchemy onupdate 가 commit 에서 updated_at 을 bump → 항상 불일치 →
승인 영구 불가. payload 교체 검사 하나만으로 core race 는 막힘.
사용자 직접 편집 감지는 별도 user_updated_at 컬럼 도입 시 재논의.
- docker-compose.yml: postgres/kordoc/fastapi/frontend 포트 127.0.0.1
바인딩. GPU 서버 로컬에만 있던 drift 를 main 으로 승격. UFW-Docker
우회 컨텍스트에서 불필요한 LAN 노출 축소.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
frontend +page.svelte:
- 4-card 메인 row 아래 새 row 추가: 자료실/오디오/비디오 (category_counts) +
자료실 제안 (library_pending_suggestions). 제안 ≥1 일 때 warning 색 + /library 링크.
- buildPipelineRows 가 pipeline_status (24h 누적) + queue_lag (현재 시점) 머지.
queue_lag.oldest_pending_age_sec 가 600초 초과면 stage 라벨 옆에 경과시간 표시.
- STAGE_ORDER/LABEL 에 stt/thumbnail 추가 (§3 신규 stage 자동 커버).
docs/categories.md (신규):
- 6 활성 + 3 유보 카테고리 정의 + 저장 경로 + 처리 파이프
- 역할 분리 원칙 (category / user_tags @library/ / facet_doctype / ai_suggestion)
- 업로드 경로 매트릭스 (web/NAS/collector/UI)
- video 채널별 정책 표 (web 거부 vs NAS quarantine)
- 업로드 한도 + error_code 7종 표
- orphan 임시파일 cleanup 정책
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stt:
- services/stt/server.py: lazy → eager preload in FastAPI lifespan.
STT_PRELOAD=0 으로 lazy 강제 가능 (개발/테스트). preload 실패해도
프로세스는 살아 있고 /ready false 로 남아 healthcheck 가 unhealthy 처리.
- docker-compose.yml: healthcheck /health → /ready. /health 는 단순
liveness 라 모델 미적재 상태도 healthy 로 잡혀 운영 신호 부적합.
queue ORM:
- app/models/queue.py: process_stage enum 에 'stt'/'thumbnail' 추가 +
create_type=False (migration 150/151 가 DB enum 확장 담당). 이게
없으면 stt_worker INSERT 시 SQLAlchemy 가 enum value 를 거부.
dashboard 강화 (§4 선제, §3 신규 stage 까지 자동 커버):
- app/api/dashboard.py: category_counts + library_pending_suggestions +
queue_lag (stage 별 pending/processing/failed + oldest_pending_age_sec).
- frontend/src/lib/stores/system.ts: QueueLag 타입 + DashboardSummary 확장.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
asyncpg prepared statement 는 single-command 만 지원 (core/database.py
exec_driver_sql 경로). §1 의 143_category.sql 이 4 statement (TYPE +
ALTER + INDEX×2) 였어서 fastapi 부팅 시 asyncpg.PostgresSyntaxError
"cannot insert multiple commands into a prepared statement" 로 실패
→ 컨테이너 restart 루프.
143 을 4 개 파일로 분리:
143: CREATE TYPE doc_category
144: ALTER TABLE documents ADD category / ai_suggestion
145: CREATE INDEX idx_documents_category
146: CREATE INDEX idx_documents_has_suggestion (partial)
DB 상태는 깨끗 (migration 143 이 부분 적용 안 됨 — asyncpg 가 batch
자체를 reject). schema_migrations 에 143 도 미기록이라 재실행 안전.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
plan: ~/.claude/plans/luminous-sprouting-hamster.md §2
- GET /api/documents/stats/category-counts — Sidebar/Dashboard 용
카테고리별 문서 건수 + library_pending_suggestions
- DocumentResponse 에 category / ai_suggestion 필드 노출 (§1 과 동일
수정, rebase 시 합쳐짐)
- SuggestionReview.svelte 신규 — ai_suggestion.proposed_category='library'
제안 카드 리스트. 단건 승인/반려 + 체크박스 대량 승인. 409 stale 시
warning toast + 자동 refetch
- /library 상단에 SuggestionReview 배치 (자료실 + 승인 대기함 겸).
승인/반려 후 tree/docs/facet 재조회
- Sidebar 재구성: 카테고리 내비(문서/자료실/뉴스/메모/검색) + 자료실
pending 배지. /api/documents/stats/category-counts 바인딩. audio/video
자리는 §3 주석 예약
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
이전 base image (pytorch/pytorch:2.5.1-cuda12.4) 가 surya-ocr 0.17.1 설치 시
torch 2.11.0 (PyPI CPU wheel) 로 업그레이드되지만 torchvision 0.20.1+cu124 는
유지돼 ABI 불일치 (torchvision::nms does not exist) → OCR 전체 실패.
native /opt/surya-ocr/venv 에서 검증된 조합으로 복제:
- python:3.12-slim base
- torch 2.11.0+cu126 / torchvision 0.26.0+cu126 (PyTorch cu126 index 고정)
- transformers 4.57.6 (5.x 는 surya detection.processor import 에서 실패)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- services/ocr/server.py: surya 0.17.x predictors 기반으로 재작성
(구 `from surya.ocr import run_ocr` 제거됨 → import error → 빈 텍스트 반환)
- NFC(DB 경로) vs NFD(NFS 파일시스템) 한글 정규화 mismatch 보정
- surya-ocr 버전 0.17.1 고정 (0.6~1.0 범위는 breaking change 노출)
- AIClient.ocr() NotImplementedError 제거 (호출처 0건, extract_worker 가
ocr-service HTTP 호출을 직접 사용)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
한도 400 → 600 자. baseline 관찰(partial avg 168자 / full 10%)에서
길이 제약이 실제 출력 제약이 되는 현상 확인, 절차·비교 카테고리
답변 깊이 확보 목적.
변경 4 라인:
- search_synthesis.txt:17 answer 400→600 characters max
- prompt_versions.py:20 v1-400char → v2-600char (telemetry)
- synthesis_service.py:42 PROMPT_VERSION v1→v2 (cache key 의미론 동기화)
- synthesis_service.py:46 MAX_ANSWER_CHARS 400→600 (hard clip 동기화)
v1 post-tier0 baseline: 225 rows, partial 51% / insufficient 49% / full 0%
(Tier 0 fix 로 full+refused=True 모순 0 건). E.6 는 이 clean baseline 을
compare-against 로 사용.
향후 티켓: PROMPT_VERSION 과 ASK_PROMPT_VERSION 단일 소스 통합.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
배경: Phase 3.5 fix2 로 서버 /ask 는 X-Source=eval 을 받아들이려면
X-Eval-Token 이 EVAL_RUNNER_TOKEN 와 일치해야 함. runner 에 해당 헤더
주입 경로가 없어 eval 호출이 전부 source='document_server' 로 강등됐음.
변경:
- call_ask / call_analyze: eval_token, eval_case_id 인자 추가. 조건부 헤더 주입
- run_eval: eval_token 파라미터 추가
- CLI: --eval-token 플래그 추가 (env EVAL_RUNNER_TOKEN 자동 fallback)
- main(): --source=eval + --eval-token 미지정 조합에 warning 출력
- eval_case_id 는 item id 자동 전달 → ask_events.eval_case_id join 키로 활용
E.6 재측정의 source='eval' 정확 기록 선결 조건.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
httpx 의 h11 레이어가 Content-Length 와 body 길이 불일치를 client-side 에서
LocalProtocolError 로 거절해서, CL 헤더만 override 해 서버 pre-check 경로를
외부에서 격리 테스트하는 것이 불가능했음. 대신 body 자체가 slack 임계치를
초과하는 케이스로 변경 — multipart CL 이 자동으로 `max_bytes * slack_ratio`
를 넘어 서버 pre-check 가 먼저 catch 함.
또한 기존 case 7 (CL 위조) 는 같은 이유로 실현 불가능해 제거. 5 케이스에서
6 케이스로 조정.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase B 의 스트리밍 size 검증을 외부에서 확인할 수 있는 스크립트.
pytest 인프라가 Phase 0 상태이므로 full test harness 구축을 미루고,
`scripts/verify_upload_size.py` 단일 파일로 경계 케이스를 즉시 회귀 검증.
7 케이스:
- 0 bytes → 400 (정책)
- 1 byte → 201 (happy path)
- max_bytes - 1 → 201 (경계 하)
- max_bytes 정확 → 201 (경계 상)
- max_bytes + 1 → 413 (스트리밍 차단)
- CL slack 초과 (override 헤더) → 413 (사전 차단)
- CL 위조 (작은 헤더 + 큰 body) → best-effort (서버 거절 status 수용)
`/api/config/public` 에서 max_bytes 를 동적 획득. slack_ratio 는 비공개라
스크립트 상수로 1.05 하드코딩 (config.yaml 과 동기화 유지 주석 명시).
Cleanup: 파일명 prefix `__upload_boundary_test__` + ns timestamp 로
실데이터와 격리. 시작 시 pre-cleanup + 각 케이스 직후 + finally 블록 cleanup.
`docker compose exec fastapi python /app/scripts/verify_upload_size.py` 로 실행.
UPLOAD_TEST_TOKEN + DATABASE_URL 환경 변수 필요. scripts/ 는 이미 read-only
volume 으로 마운트돼 있어 배포·재빌드 불필요.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
asyncpg 이 TIMESTAMPTZ 파라미터에 문자열 대신 datetime 객체를 요구
(DataError: invalid input, expected datetime instance, got str).
argparse type=datetime.fromisoformat 로 CLI 단계에서 파싱.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
asyncpg 이 $N IS NULL 비교에서 Python None 의 타입 추론 실패
(AmbiguousParameterError: could not determine data type of parameter).
None 인 조건은 WHERE 에서 아예 제외 — clauses 동적 조립.
부수 효과: 조건 0개일 때 "TRUE" 반환으로 quiet fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SQLAlchemy text() 의 `:name` 파라미터가 PostgreSQL `::type` cast 와
토큰 경계 충돌로 치환되지 않아 `syntax error at or near ":"` 발생.
`:since::timestamptz` → `CAST(:since AS TIMESTAMPTZ)` 로 변경.
Reproduction: --since/--until 옵션 사용 시 모든 집계 쿼리 실패.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
migration 142 ALTER COLUMN source SET NOT NULL 자동 적용 방지.
_run_migrations 의 glob('*.sql') 비재귀 → _deferred/ 무시.
활성화 절차 (D7 참조):
- 138~141 적용 + 7일 운영 후 SELECT COUNT(*) FROM ask_events
WHERE source IS NULL AND created_at > <deploy> = 0 확인
- git mv migrations/_deferred/142_*.sql migrations/142_*.sql
- docker compose restart fastapi (init_db 가 자동 적용)
이유: 새 코드의 source 누락 가능성 empirical 검증 후 lock.
NOT NULL 적용 후 NULL INSERT 시도 시 ask_events 기록 실패 (data loss).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Phase 3.5 calibration runner (scripts/run_eval_ask.py, calibrate_ask.py)
가 생성하는 jsonl/log/csv 를 repo 에서 제외. reports/ 는 이미 tracked
파일 있어서 전체 ignore 하지 않음.
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>
프론트의 `MAX_UPLOAD_BYTES = 100 * 1000 * 1000` 하드코딩 상수를 제거하고
서버 `GET /api/config/public` 응답을 단일 진실 공급원으로 사용.
pre-check 자체는 그대로 유지 (UX 개선 — 대용량 파일을 edge proxy 까지
올리기 전 클라이언트에서 즉시 차단). 값의 출처만 서버로 이동.
변경:
- frontend/src/lib/stores/config.ts 신규 — publicConfig readable store
* 첫 구독 시 `/config/public` 1회 fetch
* fetch 실패 시 fallback 100MB 유지 (서버 enforcement 가 본선이라 안전)
- +layout.svelte onMount 에서 prewarm refresh() 호출
- UploadDropzone.svelte 에서 `$derived` 로 store 값을 반응형 구독
* `maxBytes` / `maxBytesLabel` 을 파생
* 에러 토스트 문구도 동적 라벨 사용 (`100MB` 하드코딩 제거)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>