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>
긴 ASME 코드 절뷰가 flat 1030 으로 길어지는 문제(front-matter 240 + 다중 PART 가 GROUP_MAX 초과
→ flat 폴백)를 표현 계층에서 해결. 빌더/재분해 무접촉.
- D9 cleanHeading: ASME 개정바 ðNÞ(<sup>ð</sup>**25**<sup>Þ</sup>) 통째 strip (가운데 25 안 남김).
- D7 buildPartOutline: 첫 content part(PART/SUBSECTION/항목코드) 경계로 front-matter 분리 +
본문을 heading_path 첫 세그먼트(PART)로 그룹. window/_split 도 PART 로 모여 흡수. content part
없으면 hasParts=false 폴백. SectionOutline(D8) 이 소비.
단위 17/17(신규 6: 개정바 strip·front-matter 분리·window 흡수·폴백·항목코드). 미배포·prod 무접촉.
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>
lifespan 실 경로(init_db + 전 worker import + 전 add_job)를 prod 이미지 컨테이너 +
ephemeral PG 로 실행해 router/worker import 오류·잡 등록 오류를 검출. NAS/scheduler.start/
prewarm 3개 부작용만 중립화(prod/AI 무접촉). GPU 실측 PASS: routes=173·jobs=34·schema 361·health ok.
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>
submit_attempt FOR UPDATE(3ba9537) 1차 방어에 더해 DB 레벨 belt-and-suspenders — 모바일
더블탭/재시도가 어떤 경로로든 이중 attempt INSERT 에 도달해도 차단. prod 실측 중복 0
(GROUP BY HAVING count>1 = 0)이라 안전 — dedup DELETE 멱등 precaution + partial UNIQUE
(quiz_session_id IS NOT NULL). 세션 외 직접입력(NULL)은 비대상.
검증: migration_smoke PASS(post-baseline 361 적용). ★FOR UPDATE 가 정상경로선 막으므로
이 제약은 거의 트리거 안 됨 — 트리거 시 IntegrityError→500(should-never-happen 가시화);
graceful 409 변환이 필요하면 submit_attempt 에 try/except 추가 가능(별도).
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>
조회/수정 경로는 deleted_at 을 일관 가드하나 파일/콘텐츠 서빙 5엔드포인트
(get_document_file·image_raw·save_content·preview·content)가 'if not doc' 만 검사 →
삭제 문서 원본/preview/전문/마커이미지가 doc_id(+토큰)만으로 노출·삭제 문서 NAS 재기록.
get_live_document(session, doc_id) 헬퍼(없거나 deleted_at 이면 404)로 통일 — '경로마다
deleted_at 기억' 대신 구조 강제(추가될 서빙 경로 자동 보호). save_content 는 삭제 문서
쓰기 차단까지. find_paper_holder 도 deleted_at IS NULL 필터 추가(dedup.find_canonical 대칭).
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- internal_study._verify_token: != 비교는 첫 불일치 단락으로 prefix 길이 timing
side-channel(RAG 정답 endpoint 보호 토큰) → hmac.compare_digest(search.py 정본 일치).
- memos tag 필터: f-string 으로 사용자 tag 를 JSON 배열 리터럴에 직접 삽입 → tag 안
"/] 가 JSON 깨 500 + 필터 변형. func.jsonb_build_array(tag) 바인드 파라미터로.
검증: py_compile 통과. R7 나머지(get_live_document·paper-holder deleted_at·delete_file
purge 마커+retention sweep·fetch-page·save-content)는 이어서.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
primary(맥미니) Timeout/ConnectError 시 동의·과금 통제 없이 ai.fallback(Claude API)으로
자동 전환 → 개인 문서/쿼리/메모가 Anthropic 으로 silent egress 되던 프라이버시 결함 봉쇄.
실패는 전파 — 배치 워커는 재시도/StageDeferred(R3), interactive 는 호출자 5xx 표면화
(documents.analyze 이미 502/504). 클라우드는 premium explicit-trigger / call_fallback
명시 호출로만 (자동 진입 금지).
참고: uncoordinated-mlx-semaphores 는 gitea/main 최신에서 digest/briefing 이 이미
acquire_mlx_gate 사용(감사 20커밋 stale 탓 오탐) — 변경 불요. rerank silent-identity 의
rerank_skipped notes 플래그는 시그니처 변경 동반이라 별도 후속(Low).
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
kosha run() 이 소스별 단일 세션으로 collector 전체를 돌리고 예외 시 rollback →
페이지 _api_get 실패가 앞서 적재한 케이스/항목을 전부 폐기(부분 적재 손실 + 매번
같은 지점 실패 시 영구 미적재). disaster_cases/fatal_accidents/guide 의 케이스·항목
단위로 session.commit() 경계 추가(csb/api_standards idiom) — 실패 이전 적재분 보존,
dedup 으로 다음 run 이 이어받음. 첨부 실패는 기존대로 격리(변경 없음).
검증: py_compile 통과.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
arxiv/openalex 수집기가 run_cap 도달로 카테고리/시드 중도 절단돼도 워터마크를
newest 로 전진시켜, [oldest-ingested, 옛 watermark] 사이 미적재 항목이 다음 run 의
watermark 필터에 영구 배제되던 silent data loss 수정.
capped 플래그: cap 으로 루프 절단 시 set → 워터마크 미전진. 미전진하면 다음 run 이
최신부터 재스캔하며 적재분은 dedup-skip(cap 미소모)하고 gap 까지 내려가 이어 적재
→ 백로그 run 당 cap 소화(livelock 회피). 정상 완주(watermark 도달/cursor 소진) 시에만
전진. bulk(CLI)은 cap 무관. docstring 의 '다음 run 이월' 약속을 실제 동작과 일치.
검증: py_compile 통과. kosha 부분실패 per-case commit 은 R4 후속.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
worker_fn 이 transient 실패를 삼켜 정상 반환하면 queue_consumer 가 status=completed
로 확정 → 영구 데이터 손실 + 재시도/추적 0. 정본(extract/marker/fulltext/stt 는
re-raise)과 어긋난 곳을 통일:
- deep_summary: 호출 실패(call_failed)를 삼키지 않고 raise → 재시도→failed dead-letter
(이전엔 ai_detail_summary 영구 누락 + tier triage 고착).
- thumbnail: _extract_thumbnail 실패를 silent return → raise (썸네일 영구 누락 방지).
- queue_consumer: 완료 커밋 후 enqueue_next_stage(정상·skip-note 2곳)를 자체 try 로
격리 — enqueue 실패가 outer except 로 전파돼 completed 항목을 재오픈(stage 재실행)
하던 결함 차단. 실패는 ERROR 로 가시화.
- broad except 에 asyncio.CancelledError 명시 통과(embed worker / ask classifier·verifier).
dead-letter = ProcessingQueue.status='failed'(기존 attempts/max_attempts 머신 재사용,
신규 컬럼 불필요). 검증: py_compile 통과. 큐 재시도 의미 synthetic smoke(staging) 예정.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
asyncio.gather 가 단일 AsyncSession 에 동시 execute 를 진입시켜 부하 의존적
'another operation in progress' 비결정 크래시 (정상 순차 경로에서만 검증돼 잠복).
사이트별 처방(균일 처방 회피):
- search_with_rewrite._variant_retrieve: variant 마다 독립 async_session() fan-out
(사용자 대면 — N variant 병렬 유지)
- study explanation_rag / subject_note_rag: 백그라운드 prefetch 라 순차 직렬화
(rerank 도 순차 — DB 순차+rerank gather 분할은 _gather_* 4곳 침습이라 보류,
배경 작업의 rerank 병렬 이득 미미)
추가: rewrite(multi-query) 경로가 axis 필터(material_type/jurisdiction/year)를
single-query path 와 달리 조용히 누락 — search_with_rewrite 에 axis 인자 + _variant_retrieve
가 search_text/search_vector 에 전달.
검증: py_compile 통과. 동시 N variant 부하 테스트(staging)로 크래시 소거 확인 예정.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
init_db 의 단일 트랜잭션 적용 경로(engine.begin)를 미러해 migrations/ 전체가
빈 DB / DR(pre-320 → catch-up) 업그레이드에서 한 트랜잭션으로 적용 가능한지 검증.
pg16(pgvector/pgvector:pg16) 핀, ephemeral 컨테이너 자동 기동/정리.
현재 두 시나리오 모두 011_embedding_1024 에서 FAIL — view active_documents 가
documents.embedding 의존(DROP COLUMN CASCADE 부재). enum(326) 이전 지점.
fresh replay 가 한 번도 검증된 적 없어 누적 비-replayable cruft 다수 확인.
R1(스키마 baseline 스냅샷)으로 fix 후 PASS 가 게이트 기준.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This reverts commit 60f3b25.
병렬 세션이 동일 P0(외부 링크 새 탭+rel)를 feat/memo-to-document 브랜치에
docMarkdown.ts link 렌더러 + ADD_ATTR 방식으로 이미 구현(SSR 적용·memos 번들).
중복 회피 위해 본 $effect 구현(redundant)을 canonical 에서 되돌리고 그쪽에 양보.
분석 산출물/측정 결과는 PKM learning 문서로 기록 보존.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
문서 본문 markdown 의 외부 http(s) 링크(코퍼스 실측 521문서/9,496개)가
target/rel 없이 같은 탭으로 열려 SPA 를 이탈하던 문제 수정.
MarkdownDoc 에 heading-anchor 와 동일한 DOM 후처리 $effect 추가 —
sanitize 후 라이브 DOM 의 a[href^=http(s)] 에 target="_blank" +
rel="noopener noreferrer" 부여. marked 렌더러/DOMPurify(전역 hook)·
ADD_ATTR 무수정. 앵커(#)·상대경로·mailto 는 미변경(SPA 내부 항법 보존).
내부 위키링크([[...]])·백링크 그래프는 코퍼스 실측상 실신호 ~8개로
데이터 미지원이라 본 PR 범위에서 제외(보류, 내부 링크 증가가 트리거).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>