3일 telemetry (599 triage / 555 deep) 기반 임계치 재평가:
1. 에스컬레이션 비율 — 임계치 의미 reframe
- 기존: >20% 적색 (튜닝 필요) → 항상 적색 (운영 패턴 97%)
- 신규: <80% 적색 (정책 매칭 실패 증가)
- 메시지: "safety 정책상 95~100% 가 정상" 보조 표시
- safety_reference 99.7%, generic 100% (fallback risk_flag), msds 46.2%
→ 운영 정상 패턴 확인
2. Deep summary 안정성 — 신규 카드 추가
- mode='summary_deep' 의 error_code IS NOT NULL 비율
- 현재 5.2% (call_failed 21 + parse:ValidationError 8)
- >5% 적색 임계
- MLX 호출 timeout / JSON 파싱 실패 모니터
3. triage JSON 건강도, Backlog Suppression — 임계치 유지
- 현재 0%, 1% — 매우 안정. 보수적 임계 유효.
Backend: TierHealthStack 에 deep_total / deep_err_total 추가
Frontend: 카드 그리드 3열 → 4열 (lg), Day 4 신규 카드.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
본문에 `- [x]` 로 직접 입력된 체크 항목도 checked_at 가 기록되어 10초 후
자동 숨김 대상이 되도록 create_memo / update_memo 에 sync 로직 추가.
- _sync_task_state_with_content: - [x] 에 checked_at 없으면 현재 시각으로 기록,
- [ ] 또는 사라진 index 는 state 에서 정리
- scripts/backfill_memo_task_state.py: 배포 이전 기존 노트에 현재 시각 backfill
(docker compose exec fastapi python /app/scripts/backfill_memo_task_state.py --apply)
체크박스 체크 후 10초 경과 항목을 대시보드 핀 메모 / /memos 에서
자동 숨김, 메모 푸터 "완료 N개 보기" 버튼으로 토글.
- migration 161: documents.memo_task_state JSONB — {"<idx>":{"checked_at":"ISO"}}
- PATCH /memos/{id}/tasks/{task_index} 전용 엔드포인트:
· SELECT FOR UPDATE 로 동시 토글 race 차단
· task_index drift 시 stale state 자동 정리 (400 대신 200)
· AI 재처리/큐 enqueue 의도적 스킵 + memo_task_toggle_skip_ai 로그
- renderMemoHtml(taskStates, now) → 경과 항목에 memo-task-hidden 클래스
- Svelte 5 $effect cleanup 으로 setInterval 누수 방지
체크박스 토글 같은 {content}-only PATCH 에서 body.title==None 을 무조건
_auto_title(content)로 재생성해 제목이 체크박스 라인으로 덮어씌워지는 버그.
Pydantic model_fields_set 으로 title 전송 여부를 구분해 PATCH semantics 정상화.
- 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>
- 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>
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>
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>
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>
기존 `await file.read()` 는 임의 크기 파일을 메모리에 전부 적재한 후 저장해
디스크 고갈 / OOM 공격 벡터 였음. Caddy/home-caddy 프록시 한도에만 의존했고
FastAPI 측 policy enforcement 가 전무했음. 이 커밋으로 서버가 authoritative
으로 강제 집행.
변경:
- `Request` DI 추가 → Content-Length 사전 차단 (max_bytes * slack_ratio 초과 시 413)
- `await file.read()` → 청크 루프 스트리밍 (stream_chunk_bytes 단위)
- 누적 size > max_bytes 시 스트리밍 중 413 (Content-Length 위조 방어)
- 0바이트 파일 → 400 reject (정책: 유의미한 문서 ingest 대상 아님)
- 파일 저장 완료 + close 이후 에만 file_hash 및 DB 레코드 생성
- Document 레코드 와 processing_queue 는 단일 트랜잭션으로 묶고,
DB 예외 시 session rollback + partial file unlink 로 원자적 정리
- 예외 시 `except Exception` 으로 cleanup (BaseException 계열은 의도적으로 패스)
설정 값: config.yaml `upload.{max_bytes, content_length_slack_ratio, stream_chunk_bytes}`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
업로드 크기 한도를 프론트 하드코딩이 아닌 서버 config 의 단일 진실 공급원
으로 이동. 프론트는 Phase B 후속 커밋에서 이 값을 읽어 pre-check UX 에 사용.
- config.yaml 에 `upload` 블록 추가:
* max_bytes (authoritative policy)
* content_length_slack_ratio (multipart 오버헤드 여유)
* stream_chunk_bytes (스트리밍 IO 단위)
- app/core/config.py 에 UploadConfig pydantic 모델 + Settings.upload 필드
- app/api/config.py 신규 — GET /api/config/public 엔드포인트
* 민감정보 없는 프론트 필수 설정만 노출
* 범용 서버 설정 공개 창구로 확대 금지 (docstring 명시)
- /api/config 를 setup redirect bypass 에 추가 (초기 setup 전에도 조회 가능)
이 커밋 자체는 기존 upload 동작에 영향 없음. 후속 커밋에서 enforcement +
프론트 구독을 연결.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
E.3 400→600자 튜닝 전후 비교 + 단계 5 failure mode 분석의 기준 필드 추가.
- migrations/135: answer_length/covered_aspects/missing_aspects/model_name/prompt_version 컬럼 + prompt_version 인덱스
- ORM: ask_event.py에 동일 5개 필드 매핑
- prompt_versions.py: ASK_PROMPT_VERSION="search_synthesis.v1-400char" 상수 + resolve_primary_model() helper
- search_telemetry.record_ask_event: 시그니처에 keyword-only 필드 5개 추가 (하위 호환)
- search.py: refused + success 두 호출사이트에서 새 필드 전달. answer_length는 len(sr.answer or ""), model_name/prompt_version은 상수 모듈 기반
기존 호출 구조(이미 search_telemetry+background_tasks로 DB insert 중)는 유지. 순수 확장 커밋.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
FileInfoView에 회사/주제/연도/문서유형 select 4개 추가.
facet 옵션은 /api/library/facets에서 로드, 세션 캐시.
업로드 엔드포인트에 facet Form 파라미터 4개 추가.
업로드 시 현재 선택 facet 자동 전달 + 미리보기 텍스트.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
library_categories 테이블 추가로 빈 카테고리 생성 가능.
CRUD API (생성/leaf rename/leaf delete) + 트리 머지 엔드포인트.
사이드바 트리에 컨텍스트 메뉴 (추가/이름변경/삭제).
LibraryPathEditor를 카테고리 기반 flat selector로 전환.
미분류는 시스템 분류로 보호 (삭제/이름변경 불가).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
기존 UNIQUE(document_id, stage, status)는 pending+processing 동시 존재를
허용해서 stale 복구 시 충돌 발생. 2-layer 방어로 근본 차단:
1) DB: partial unique index uq_queue_active — 활성 행(pending/processing)은
(document_id, stage)당 최대 1개만 허용
2) App: enqueue_stage() 중앙 함수 — INSERT ON CONFLICT DO NOTHING으로
모든 9개 경로의 check-then-insert TOCTOU race 제거
migration 117은 guard check 포함 — 활성 중복이 남아있으면 RAISE EXCEPTION
으로 중단, 수동 정리 유도.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
폴더 미선택 상태에서 업로드하면 doc_purpose='business'만 설정되고
@library/ 태그가 빠져서 자료실에 문서가 표시되지 않던 버그 수정.
백엔드: business 업로드에 library_path 없으면 @library/미분류 자동 태깅.
프론트: activePath 없을 때 기본값 '미분류' 전송.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
메모 입력/편집:
- 선택적 제목 토글 (기본 숨김, "제목" 버튼으로 활성화)
- 툴바 버튼: 체크리스트/굵게/제목 (모바일에서 마크다운 수동 입력 불필요)
- 편집 모드에도 동일 툴바
대시보드 핀 메모:
- 클릭 시 /memos 이동 대신 인라인 펼침/접힘 (details)
- 제목이 있으면 제목 표시, 없으면 첫 줄
- 펼치면 마크다운 렌더링된 본문 + "메모함에서 보기" 링크
Backend:
- MemoCreate/MemoUpdate에 선택적 title 파라미터 복원
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
대시보드를 통계판에서 상황판으로 전환:
- 헤더 + 시스템 상태 인라인 (비클릭)
- 핀 메모 최상단 조건부 (컴팩트 띠, 최대 3개)
- 카드 4개 (문서함/메모/뉴스/승인대기) 모바일 2×2
- 최근 활동 전체 너비 7건, 2줄 스캔형 + 법령 배지
- 파이프라인 details 접힘 (실패 시 자동 open)
- 제거: 도메인 분포, 법령/시스템 별도 카드, 8:4 분할
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
전체 문서 1개 카드를 6개로 분리: 문서함, 메모, 뉴스, 승인대기,
법령알림, 시스템. 단일 FILTER 쿼리로 효율적 카운트.
각 카드 클릭 시 해당 페이지로 이동.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Inbox 가 review_status=pending 서버 필터로 받는데 pending 이 291 건 이라
page_size 100 으론 191 건 누락. inbox 는 작업 큐 성격이라 한 번에 보는 게 UX.
500 으로 상향: data 폭발 없음(filter 로 boundedness 보장), latency 영향 미미.
전략적 임시 — Phase 4.5 UI 작업에서 inbox 에 infinite scroll 또는 pagination
추가하면 le=100 으로 다시 내려도 됨.
Inbox 페이지가 /documents/?page_size=200 를 호출하는데 백엔드 Query 가
le=100 이라 422 발생 — Phase 2 첫 commit(2026-04-02)부터 dormant 버그.
inbox 코드 안에 'TODO(backend): review_status filter 지원 시 page_size 축소'
주석이 있던 상태.
backend:
- list_documents 에 review_status: str | None Query 파라미터 추가
- WHERE 절에 review_status 매칭 분기 추가
frontend:
- /documents/?review_status=pending&page_size=100 으로 변경
- 클라이언트 필터링 코드 제거 (서버 필터로 대체)
100 미만 안전. pending 이 100 넘으면 다음 페이지 로직 추가 필요 (별도 작업).