코드리뷰 AIClient 정비 PR-B (#2 gate·#3 httpx·#4 public).
#2 gate 구조 (call-site 컨벤션 — gate 는 caller-managed, AIClient self-gate 금지):
· classify_worker consumer call_triage: gate 없이 Mac mini 직타하던 것 → acquire_mlx_gate(BACKGROUND).
(drain 경로 call_deep_or_defer 는 맥북 deep 슬롯이라 mini gate 무관, 미적용.)
· verifier_service: gate 없이 _request(verifier) 하던 것 → acquire_mlx_gate(FOREGROUND) + call_verifier.
classifier/evidence 와 동일 gate 공유로 thundering-herd(22-timeout 사고) 방어.
★재진입 안전 검증: AIClient 메서드 내부 self-gate 0(전부 call-site) + evidence/classifier 는 이미
독립 gate 보유 + api/search 오케스트레이터 gate 미보유 → double-acquire 데드락 불가.
#4 public 메서드: call_classifier/call_verifier 추가 → classifier/verifier_service 의 private _request
직접호출 봉인(egress 가드 일관 적용). gate 는 caller-managed 유지(call_primary 와 동일 계약).
#3 공유 httpx: 호출마다 AsyncClient 생성(30+ 사이트)을 _get_shared_http() 단일 풀로 — keep-alive
재사용. 이벤트루프 바인딩이라 루프 변경(테스트) 시 재생성, close() 는 no-op.
py_compile PASS. (잔여 #4: query_analyzer/digest/backends 의 _request·_call_chat 직접호출은 gated 라
안전, 후속 sweep.)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
app 은 라우터 경유로 전 모델을 import 하지만 standalone 백필 스크립트는 부분만 import →
SQLAlchemy mapper 의 string 관계(StudyTopic.sessions->StudySession 등) 해소 실패로
InvalidRequestError. pkgutil 로 models/* 전 모듈 import 해 레지스트리 완성(전부 컨테이너서
import 가능 = app 기동 시 로드되는 것과 동일). 백필 3종 실행 검증: topics 1·cards 65·progress 22 적재.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
검수완료(needs_review=false)·미삭제 study_memo_card 만 발행(kind=study_card,
뷰어 pubstudy.ts getCards 계약 일치). plan study-viewer-port S-2.
- projection: KIND_CARD + project_card(format·cue·fact·cloze_text·source_question_id·source_generated_at).
- enqueue: enqueue_card_publish = 카드 상태 기반 publish/tombstone 단일화(경로별 가드
기억 회피) + backfill_publish_cards.
- 저작훅(study_publish_enabled 게이트): approve-batch(검수완료→발행)·update(수정=재투영/
검수대기복귀=tombstone)·delete(tombstone).
- 발행자격 상실 경로 tombstone(viewer stale 잔류 0): 워커 supersede(재추출 retire)·
flag_cards_for_source(소스문제 정정/삭제). 두 fn 은 '발행 중이던'(needs_review=false) id
만 선캡처 반환 → 미발행 카드 스푸리어스 tombstone 회피.
- scripts/backfill_publish_cards.py.
py_compile PASS · project_card payload 단위검증(getCards 계약 일치). 워커·/published/feed
kind-generic 무변경. flag on 환경 배포 시 주제처럼 카드 발행 시작.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
주제(study_topic) 메타를 발행 레이어에 실어 viewer 가 주제/회차 단위 퀴즈를
구성하게 한다(현재 topic 이름 미발행이라 불가). plan study-viewer-port S-1.
- publish_projection: KIND_TOPIC + project_topic(topic_id·name·exam_round_size).
회차는 미발행 = viewer 가 pub_content(study_question) 의 exam_name/exam_round 로
파생(추가 발행 불요). topic_id = project_question.topic_id 와 동일 DS 식별자라
viewer 문항→주제 상관 키(pub_id 는 opaque 라 상관 키 아님).
- publish_enqueue: enqueue_topic_publish + backfill_publish_topics(bounded page,
deleted_at IS NULL). 멱등 = 워커 (payload_hash, deleted) 디둡.
- study_topics 저작훅(전부 study_publish_enabled 게이트): create(flush→enqueue→
commit) / update(재투영, payload 무변경은 디둡이 rev 안 올림=churn 0) /
delete(tombstone, raw DELETE 금지·워커 경유).
- scripts/backfill_publish_topics.py: 기존 주제 1회 outbox 적재(overflow 가드).
워커·/published/feed 는 kind-generic(무변경, 실측). flag on 환경 배포 시 주제 발행
시작 → S-3 viewer 수용(generic upsert·kind-filtered read) 선행 전제, 게이트 PASS 됨.
백필 실행·배포순서 cutover 는 deploy 게이트(소프트락)라 본 슬라이스 미포함.
py_compile PASS · project_topic payload 단위검증.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
뷰어가 published 테이블을 rev 커서로 incremental pull 하는 read-only feed.
- GET /published/feed?since={rev}&kind=&limit= → rev>since ORDER BY rev ASC LIMIT(cap 500)
- Bearer(viewer_sync_token) default-deny + 상수시간 비교(internal_study 패턴 재사용)
- 엔벨로프 schema_version + items[pub_id·kind·source_id·rev·deleted·schema_version·payload]
+ next_since·has_more. tombstone(deleted=true) 1급 이벤트 포함.
- viewer_sync_token = Mac mini internal_worker_token 과 분리(폭발반경 격리), 기본 ""=default-deny.
rev 커서 안전 = 워커 단일 라이터(advisory lock) 배치 원자 커밋. 배포는 P0 seam
(P0-3 뷰어 pull-sync) 완성 후 일괄 게이트. read API = additive.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
study_question 발행 outbox enqueue 를 settings.study_publish_enabled 게이트로
5경로 결선(전부 같은 tx, caller commit = 콘텐츠 변경과 outbox INSERT 원자성):
- create_question_in_topic: 신규 문항 발행
- update_question: 문항 재투영(해설 ready 일 때만 동봉)
- soft_delete_question: tombstone(문항 + 해설 본문 존재 시 해설 kind)
- run_explanation_job (4-A 워커): 해설 ready → 문항+해설 발행
- generate_ai_explanation (실시간): 해설 ready → 문항+해설 발행
플래그 기본 false = 코드 inert(배포 후 GPU .env STUDY_PUBLISH_ENABLED 로 점등).
stale→tombstone 은 P1-3(해설 라이프사이클)로 분리.
검증: py_compile 6파일·결선 5곳 grep·플래그 기본 false.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
라이브 pkm DB 가 scan-feature-build 의 365(scan_jobs)·366(pending_command)
을 이미 apply + schema_migrations 스탬프함. 발행 마이그가 365 부터면 러너가
365/366 을 적용필로 보고 스킵 → published 테이블 미생성 → 367 깨짐.
다음 free=367 로 +2 시프트해 회피. 파일 rename + 헤더 주석 + published.py
모델 주석(mig 번호) 동기화. 내용 무변경(멱등 CREATE ... IF NOT EXISTS).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
C2 csb_collector: 주간 run 의 per-URL 루프에 try/except/continue — URL 1건 실패(page-extract
예외·DB DataError)가 run() 밖으로 전파돼 이후 URL 전부 스킵+watermark 정지하던 것 차단. 각
iteration 자체 session 이라 실패 격리.
H3 news_collector: 공유 세션+종단 단일 commit → 한 소스 DB오류가 오염시켜 전 소스 insert 소실하던
구조를 소스별 독립 세션으로(csb 패턴 동형). 실패 시 rollback 후 깨끗한 상태에서 failure 기록.
실증: 수동 수집서 Taipei Times ReadTimeout 격리하고 327건 정상 완주.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
호스트 30GB 빠듯(여유 <1GB·스왑 full)에서 mineru VLM 스파이크가 글로벌 OOM 유발 시 커널이
가해자 대신 postgres(prod DB)/fastapi(앱+스케줄러 SPOF)를 reap 하던 비대칭 제거. tier-0 = -900(보호),
mineru = 16g cap(steady ~12GB)로 봉쇄. mineru 는 docker update 로 live 선적용.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
H1 marker_worker: PDF arm + split arm 에 빈 md_content 가드(office arm 동형 raise → queue 재시도 후
failed). 빈 추출(스캔/이미지 PDF)을 md_status=success+빈 md 로 박제하던 불변식 위반 제거.
H2 summarize_worker: 빈/think-only 요약을 ai_summary= 로 박제(completed 마크)하던 것 raise 로 가시화
+ briefing/digest loader 에 length(ai_summary)>0 방어(기존 누출 행도 배제).
H4 main.py: AsyncIOScheduler job_defaults misfire_grace_time 1s→45s — 단일 루프 1초 혼잡에 1분 컨슈머
틱이 run time missed 로 침묵 스킵하던 것 차단(coalesce 유지).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
study explanation/session-analysis/memo-card 워커 + study_questions/study_topics(subject-note·diagnosis)
+ documents.analyze 의 하드코딩 30~60s asyncio.timeout 7곳 제거. 빠른 Gemma 기준 리터럴이 Qwen 27B
교체(2026-06-11) sweep 누락 → 느린 콜을 잘라 사용자 대면 504 + 워커가 매 재시도마다 느린 콜 재실행해
문서가 큐에서 영영 못 빠지는 liveness halt. digest_llm_timeout_s 와 동형으로 config.pipeline.llm_call_timeout_s(300)
단일소스화. 다음 모델 교체 때 재발 차단.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
interval hours=6 는 컨테이너 시작시각 앵커라 재시작마다 드리프트 →
새벽 수집이 브리핑 윈도우(00:00~05:00 KST) 밖(05시대)으로 밀려 6/19·6/20 briefing
status=empty(기사 0). cron 고정으로 00:00 수집 보장 + 05:10 브리핑까지 ~5h 가공 lead time 확보.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
문제: /clause 결과 클릭이 문서 첫 화면으로만 가고 해당 절로 안 감.
수정 2곳:
- /clause → /documents/{id}?section={chunk_id} 로 이동.
- 읽기뷰 defaultSelId 가 ?section=<chunk_id>(outline 에 존재 시)를 우선 선택 → 그 절 표시.
- 컨테이너 절(is_leaf=false 비-split, outline 부재: UG-136/UHX-13 등 26개 핵심절)은
clause-lookup 이 문서순서상 첫 딥링크 가능 자손으로 점프 타깃 해소(검색은 그대로 찾되
클릭은 그 절 내용으로). 26개 전부 해소 검증.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ASME 절 식별자(UG-79 등) 입력 → /api/documents/clause-lookup → 문서·위치 결과 →
읽기뷰 이동. 절은 in_corpus=false(의미검색 비활성)라 이 정확지목 진입점이 유일 경로.
사이드바(자료실 옆 'Hash 절 바로가기')로 노출. 신규 라우트라 기존 표면 미접촉.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A-1: _detect_heading 의 ATX 분기가 절번호 식별자(UG-79/PG-27.4.1/UW-11/A-69 등,
[A-Z]{1,4}-\d+(\.\d+)*)를 node_type='clause' 로 분류(과거 ATX=무조건 None).
ASME clause=0 사각지대의 근본 원인 — 절은 이미 ATX heading 으로 탐지되나 'clause'
타이핑이 한국 제N조 전용이었음(5180 Sec I = clause 0, heading_path 1637 = window/None).
C-4: _clean_label 로 marker LaTeX/markdown/페이지번호 아티팩트
('$\textbf{PG-20.1 ...}', '(25) **A-69**')를 패턴 매칭 전 정제 — 없으면 노이즈에
막혀 매칭 0. 표시 라벨도 동시 정제. 한국 법령/일반 ATX 엔 inert(무회귀).
A-2: 큰 절(>LEAF_HARD_MAX)은 기존 window-split 이 'clause'→'clause_split'
(char_start 점프 타깃 보존)로 자동 처리 — 추가 코드 없음.
검증(순수함수, DB/GPU/재마크다운 0): test_asme_clause 6/6 신규 + test_eng_matcher 4/4
(PG-1 계약을 clause 로 갱신) + test_builder_char_start 7/7(char_start 무영향).
DS 적용(V-0 스모크 → 기존 md V-1 0-cost 검증)은 후속.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
ToC 없는/게이트 미달 대형 PDF(>=60p)에 한해 off-card Qwen(맥북, call_deep_or_defer,
StageDeferred-safe) 경계 제안 → 동일 검증게이트(_is_clear_bundle) 통과 시에만 deterministic 과
공유하는 _create_children 로 분할. is_bundle=false/파싱·검증 실패=단일문서(오늘과 동일)+로깅.
- env PRESEGMENT_LLM_FALLBACK 기본 false → 배포 동작 무변(LLM 미호출, 검증=unit test)
- 자식생성 _create_children 공유 헬퍼로 리팩터(deterministic+LLM 단일 경로, 동작 동일)
- SegmentationOutput Pydantic + parse_json_response(house 패턴) + per-page heading 샘플(본문 미전송)
- prompt app/prompts/presegment_boundaries.txt + tests/test_presegment_llm.py(14, fitz/DB/LLM mock)
no direct HTTP·no silent fallback. 활성=flag ON + 실 router fixture 검증 후.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
합성 번들 e2e PASS(자식 3개 합성 file_path·range, uq 위반 0 + 자식 extract range-clamp 1110자
range_ok) 후 인제스트 presegment 재활성(documents.py upload + file_watcher 3곳). 非PDF/단일=통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
mineru-service profile-gate 해제(상시 기동) + fastapi depends_on 추가 +
MARKER_ENDPOINT 을 mineru-service:3301 로 flip. marker-service 는 롤백 대비
Phase 2 까지 잔존(depends_on 유지, 호출만 안 됨 → idle-unload). 동일 /convert 계약.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
rerank_service.py 가 후보를 MAX_RERANK_INPUT=200 까지 청크 없이 한 번에 TEI 로 POST → TEI 한도 64 초과(85) 시 HTTPError → RRF silent fallback(리랭크 누락=검색 품질 저하, 48h 4회). MAX_BATCH_TOKENS=16384 가 VRAM 상한이라 client batch entries 한도만 256(MAX_RERANK_INPUT 200 커버)으로 상향, reranker 만 재생성. 검증: 85-text rerank HTTP 200, batch 에러 0.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
절 보유 문서(예 5180)에서 이미지가 살짝 보였다 빈 절로 바뀌는 2단 플래시 수정:
① sections 로딩 전 useSectionView=false → fallback 풀-문서 뷰어(전체 md_content=이미지)가
잠깐 뜨고 곧 절뷰로 교체 → sectionsLoaded 플래그로 로딩 중엔 skeleton(풀-문서 미표시).
② 절뷰 진입 시 selectedSectionId=null 이면 selectedItem 이 outline[0](표지/front-matter,
이미지 가능)로 잠깐 렌더됐다 effect 가 defaultSelId(첫 본문 Part)로 점프 → selectedItem
조회 키를 (selectedSectionId ?? defaultSelId)로 바꿔 첫 프레임부터 본문 Part 직행.
데이터는 정상(5180 이미지 207개 DB row+파일 실존+key 일치) — 순수 렌더 전환 플래시였음.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
96bd849(절뷰 본문 MarkdownDoc 교체, 이미지·수식 fix)는 main 에 머지된 적 없이 라이브
프론트엔드에만 배포돼 있었는데, D8(main 기준 빌드) 배포가 옛 renderMd(plain marked)로
되돌려 docimg 이미지 제거·$$ 수식 raw 회귀. 절 본문 2곳(데스크탑 focusView·모바일 카드)을
다시 <MarkdownDoc mdContent={bodyText}> 로 — pre-render(수식·이미지 placeholder) + swap
(실 이미지). 96bd849 와 동일 변경, D8 의 Part 접이 위에 재적용.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>