fastapi 컨테이너는 WORKDIR=/app, 코드가 직접 풀려있고 app/ 디렉토리 없음.
backfill_category.py 의 ../app 패턴은 컨테이너 안에서 /app/app (없음)
가 되어 ModuleNotFoundError. 스크립트 자기 디렉토리의 .. 를 sys.path 에
넣어 /app 루트 노출.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30건 한정 stratified pilot. baseline markdown 품질 측정 후 Phase 2 전체
백필 결정. 영구 worker 경로 아님.
대상 WHERE:
deleted_at IS NULL
AND file_format='pdf'
AND md_status='pending'
AND category='document'
AND document_type NOT IN SKIP_DOC_TYPES (marker_worker 와 일관)
Stratification:
ai_domain × file_size_bucket (small<500KB / medium<5MB / large)
documents 에 page_count 컬럼 부재 (marker_worker 가 PyMuPDF 로 동적
측정) → file_size 를 길이 proxy 로 사용.
cell 안에서 file_size 작은/큰 mix 로 짧은/긴 문서 차이 관찰.
Subcommands:
select — 30건 dry-run + JSON 저장 (/tmp/phase1d_pilot.json)
enqueue — markdown 큐 enqueue (uq_queue_active 충돌 시 skip)
report — md_status / 평균 elapsed / 실패 top5 / heading anchor 후보 /
KaTeX 후보 / file_size bucket 별 success 비율 / UI 검수 URL
리포트 메모:
markdown_image_count 는 현재 server.py 가 _images 버림 → 0 정상.
Phase 1B.5 에서 _images 출력 시 자동 활성.
실행:
docker compose exec fastapi python /app/scripts/phase1d_pilot.py select
docker compose exec fastapi python /app/scripts/phase1d_pilot.py enqueue --yes
docker compose exec fastapi python /app/scripts/phase1d_pilot.py report
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
문제: 보기/해설 본문의 \$\$ ... \$\$ block math 가 앞뒤 빈 줄 없으면
마크다운 파서가 라벨/텍스트와 같은 단락에 묶어 KaTeX 렌더 실패 → raw 표시.
운영 결과 (21회분 = 2,100문항):
- HC-5 detect 317건 모두 자동 fix 완료. 모든 회차 재검사 0건.
- 추가 fix: q1579 (2023년 1회 q81) 바이메탈 ASCII 다이어그램 fence wrap.
알고리즘:
- 자체 줄 \$\$...\$\$ (한 줄 안 시작·종료, 길이 4+) detect.
- 앞·뒤 라인이 비어있지 않으면 빈 줄 삽입 — idempotent.
- inline \$ ... \$ 영향 없음.
- 의미 변경 0 (빈 줄 삽입만, 본문 텍스트/수식 보존).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
운영 중 발견한 패턴 추가:
- 보기 형식: "1번:" + "1." + "1)" 모두 매칭 (2022년 회차에서 "1." 사용 발견).
- subject 정규화: 괄호 형태(연소공학 (열역학))뿐 아니라 슬래시 형태
(가스안전관리 / 가스설비) 도 head + scope 분리.
운영 결과 (6회분 = 600문항 추가):
- 2020년 3회 / 2021년 1·2·3회 / 2022년 1·2회 모두 등록 완료.
- 이미지 27건 자동 첨부 (1+4+7+6+5+4).
- audit: HC 0건, LC-5 2건 (2022년 2회 q41/q90 표 구분자 누락) 자동 fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AI 응답이 마크다운 자체를 \`\`\` 으로 감싸서 오는 패턴 (시작만 있고 닫음 누락 포함)
때문에 explanation/AI 해설 영역이 raw 코드블록으로 보이는 회귀.
- frontend/lib/utils/mathMarkdown.ts: stripOuterFence helper.
- terminated wrap 처리 (inner 에 \`\`\` 추가 있으면 보존)
- unterminated 처리 (백틱 그룹 == 1 인 경우만 안전하게 unwrap)
- 본문 중간 정상 코드블록은 보존
- scripts/strip_outer_fences.py: dry-run + --apply 양 모드.
- 5개 필드 (question_text, choice_1~4, explanation, ai_explanation, content) 검사.
- 운영 결과 explanation 34건 unwrap 적용 완료, recount 0 검증.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
3일 운영 결과 doc 4811, 5181 가 extracted_text='' (빈 문자열) 인데
IS NOT NULL 만 걸려 enqueue → classify_worker 의 not doc.extracted_text
truthy 체크에서 ValueError → max_attempts(3) 도달 → status=failed.
다음 backfill 사이클에서 다시 enqueue 되어 12회 반복, failed 24건 누적.
수정: tier_backfill.py + backfill_tier.py 양쪽 SQL 에
LENGTH(extracted_text) > 0 추가. 빈 문자열 문서는 enqueue 자체에서 제외.
기존 failed 24건 정리 SQL (사용자가 수동 실행):
DELETE FROM processing_queue
WHERE stage='classify' AND status='failed'
AND error_message LIKE '%extracted_text%';
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)
PR-B B-1 배포 이전에 classify 된 6770건 레거시 문서에 대해 ai_tldr /
ai_bullets / ai_detail_summary 등 tier 산출물을 채우기 위한 백필 도구.
사용:
docker exec hyungi_document_server-fastapi-1 \
python /app/scripts/backfill_tier.py --domain safety --limit 50 --dry-run
docker exec hyungi_document_server-fastapi-1 \
python /app/scripts/backfill_tier.py --domain safety --limit 50 --apply
도메인 필터: safety / law / manual / news / drive_sync / memo
ORDER BY created_at DESC 로 최신 우선. ON CONFLICT DO NOTHING 이라
기존 pending/processing 행 있으면 중복 enqueue 방지.
MLX 26B 단일 Semaphore 경로라 처리 속도 ~1건/분. 50건 ≈ 1시간.
대량 백필은 야간 분할 권장. 이번 세션 Industrial_Safety 50건이
첫 smoke 대상.
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>
배경: 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>
- Implement kordoc /parse endpoint (HWP/HWPX/PDF via kordoc lib,
text files direct read, images flagged for OCR)
- Add queue consumer with APScheduler (1min interval, stage chaining
extract→classify→embed, stale item recovery, retry logic)
- Add extract worker (kordoc HTTP call + direct text read)
- Add classify worker (Qwen3.5 AI classification with think-tag
stripping and robust JSON extraction from AI responses)
- Add embed worker (GPU server nomic-embed-text, graceful failure)
- Add DEVONthink migration script with folder mapping for 16 DBs,
dry-run mode, batch commits, and idempotent file_path UNIQUE
- Enhance ai/client.py with strip_thinking() and parse_json_response()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add users table to migration, User ORM model
- Implement JWT+TOTP auth API (login, refresh, me, change-password)
- Add first-run setup wizard with rate-limited admin creation,
TOTP QR enrollment (secret saved only after verification), and
NAS path verification — served as Jinja2 single-page HTML
- Add setup redirect middleware (bypasses /health, /docs, /openapi.json)
- Mount config.yaml, scripts, logs volumes in docker-compose
- Route API vs frontend traffic in Caddyfile
- Include admin seed script as CLI fallback
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>