statute_collector(safety-library-1 B-1)가 대체 — 모듈 import 0·스케줄러 미등록·동적로딩 없음 = 절대 로딩 안 되는 dead file. 'law_monitor' 잔존 출현 34건은 전부 source_channel enum 값/config 키/주석(모듈 참조 아님). 복구는 git history.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
코드리뷰 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>
검수완료(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>
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>
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>
영문 구조 헤딩 매처가 본문 'Part III to demonstrate…'·'Section I or Section VIII…'
같은 소문자 문장연속을 가짜 절로 잡던 것 차단. 식별자 뒤 선택 제목은 대문자/괄호/숫자로
시작해야 헤딩 인정. ATX 파트(# PART PG)·항목(#### PG-1)은 ATX 우선이라 무영향.
단위 11/11(음성·양성·ATX보존·통합 + 기존 7) + held-out 실데이터 회귀(5180 가짜절 1건 제거·
5206/5120/5130 무영향·added 0). CHUNKER_VERSION 유지(hier-rule-v1, D0a 결정).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
★실제 init_db() 런타임 검증(psql migration_smoke 가 못 잡는 asyncpg 경로)에서 발견·수정:
1. baseline 덤프에 CREATE TABLE schema_migrations 포함 → init_db 가 IF NOT EXISTS 로 선-CREATE
후 baseline 이 재-CREATE 충돌. --exclude-table=schema_migrations 재덤프(init_db 가 소유).
2. baseline 은 multi-statement 인데 exec_driver_sql(asyncpg prepared)은 multi-statement 불허
('cannot insert multiple commands into a prepared statement'). raw asyncpg simple 프로토콜
execute() 로 적재(같은 connection = 트랜잭션 내).
3. 마이그 360(10 DROP)·361(DELETE+CREATE)이 multi-statement → init_db 적용 실패. 360=콤마구분
단일 DROP, 361=단일 CREATE UNIQUE INDEX(prod 중복0·fresh 빈테이블이라 dedup DELETE 불요).
★검증: scripts/ci/initdb_runtime_test.py 로 실제 init_db 2회 — 1st(fresh: baseline 262 스탬프 +
359/360/361 적용, documents·purge_col·cand_drop·attempt_unique 전부 확인), 2nd(멱등 skip) PASS.
psql migration_smoke 도 PASS 유지.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
audit 의 dup-url-normalizer-divergent 는 design intent 오탐: news._normalize_url 은 query-식별
사이트(hada.io?id=·HN item?id=) 별개 기사 붕괴 방지 위해 보수적(query 보존·sort/trailing-slash/
소문자화 안 함), file_watcher._canonicalize_url 은 web_clip dedup 위해 공격적 정규화 — 채널별
의도된 차이. 통합하면 news dedup 가 깨진다(docstring 경고). 두 함수 docstring 에 상호 cross-ref +
'통합 금지' 명시해 미래 잘못된 통합 차단. 동작 변경 0(주석만).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
_fetch_rss/_fetch_api_guardian/_fetch_api_nyt 가 복제하던 동일 존재체크
(file_hash 또는 edit_url.in_([normalized,link]) 매칭) 를 단일 헬퍼로 — byte-identical
블록이라 동작 100% 보존. news_collector god-file 중복 일부 감소.
(채널별 Document 빌드 30줄 3중복 통합은 채널별 필드 차이 검증 필요 → staging/별도.)
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
company/topic/year/doctype 4 facet 집계 블록이 거의 동일 복붙(각 base_query 재구성 +
다른 3축 필터 적용). 적용된 facet 필터를 applied dict 로 모으고 '자기 자신 축 제외' 헬퍼
_facet_count(name, col, order_by, value_fn)로 추출 — 쿼리/자기제외/order_by(year=desc·others=count)/
value 매핑(year=str) 모두 동일 보존. 동작 무변경(staging 에서 facet 카운트 동등성 확인 권장).
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- synthesis_service: _CACHE 가 ts 없이 result 만 저장해 CACHE_TTL(1h) 미적용 → 원문 수정돼도
CACHE_MAXSIZE 찰 때까지 stale answer 반환. (ts, result) tuple + get_cached 에서 만료 pop
(query_rewriter expire_at 정본 복제).
- chunk_worker: 문서마다 news_sources 전량 로드 후 Python prefix 루프 → DB 필터 푸시다운
((name==source_name) | startswith(source_name+' ')). split[0]==source_name 과 동치, autoescape.
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
거래문서(LIBRARY_SUGGESTION_DOCTYPES) 제안이 doc.ai_suggestion is None 체크 없이 덮어써,
material 제안 블록(material_type 제안)이 이미 점유한 ai_suggestion 을 clobber 하던 비대칭.
material 블록과 동일하게 is None 가드 추가 — 주석의 '기존 제안 우선' 사상 일치.
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
전체 Event.id 를 메모리 로딩 후 len() 하던 것을 select(func.count(Event.id)) 로 전환 —
행 수에 선형이던 메모리/전송 비용 제거. 결과 동등(단순 카운트라 golden-diff 불요).
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- news.collect: locked() 체크 후 실제 acquire 가 별도 task 안에서 일어나 그 사이 다른 요청이
끼어들어 이중 수집 task 가 생기던 TOCTOU. 핸들러에서 동기 acquire + task finally release 로 원자화.
- tier_backfill._enqueue_domain: filter_clause 가 SQL 에 직접 보간되나 allowlist 가드 부재
(retrieval_service _VALID_DOCS_TABLE 정본 대비 비대칭). DOMAIN_PRIORITY 출처 allowlist final
gate 추가 — 현재 모듈 상수라 injection 0 이나 외부 입력화 시 즉시 차단.
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- ai_tags: 주석/Mapped 타입이 dict 인데 실제 list 적재 → list 로 정정.
- ai_tags/user_tags: default=[] (정의 시점 1회 평가되는 공유 가변 인스턴스) → default=list
(callable, 행마다 새 리스트). SQLAlchemy column default 관용 idiom.
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
setup 완료 후에도 모든 비-bypass 요청이 select count(User.id) 를 실행하던 per-request
비용. 셋업 완료(user 존재)는 monotonic 이라 1회 확인 후 _setup_complete 플래그로 영구
skip(이후 요청 DB 쿼리 0). global 선언은 함수 첫 줄(read+assign 혼용 UnboundLocalError 방지).
R10 잔여(library-tree jsonb 집계 golden-diff·facet-counts·events-count·synthesis cache TTL)는
결과 동등성 검증 동반이라 후속. 검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
quiz_session 을 session.get(잠금 없음)으로 읽어 모바일 더블탭/재시도 시 동시 제출 둘 다
cursor=N 을 보고 cursor+1·correct/wrong/unsure count 를 이중 가산하던 race. select +
with_for_update() 로 행 잠금 → 직렬화. 두 번째 제출은 첫 commit 후 cursor=N+1 을 읽고
cursor 위치 불일치 409 로 거부된다.
belt-and-suspenders 인 attempt UNIQUE 제약은 기존 중복 dup-backfill 마이그가 선행조건이라
별도(R9 후속). 검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
delete_file 파라미터가 광고만 하고 본문에서 0회 참조(soft-delete만, 파일 영구 잔존 +
프론트가 실제 호출)되던 거짓 계약 구현. (c) 큐드삭제:
- 마이그 359: documents.purge_requested_at 컬럼(ADD COLUMN IF NOT EXISTS, replayable).
- delete_document: delete_file=true 시 purge_requested_at 마커 set(deleted_at 과 별도).
- document_purge_sweep cron(03:20 KST): purge_requested_at + grace(30일) 경과 + 파일 존재
시 NAS 원본 unlink + AUDIT 로그. ★sweep 는 deleted_at 아니라 purge_requested_at 기준 —
일반 숨김(delete_file=false)은 파일 보존(undelete 가능), 명시 purge 만 물리삭제(데이터 안전).
- DELETE 요청 경로엔 동기 비가역 op 0. 파일 존재 체크로 멱등. unlink 는 to_thread(R5 일관).
검증: py_compile 통과. migration txn 제어문 없음.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>