Commit Graph

173 Commits

Author SHA1 Message Date
hyungi daf6a0ade9 feat(documents): S1 dedup·office-md·storage scaffold (B/C/D/E)
plan ds-s1-backend-1 잔여 구현 (A·C-1 은 16b0fe1):
- B 중복검사: services/dedup.py (OFF-list law_monitor 공용) + 업로드 채움(B-1)
  + GET /documents/duplicates(B-2) + post-upload near-dup 비동기(B-3)
  + backfill_dedup.py(B-4) + 야간 dedup_reconcile 잡(03:30 KST 멱등 재계산)
- C MD-first: marker_worker office/hwp 분기 _process_office(C-2) + md_status
  상태머신 postcondition success|failed(C-5) + backfill_nonpdf_markdown.py(C-4)
  + requirements markitdown
- D 스토리지: services/storage ABC+Range 계약 / LocalBackend / NasApiBackend 503
  (D-1) + /file resolver 경유, 로컬 동작 불변(D-2)
- E 운영: pre-change pg_dump + rollback_287.sql + apply runbook(E-3) + 테스트(E-1)

비파괴 불변식 유지(기존 응답 shape 무변경, md_status success→completed read-time 매핑).
어드버서리얼 리뷰 확정 1건(soft-delete canonical 승격 시 stale duplicate_of) → B-1
승격 정규화 + 야간 재계산으로 정합.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 03:05:30 +00:00
hyungi 68e2d7ea04 feat(documents): S1-ADD dedup·원본명 3컬럼 + md_status success→completed 매핑 (A) + office→md PoC (C-1)
plan ds-s1-backend-1 (r5 수렴). 코드만 스테이징 — migration 미적용(restart 보류, E-2 Soft Lock 예외창).

A (앱 v1 디코딩 비파괴 최소선):
- A-1 migrations/287_documents_dedup_fields.sql: original_filename TEXT / duplicate_of BIGINT FK ON DELETE SET NULL
  / duplicate_count INTEGER NOT NULL DEFAULT 0. 단일 statement·PG16 fast-path·BEGIN/COMMIT 금지. backfill 미포함(B-4).
- A-2 app/models/document.py: 1계층 블록에 3 mapped_column (+ ForeignKey import). md_* 는 기존.
- A-3 app/api/documents.py: DocumentResponse 3필드(duplicate_count=0 non-opt) + DocumentDetailResponse
  field_validator(success→completed, mode=before) — read-time DB→API 단방향, write(ORM) 미적용.
- A-4 tests/test_s1_dedup_shape.py: success→completed 동작 + 비-success 통과 + 3필드 디폴트/roundtrip
  + ds-app contract fixture 디코드(skip-if-absent). py_compile OK. ★ backend 절반 — 전체 비파괴는 S3 render 테스트와 AND.

C-1 PoC (워커 미연결 — C-2 에서 marker_worker 분기 연결):
- app/workers/office_md.py: OOXML=markitdown(신규 dep, lazy) / hwp·hwpx=LibreOffice headless→HTML→markdownify(기존 dep).
  실패·빈출력·타임아웃·dep부재 → OfficeMdError raise (success+빈md 금지 = C-5 postcondition 의 변환기 계약).
- scripts/poc_office_md.py: 표 fidelity 측정 하니스. E-1 = prod LibreOffice 버전핀 안전컨텍스트 실행(hwpx 필터 버전 의존).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 03:05:30 +00:00
hyungi 5a19cde38c fix(documents): 도메인 트리 카운트를 문서함 list 제외와 일치
트리(/documents/tree)는 deleted 만 제외하고 뉴스/법령/메모를 다 세는데, 문서함 list 는
source_channel news/law_monitor + file_type note 를 기본 제외 → '트리는 N건인데 클릭하면
0건' 불일치(예: Philosophy/Aesthetics 5건 전부 news+note 라 클릭 시 0). 트리 쿼리에 동일
제외 적용해 카운트=실제 표시 일치. 영향: Philosophy 12→2, General 189→84 등 정상화.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 09:57:47 +09:00
hyungi 6a85087b83 feat(eid): 이드 persona substrate W2~W4 — DS compose·약점진단·egress 코드층 박탈
전 로컬 LLM 관통 '이드' persona substrate 의 Document Server 측 빌드(W2~W4).
설계 = PKM eid-persona-substrate(r1~r3 수렴) / impl = eid-persona-impl.

W2 — compose + 표면 배선:
- app/eid/compose.py: persona→rules→overlay→task 단일 system 문자열 + 정적 ROUTE_MAP
  (런타임 sniffing 아님) + rules 부재 fail-loud · persona 부재 quiet · overflow fail-loud.
- 자유-prose 3 표면(react_ask·study_subject_note·study_question_explanation) 중복 정체성·
  generic 정책 trim + compose 배선(AIClient 에 additive system 파라미터). 도메인 calibration 보존.
- STRICT JSON 기계류(briefing_comparative·digest_topic)는 persona-ZERO 동결(불변식 #3).
- app/prompts/substrate/: persona(외부 컴파일 산출물 vendor) + rules(생성 가드 서브셋) + overlay 5.

W3 — migration + 워커 + study_diagnosis:
- migration 301~305: eid_* append-only 원장(약점/복습초안/회고) + approval_requests(가변 큐) + 일정 파생뷰 2.
- app/workers/study_weakness.py: study_question_progress.pattern_state 집계로 약점 derived 산출
  (LLM 0) + bounded tier(watch/review/focus). nightly cron.
- study_diagnosis 표면: 최신 스냅샷을 코치 언어로 번역(약점 판정은 코드, LLM 은 블록 값만 인용).

W4-1 — egress 코드층 박탈:
- app/eid/ai.py EidAIClient: 이드 표면 = call_primary(내부 MLX) only. 외부 LLM fallback 경로
  구조적 봉쇄(call_fallback raise · 자동 fallback 제거 · 외부 endpoint 차단). egress 워커는 분리 유지.

load-bearing 정정 3(환경 grounding 강제, 설계 회귀 아님):
- rules = 운영 ruleset 전체 → 생성 가드 서브셋(HTML 산출물 룰이 study task 와 충돌).
- append-only = REVOKE → CREATE RULE DO INSTEAD NOTHING(단일 owner role 은 REVOKE 무효 +
  migration 검증기가 plpgsql BEGIN 거부) + actor/source_* NOT NULL 스탬프.
- 이드 LLM 봉쇄 = path discipline → EidAIClient 구조화.

검증: eid 순수 단위테스트 30 통과 + py_compile + migration 검증기 모사 + egress 적대감사 COMPLETE.
DB/LLM/httpx 의존 테스트(append-only RULE·EidAIClient·E2E)는 staging(Docker) 가동.
W4-2 네트워크 belt 은 조건부 보류(코드층 1차 충분, P0-3② 원격 실측 후 hard-gate 시 승격).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 15:13:20 +09:00
hyungi c12c04a9b1 fix(study): 복습 큐 cold-start — /due 에 신규 승인 카드 포함(첫 회상)
B2 /due 가 due_at<=now(progress 보유) 카드만 반환 → progress 는 rate_card(=/rate)로만 생기고 /rate 는 /due 카드만 평가 → 신규 승인 카드가 SR 큐에 영영 못 들어가는 순환 갭. 복습 트랙이 절대 안 채워짐.

- /due 를 outerjoin 으로 재작성: 신규(progress 없음=첫 회상 전) OR 예정 due(due_at<=now, stage<4). 예정 due 먼저, 신규(due NULL) 뒤로. '첫 회상 후 due' 규칙·시안('오늘 복습'에 stage0 신규 포함)과 일치.
- 신규 카드 '암'은 백엔드가 due 안 박음(외움→큐 제외, 큐 폭발 방지)이라 correctLabel(null)='안 나옴'으로 정합(기존 '+3일'은 거짓 라벨). 큐 stage0 '암'은 그대로 '+3일'.

검증: py_compile OK. 신규 암→progress(due null, 재출제 X) / 애매·모름→due 내일 입고 / 큐 stage 전진 불변.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:45:07 +09:00
hyungi 861db96305 feat(study): 카드 SR 모바일 학습 UI — 복습/그냥공부 2트랙 (B3)
검수 완료 카드를 모바일에서 학습하는 UI. 복습(SR)=앞면 회상→reveal→3단 자기평가(모름/애매/암) / 그냥공부(cram)=덜 본 순 휙휙+봤다(SR 무관).

- 새 페이지 /study/cards-study(+page.svelte): landing 트랙선택·진행바·결과(세션 tally)·빈/로딩 상태·cram format 필터·키보드(Space reveal·복습 J/K/L·cram Enter). 아이폰15PM 우선, 세이지 토큰.
- '암'(correct) 버튼 stage별 동적 라벨(+3/7/14일·졸업), 모름/애매=내일. correctLabel은 sr_schedule REVIEW_INTERVAL_DAYS 미러(라벨 전용, 산술 정본은 백엔드).
- API: /study-cards/due CardItem에 review_stage 추가(복습 큐에서만 채움, 동적 라벨용). _build_card_items(session,cards,stages) 확장, /due는 select(card, progress.review_stage)로 변경.
- 진입: 허브 '암기카드 학습' 카드+예정목록 갱신 / 검수 UI 헤더 '학습' 버튼.

검증: py_compile OK · 4차원 적대검토(runes·API계약·SR규칙·UX) 통과(확정 조치 0, 지적 2건 거짓양성). 로컬 vite 빌드 불가(node_modules 부재)→배포가 컴파일 게이트.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 11:37:19 +09:00
hyungi 0d274cc5fe feat(study): 카드 SR writer + 두 트랙 API (B2 — 복습/그냥공부)
검토 완료 카드를 학습하는 백엔드. 복습(SR)=즉시 자동 입고 / 그냥공부(cram)=봤다 기록, SR 무관.

- migrations 299(idx_card_progress_due partial) + 300(study_memo_cards view_count/last_viewed_at).
- StudyMemoCardProgress 모델(294 미러, UNIQUE user+card) + rate_card(get-or-create → sr_schedule.advance/first_due, 즉시 자동 입고: 애매/모름 평가 즉시 due, 암은 due 안 박음).
- StudyMemoCard view_count/last_viewed_at + record_card_view 헬퍼(cram, SR 무관).
- API: GET /study-cards/due(복습 큐, 검수통과만) · POST /{id}/rate(자기평가 read-time 매핑) · GET /deck(cram, 덜 본 순) · POST /{id}/view(봤다 기록).

검증: 부팅+8라우트 등록 · 287~300 ephemeral 적용(인덱스·컬럼 확인) · sr_schedule 회귀 7/7(B1).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:18:17 +09:00
hyungi e1da984e08 refactor(study): SR 산술 sr_schedule.py 공용추출 (B1 — 카드 SR 토대)
문제 SR과 카드 SR이 같은 간격 상수·산술을 참조하도록 순수함수 추출. 운영 동작 무변경.

- app/services/study/sr_schedule.py: REVIEW_INTERVAL_DAYS{1:3,2:7,3:14}/MASTERED=4/FIRST_DUE=1
  + advance(stage,outcome,now)→(new_stage,new_due) | None(skipped) + first_due(now).
  진입 게이트(due_at IS NOT NULL/최초 due/skipped 불변)는 호출부 잔류(finalize vs review-complete 정책 차이).
- session_finalize.py: 상수·advance 분기 → sr_schedule import + sr_advance() (re-export 유지).
- study_question_progress.py: DEFAULT_FIRST_DUE_DAYS → sr_schedule import.
- 회귀 테스트 7/7: 전진 1·3·7·14·졸업·리셋·skipped불변·상수 + 전 stage×outcome 구 로직 바이트 동등.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 10:11:38 +09:00
hyungi e9a95934ef feat(study): 카드 검수 그룹핑 — manual(직접 추가) 카드를 자료(material)별 묶음 + source_kind 노출
직접 추가 자료 카드(source_kind='manual', 출처 문제 없음)가 검수 UI에서 null 한 덩어리로
뭉치지 않도록 extra.material 별 그룹("[자료] ...") + CardItem.source_kind 노출(프론트 '직접 추가 자료' 라벨).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 09:41:13 +09:00
hyungi b9f2ade55e feat(study): 암기카드 검수 UI — 백엔드 카드 review API + SvelteKit /study/cards-review
577 카드(needs_review=true)를 보고 채택/수정/폐기하는 첫 검수 화면(학습 흐름 '마지막 한 칸' 1번).

- 백엔드 app/api/study_cards.py(prefix /api/study-cards): GET(출처 문제별 그룹, evidence 동반)·needs-review/count·PATCH(승인 needs_review=false / 수정 시 dedup_hash 재계산+검수완료)·DELETE(soft)·approve-batch(문제 단위, 전체 일괄승인 없음).
- 프론트 /study/cards-review: 반응형 그룹 목록(문제+카드) · 카드별 승인/수정(인라인)/삭제 · 문제 단위 일괄승인 · format 필터 · 세이지 토큰. study 허브에 진입 링크+대기 카운트 배지.
- 카피 drift 정정: 허브 '예정(Phase 2~)'이 가동 중인 퀴즈/SRS/통계를 잘못 표기 → 예정은 카드 SRS·모바일·알람으로 수정.

검증: 백엔드 부팅+라우트 등록 OK(4 route). 프론트 빌드는 배포 시 vite.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:49:11 +09:00
hyungi 19f544fb5e feat(study): 공부 암기노트 Phase 1 — 정정/삭제 훅 + needs_review 큐 + 알람 재료 (HR/A)
추출 파이프라인(287~298, 별 커밋) 위 HR/A. 신규 마이그레이션 0 (DDL은 295~298 재사용).

- HR 정정/삭제 훅: PATCH 본문 수정 → 파생 study_memo_cards needs_review=auto(source_changed),
  soft-DELETE → source_deleted. flag_cards_for_source 헬퍼(임시 플래그, 최종정리는 워커 supersede).
- HR needs_review: PATCH set/clear(flagged_by='user' 서버강제) + GET /study-questions/needs-review
  목록·count(부분인덱스 술어 일치, 동적 {id} 라우트보다 먼저 등록해 int 파싱 충돌 회피).
- A 알람 재료: study_topics.focused_at 공부중 토글 + study_reminder cron(09/13/19 KST, due 술어
  quiz_selection SQL 재현·시간슬롯 truncate 멱등·LLM 0) + GET /api/study-reminders/latest(없으면 204).
- 테스트: 가드/정규화 18/18 (정량=evidence 원문·cue/cloze 누출·dedup·배치).

검증: 앱 부팅 import+mapper OK · 가드 18/18 PASS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 08:08:55 +09:00
hyungi aa2d7814e3 feat(digest): date picker URL sync + article→문서 라우팅 + country 국기·한국어
- GET /api/digest/dates 신설 (브리핑 /briefing/dates 패턴 미러, read-only)
- topic article 제목 enrich (documents 배치 1쿼리 + dedupe(set) + map-miss=null → 프론트 '(제목 없음)')
- /digest 재작성: ?date=&country= URL sync(공유·뒤로가기), 국가 탭=인라인 SVG 국기+한국어, 기사=/documents/{id} 링크(상위5+펼치기)
- Phase 4.5(PR #22) 후속. 검증: py_compile·dates/enrich 쿼리(275 resolve·miss 0)·frontend docker build PASS. 시각 렌더 검증=preview 게이트 대기

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 23:39:07 +00:00
hyungi 2f152911f7 feat(search): /ask corpus_variant + exact_knn (EVAL-ONLY) for passage-RAG diagnose
PR-DocSrv-Hier-PassageRAG-Diagnose-1 c1. /ask evidence retrieval 의 chunk leg 를
측정 뷰(prehier/hier_sim_*)로 교체 + exact_knn — passage evidence 단위(hier 절 vs
legacy 윈도우) 비교용. /search 와 동일 패턴, run_search 전달. EVAL-ONLY 박제,
default(미지정) 시 기존 /ask byte/behavior 동일(회귀 0). pattern 검증 → 잘못된 값 422.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 06:14:59 +00:00
hyungi 100aaa3b0c feat(search): corpus_variant + exact_knn measurement dispatch (replace-diagnose c4+c5)
PR-DocSrv-Hier-Replace-Diagnose-1 c4+c5. hier vs prehier(legacy) go/no-go 비파괴 측정 hook.
- 측정 뷰 3종 (hier_measure_views.sql, additive/droppable): corpus_chunks_prehier
  (legacy+null-source 375 포함) / hier_sim_raw / hier_sim_clean (childless-tiny<30 제외,
  all-tiny doc 은 legacy fallback 정합).
- retrieval_service: _resolve_corpus_variant + CORPUS_VARIANT_MAP + _VALID_CHUNKS_TABLE
  3 뷰 추가 + exact_knn(SET LOCAL enable_indexscan/bitmapscan=off, eval 전용).
  chunk leg 만 영향 (doc-level + fts/trgm = documents 무관). baseline/None path 회귀 0.
- search_pipeline.run_search + search.py: corpus_variant/exact_knn 전달, unknown→400,
  embedding_backend cand 와 동시 사용 금지(400).
- run_eval: --corpus-variant + --exact-knn flag.
- tests/test_corpus_variant.py 22 PASS (resolver/map/allowlist + SQL injection 거부).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 05:37:15 +00:00
hyungi f7198d9d68 feat(search): expose hier section outline & summaries in document detail
PR-DocSrv-Hier-Section-UI-1 Phase 1 (코드+커밋만, 배포는 Phase 2 backfill 완주 후).

- backend: GET /documents/{id}/sections — hier leaf 목차 + chunk_section_analysis
  요약. document_chunks 직접 조회(retrieval 아닌 목차 표시라 corpus_chunks 뷰
  의도적 우회 — docstring 명시). DISTINCT ON 으로 최신 분석 1행.
- frontend: SectionOutline.svelte(좌측 목차, per-doc 동적 그룹/flat, window
  dedupe, 클릭 시 요약/breadcrumb 인라인), headingPath.ts 순수 유틸(+node:test
  단위테스트 8케이스). [id]/+page.svelte 3-zone 레이아웃 + 우측 메타 Tabs
  [정보|AI|관리] 로 카드 스프롤 해소.
- 절 없는 문서/404 는 목차 숨김(graceful). 본문 점프는 follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-25 00:22:34 +00:00
hyungi c4a40ab18a docs(search): Phase 2Q closed as evaluated experiment (deprecated, not recommended for production)
사용자 결정 (2026-05-24, measurement chain 4-layer 정정 완료 후):

> Phase 2Q Query Rewrite is closed as an evaluated experiment.
> After result-level dedup correction, true net gain was marginal
> (NDCG +0.019, Recall t≥2 +0.030) while latency cost was high
> (cold +876%, warm +320%). Therefore, multi-query rewrite is not
> recommended for default production rollout. Keep opt-in path as
> experimental/deprecated reference only; do not proceed to
> Cache-Prewarm unless future real-query evidence shows a stronger gain.

변경:
- docs/phase_2q_apply_opt_in.md: 🛑 DEPRECATED / EXPERIMENTAL status 박제. measurement chain
  정정 history (4-layer) + 진짜 효과 + Phase 2Q 성과 보존.
- app/api/search.py: rewrite_backend query param description 갱신 (⚠️ EXPERIMENTAL/DEPRECATED,
  production 추천 문구 제거, opt-in 실험 reference 만 유지 명시).

5 액션 박제 (사용자 결정):
  1. opt-in 코드 유지 (recommended=false / experimental)
  2. docs/ deprecated 박제
  3. search.py description production 추천 제거
  4. PR-2Q-Cache-Prewarm + PR-2Q-Apply-Default-ON-1 폐기
  5. Extended 4건 중 SynonymDict (deterministic, LLM 우회) 만 별도 후보 보존

신규 feedback memory: [[feedback_measurement_chain_audit]] — Diagnose 측정이 Apply/rollout
결정 기준일 때 retrieval/fusion/rerank/eval 모든 layer audit 필수. Phase 2Q 4-iteration
정정 chain (0.927→0.876→0.641→0.663) origin.

Phase 2Q 성과 (실패가 아닌 좋은 실험):
- chunk_id/doc_id 중복 inflation 발견 + measurement chain audit pattern 확립
- LLM rewrite 는 현재 DS 검색 기본값으로는 ROI 낮음 결론 확보
- search_pipeline 의 multi-query 합성 + 3-layer dedup 인프라 보존 (Extended SynonymDict
  또는 미래 cloud LLM scaffold 재사용 가능)
- 신규 feedback memory 4건: fixture-first-call-shape / apply-prereq-structural-fix /
  graded-ndcg-dedup-invariant / measurement-chain-audit

main 위 직접 commit (read-only docs / API description, retrieval path 영향 0).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:57:11 +00:00
hyungi 59bde9a399 feat(search): phase-2q apply opt-in — production rollout 시작, 1주 관찰 (gemma-4)
plan pr-2q-apply-query-rewrite-1-bright-meadow.md. Phase 2Q Diagnose closure +
Rerank-Payload-Fix (main 0257a5d) 완료 후 Apply rollout. opt-in path 가 Phase 1B/2
부터 이미 production 가동 중 → 본 PR 의 production 영향 0 (marker PR).

rollout 정책:
  · default = rewrite_backend null (single-query path, baseline 회귀 0 invariant)
  · 명시 opt-in = ?rewrite_backend=cand_multi_query_macmini (추천 gemma-4)
  · 대안 = cand_multi_query_macbook (qwen3.6, mixed/english 강점, MacBook 가동 시)
  · 1주 관찰 (2026-05-24 ~ 2026-05-31) → metric 정상 시 default ON 별 PR

변경 (production 영향 0):
- docs/phase_2q_apply_opt_in.md 신규 — 사용자 가시화:
  · 사용 방법 (query param + SvelteKit fetch 예시)
  · 1주 관찰 metric 목표 (cache hit ≥ 50% / LLM warm p50 ≤ 1500 / 503 ≤ 5/day / Recall t≥3 ≥ 0.74)
  · 추천 LLM 사유 (decision md §4 4-factor) + 대안 명시
  · Phase 2 QueryAnalyzer sequencing 박제 (영향 0, ask_events 0건 운영 관찰 후 확정)
  · Follow-up PR 5건 명시 (Telemetry / Alert / Default-ON / Cache-Prewarm / Category-Analysis)
- app/api/search.py — rewrite_backend query param description 갱신.
  Apply 진입 박제 + 추천 LLM 표시 + docs 링크. 동작 변경 0.
- tests/search_eval/baselines/v0_2_phase2q_apply_smoke_2026-05-24.json — production smoke:
  · opt-in path HTTP 200 + total_ms 957 (cache hit) + rerank_ms 109 (정상 호출) + fallback 0
  · baseline path HTTP 200 + total_ms 207 + rerank_ms 19 + fallback 0 (회귀 0 확정)

38/38 unit test PASS (회귀 0). main HEAD 0257a5d 위 branch.

Closure gate PASS:
  · docs 가시화 / search.py description / smoke json 박제
  · production smoke 양쪽 path 정상 + 회귀 0 verify
  · 메모리 갱신 + 1주 관찰 종료일 2026-05-31 박제

Follow-up: 1주 후 PR-2Q-Apply-Default-ON-1 (metric 정상 시) 또는 fix PR.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 04:01:49 +00:00
hyungi 1ae7802485 Merge pull request 'Feat/ds ai routing policy' (#23) from feat/ds-ai-routing-policy into main
Reviewed-on: #23
2026-05-24 12:20:49 +09:00
hyungi ecd2350c15 feat(search): Phase 2Q Diagnose Phase 2 — multi-query retrieval fusion
phase-2q-query-rewrite-diagnose.md v6 plan §5.5 + §7 Phase 2.
Phase 1B 3e6866b (scaffold + dispatcher) 위 retrieval 합성 wire-up.

신규:
- search_pipeline._rrf_fuse_variants() — N variant ranked list RRF 합성.
  fusion_service.RRFOnly 알고리즘 동일 (k=60), 첫 등장 variant representative 보존.
- search_pipeline.search_with_rewrite() — variant N 별 retrieval+fusion 후
  unified RRF (cap 60) → reranker 1회 (query=원본 q) → diversity+freshness+display.
  · per-variant K = 50//3 = 16 (PHASE2Q_PRODUCTION_TOPK//N, A1 채택)
  · variant 별 retrieval asyncio.gather 병렬
  · chunks_by_doc merge (variant 무관 unified reranker input)
  · production fusion_service.get_strategy() + rerank_chunks() 재사용
- 상수: PHASE2Q_PRODUCTION_TOPK=50, PHASE2Q_UNIFIED_CAP=60, PHASE2Q_RRF_K=60.

수정:
- search_pipeline.run_search() — rewrite_backend param 추가. hybrid + cand_<slug> 시
  search_with_rewrite() 위임. baseline/None 시 기존 single-query path 그대로 (invariant).
- app/api/search.py — Phase 1B scaffold discard call 제거. run_search 에 rewrite_backend
  전달. ValueError → 400 (unknown_rewrite_backend 우선 분기) / RuntimeError → 503
  (rewrite_llm_unavailable).
- tests/test_query_rewriter.py — Phase 2 test 9개 추가:
  · _rrf_fuse_variants 6 (single / overlap accumulation / representative / cap limit /
    empty / rank position)
  · search_pipeline import + run_search rewrite_backend default=None signature 1
  · PHASE2Q_* constants 1
  · DATABASE_URL dummy 주입 (api.search import → SQLAlchemy engine init 회피)

30/30 unit test PASS (Phase 1B 21 + Phase 2 9).

baseline 회귀 0 invariant:
- run_search(rewrite_backend=None) → 기존 path 100% 그대로 (분기 first line guard)
- run_search(rewrite_backend=baseline) → 동일
- mode != hybrid → multi-query path 비활성 (text-only/vector-only/trgm 영향 0)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 22:41:50 +00:00
hyungi 3e6866b4ae feat(search): Phase 2Q Diagnose Phase 1B — scaffold + dispatcher
phase-2q-query-rewrite-diagnose.md v6 plan Phase 1 의 fixture 외 잔여.
Phase 1A 446ba82 위 dispatcher + cache + LLM call + API param + eval flag + 21 unit test.
retrieval 합성 (search_with_rewrite) 은 Phase 2 별 commit.

신규:
- app/services/search/query_rewriter.py — LLM_BACKEND_MAP + _resolve + cache + rewrite()
  · slug-based allowlist (no silent fallback), httpx 직접, Priority.FOREGROUND semaphore
  · sampling 박제 (gemma response_format json_object / qwen prompt rule only — Phase 0 inspect 9)
  · manual TTL cache (query_analyzer 패턴 1:1, sha256[:32] NFKC key, LLM_REWRITE_TIMEOUT_MS=15000)
- tests/test_query_rewriter.py — 21 test PASS (resolve / cache key / parser / cache TTL / constants)

수정:
- app/api/search.py — ?rewrite_backend= query param + 400 unknown / 503 unavailable.
  scaffold = call but discard variants (retrieval path 영향 0). Phase 2 에서 합성.
- tests/search_eval/run_eval.py — --rewrite-backend flag + 4 hot spot wire-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 22:25:03 +00:00
hyungi 076c0e1802 feat(eval): Phase 2B Reranker Diagnose — dispatcher + gte 측정 + decision (H3 bge-reranker-v2-m3 유지)
round-2-review-mighty-starfish.md v2.1 (Phase 2B Reranker Diagnose) plan 실행.
Phase 2A 의 CANDIDATE_BACKEND_MAP 패턴 재사용 + RERANKER_BACKEND_MAP 신규.

코드 변경 (4 파일):
- app/services/search/rerank_service.py:
  - RERANKER_BACKEND_MAP allowlist (baseline / cand_gte_ml_base, slug-based resolve)
  - _resolve_reranker(slug) → endpoint URL or None
  - _rerank_via_candidate_endpoint() — 후보 TEI POST /rerank
  - rerank_chunks() 시그니처에 reranker_backend + snapshot_*_id_max 추가 + dispatch log
- app/services/search/search_pipeline.py: run_search() threading
- app/api/search.py: reranker_backend Query parameter + 400 unknown_reranker_backend 에러 매핑
- tests/search_eval/run_eval.py: --reranker-backend flag + call_search/evaluate threading

infra:
- docker-compose.override.rerank-cand.yml: 3 후보 service (gte_ml_base / mxbai_large / bge_v2_gemma_2b),
  profile 'rerank-cand' 격리, restart=unless-stopped

측정 산출물 (51 case, scored=46, failure=5):
- reports/v0_2_phase2b_baseline_snapshot_2026-05-23.csv (NDCG 0.659, Phase 2A 와 일치 = 재현성 PASS)
- reports/v0_2_phase2b_gte_ml_base_2026-05-23.csv
- tests/search_eval/baselines/v0_2_phase2b_{baseline_snapshot,gte_ml_base}_2026-05-23.json
- reports/phase_2b_reranker_decision_2026-05-23.md
- tests/fixtures/tei_rerank_response.json (G0-1 한국어+영어 mixed sample sanity PASS)

후보 TEI 1.7 호환성 (Phase 1 smoke gate):
- cand_gte_ml_base       :  PASS (xlm-roberta-based, TEI 호환)
- cand_mxbai_large       :  deberta-v2 미지원 → Phase 2B-Extended (sentence-transformers wrapper)
- cand_bge_v2_gemma_2b   :  LLM-based reranker, 1_Pooling/config.json 부재 → Phase 2B-Extended (FlagEmbedding wrapper)

결과 (1 후보 측정 + baseline rebaseline):
| Candidate                          | NDCG  | Δ baseline | mixed | korean | exam  | p50 ms |
|------------------------------------|------:|-----------:|------:|-------:|------:|-------:|
| bge-reranker-v2-m3 (baseline)      | 0.659 | —          | 0.39  | 0.51   | 0.74  | 454    |
| cand_gte_ml_base                   | 0.604 | -0.055     | 0.38  | 0.41   | 0.62  | 345    |

Decision (H3): bge-reranker-v2-m3 유지. gte 의 reranker quality 가 production 보다 약함 (korean_only -0.10, exam -0.12, overall -0.055).

후속 PR 백로그 (6건):
- PR-Search-Query-Rewrite-1 (Phase 2Q, korean_only/mixed 보완 권고)
- PR-2B-Extended-Mxbai-Large (sentence-transformers wrapper)
- PR-2B-Extended-Bge-V2-Gemma (FlagEmbedding LayerwiseReranker wrapper)
- PR-2B-Extended-Jina-V2-ML (license 결정 후, 개인 비영리 가정)
- PR-2B-Cloud-Reranker-Scaffold-1 (Cohere scaffold-only, 선택)
- PR-2B-Rerank-Cand-Cleanup-1 (1주 후 cand 컨테이너 정리)

production 영향:
- production reranker (bge-reranker-v2-m3) 변경 0
- config.yaml ai.models.rerank.endpoint 변경 0
- embedding (bge-m3 ollama) 변경 0 (Phase 2A 결정 보존)
- documents / document_chunks 변경 0 (21365 docs / 30605 chunks 그대로)
- 4 smoke PASS (baseline / baseline+snapshot / cand_gte_ml_base / cand_invalid → 400)
- dispatch log 박제 verify (endpoint + snapshot id)

closure gate: 16 항목 PASS (flex closure 조항 적용 — 1 후보 측정, 2 후보 TEI 호환 탈락 사유 명시).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 08:37:42 +00:00
hyungi 3092e3009d feat(eval): Phase 2A Diagnose Phase 3+4 — dispatcher + 3 측정 + decision (H3 bge-m3 유지)
phase-2a-embedding-diagnose.md v4 § 6 (dispatcher) + § 7 Phase 3 (51 case 측정) + § 7 Phase 4 (decision)
Round 2 review: round-2-review-mighty-starfish.md (R2-2 + R2-B1 페어 invariant + slug-based resolve)

코드 변경:
- app/services/search/retrieval_service.py:
  - CANDIDATE_BACKEND_MAP allowlist (baseline / cand_me5_large_inst / cand_snowflake_l_v2)
  - _resolve_backend(slug) → docs_table/chunks_table/embed_endpoint or None
  - _embed_query_via_tei() — candidate TEI 엔드포인트 호출 (cache 미사용)
  - _VALID_DOCS_TABLE + _VALID_CHUNKS_TABLE regex (R2-B1 2단계 gate)
  - _search_vector_docs / _search_vector_chunks: docs_table/chunks_table + snapshot_*_id_max 파라미터
  - search_vector + search_vector_multilingual: embedding_backend + snapshot_*_id_max 파라미터 + dispatch log
- app/services/search/search_pipeline.py: run_search() 시그니처 + 4 search_vector* 호출 threading
- app/api/search.py: 3 Query parameter + ValueError → HTTP 400 (allowed list 응답)
- tests/search_eval/run_eval.py: --embedding-backend + --snapshot-doc-id-max + --snapshot-chunk-id-max
  + call_search/call_search_full/evaluate threading + main 3 asyncio.run threading

측정 산출물 (51 case, scored=46, failure=5):
- reports/v0_2_phase2a_baseline_snapshot_2026-05-23.csv (snapshot filter 적용 production path)
- reports/v0_2_phase2a_me5_large_inst_2026-05-23.csv
- reports/v0_2_phase2a_snowflake_l_v2_2026-05-23.csv
- tests/search_eval/baselines/v0_2_phase2a_{baseline_snapshot,me5_large_inst,snowflake_l_v2}_2026-05-23.json (3개)

결과:
| Candidate                          | NDCG | Δ vs baseline | mixed | korean_only | p50 ms |
|------------------------------------|-----:|--------------:|------:|------------:|-------:|
| bge-m3 (baseline snapshot)         | 0.659| —             | 0.39  | 0.51        | 464    |
| cand_me5_large_inst                | 0.477| -0.182        | 0.17  | 0.47        | 194    |
| cand_snowflake_l_v2                | 0.616| -0.043        | 0.35  | 0.52        | 254    |

Decision (H3): bge-m3 유지. 둘 다 net 회귀.
- mE5-large-instruct: 전 카테고리 회귀 (-0.182). prefix 미적용 변수 — 별 PR PR-2A-mE5-Prefix-Retry 후보.
- snowflake_l_v2: 가벼운 회귀 (-0.043). korean_only +0.01 미세 개선 신호.
- korean_only/mixed 약점 보완은 Phase 2B (Reranker) 또는 Phase 2Q (Query rewrite) 권고.

Decision report: reports/phase_2a_embedding_decision_2026-05-23.md (§ 1~8 포함, Closure gate 16 항목 모두 PASS).

후속 PR 백로그:
- PR-2A-mE5-Prefix-Retry (별 PR)
- PR-2A-Extended-Bge-Mgemma2 (별 PR, v3 결정)
- PR-2A-Cloud-Embedding-Scaffold-1 (Cohere/Voyage scaffold-only, 선택)
- PR-Search-Query-Rewrite-1 (Phase 2Q)
- PR-Search-Reranker-V2-Diagnose (Phase 2B)
- PR-2A-Chunks-Cand-Cleanup-1 (1주 후 cand 테이블 DROP)

production 영향:
- documents / document_chunks 컬럼/row 변경 0
- config.yaml 변경 0 (ollama bge-m3 unchanged)
- 추가된 endpoint = query parameter opt-in (미지정 시 production path 회귀 0)
- smoke 4건 PASS (baseline / baseline+snapshot / cand_me5 / cand_invalid → HTTP 400)
- dispatch log 박제 verify (snapshot_doc/chunk_id_max 박제)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 06:55:13 +00:00
hyungi bcf644f893 refactor(search): /api/search/ask dispatcher route via llm-router
PR-2 of DS AI routing policy (2026-05-23, see plan
~/.claude/plans/document-server-ai-cheeky-reddy.md +
memory project_document_server_ai_routing_policy).

DS 의 모든 backend 호출이 llm-router :8890 단일 경유. 정칙 정합:
- 신규 RouterBackend (services/llm/backends.py) — alias 별 router POST
  + requires_gate 분기 (mac-mini-default 만 llm_gate FOREGROUND 보호).
- 기존 GemmaMacMiniBackend + QwenMacBookBackend = legacy 보존
  (DS_BACKENDS_VIA_ROUTER=false rollback safety only). 1주 후 별
  cleanup PR (PR-DS-Backends-Legacy-Cleanup-1) 로 폐기.
- get_backend factory dual-path (env flag) — backward-compat
  (gemma-macmini alias → mac-mini-default 매핑).
- search.py:457 Query pattern 확장: mac-mini-default|claude-cloud|auto
  추가. /ask/react 의 isinstance(QwenMacBookBackend) → hasattr
  duck-typing (RouterBackend + Legacy 모두 generate_with_tools 구현).
- SearchAskBackendConfig 에 router_url 신규 (env LLM_ROUTER_URL 또는
  hardcoded MVP default http://100.76.254.116:8890).
- docker-compose.yml fastapi env 에 LLM_ROUTER_URL +
  DS_BACKENDS_VIA_ROUTER 추가.

AIClient (_call_chat, call_triage, call_primary, call_fallback) 경유
path 는 별 PR (PR-AIClient-Router-Migration-1) — MVP scope C 채택,
회귀 risk 최소화.

Closure (즉시 fixture/matrix):
- factory smoke 6 alias (None/mac-mini-default/gemma-macmini/
  qwen-macbook/claude-cloud/auto) + 1 invalid (nonsense → ValueError).
- live 3 case: mac-mini-default 200 \"pong! 🏓\" + qwen-macbook cold
  502 upstream_502_primary=ConnectError + claude-cloud 503
  provider_not_configured.
- silent fallback 0 + direct M5/Mac mini socket 0
  (RouterBackend 만 router 호출).

Backup: ~/.local/share/ds-routing-pr2-backups/20260523/
(backends.py + config.py + search.py + docker-compose.yml).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 03:41:29 +00:00
hyungi 51c3f6df10 feat(search): /ask/react endpoint with Qwen native tool calling ReAct loop
PR-DocSrv-Ask-ToolCalling-ReAct-1 — Qwen3.6-27B-8bit 의 native tool calling
으로 ReAct loop 도입. 기존 /api/search/ask 무수정. 트랙 B (frontend /ask SSE)
와 파일 단위 충돌 0 (search.py 의 ask() 함수 line diff = 0, 순수 추가).

핵심 invariant:
- 별 endpoint /api/search/ask/react (qwen-macbook only, implicit opt-in)
- MacBook unavailable 시 HTTP 503 + error_reason=macbook_unavailable.
  Gemma 자동 fallback X (정정 4 의 연장)

G0 (구현 전 hard gate, plan b-velvety-hare.md):
- G0-1 fixture (tests/fixtures/qwen_tool_call_response.json): 실제 mlx-vlm
  응답 박제. shape = OpenAI 표준 호환 (choices[0].message.tool_calls +
  function.arguments JSON string). generate_with_tools() 가 본 shape 기준 구현.
- G0-2 counter semantics: max_tool_rounds=2 + max_llm_calls=3 + search_exec_max=2.
  마지막 LLM 호출은 tool_choice="none" + system instruction 으로 final 강제.
- G0-3 trace exposure: default response 의 debug_trace=null. debug=true 시만
  채움. server log 에는 항상 round 기록.

backends.py (193 → 261줄):
- QwenMacBookBackend.generate_with_tools(messages, tools, tool_choice)
  신규 method. 기존 generate() 무수정. BackendUnavailable 처리 동일.

react_loop.py 신규 (275줄):
- agentic_ask_loop(session, query, *, backend, max_tool_rounds, debug)
- tool round 안에서 run_search 호출, results dedup by id, final round 강제,
  partial=True 조건 (final content 빈 경우)

search.py (+82줄):
- POST /api/search/ask/react + AskReactRequest/Response schema
- BackendUnavailable → JSONResponse(503, error_reason=macbook_unavailable)

config.yaml + config.py:
- search.ask.react: { enabled, max_tool_rounds=2, search_tool_limit=5,
  search_tool_mode=hybrid }

tests (566줄, 18 신규 + 23 회귀 모두 PASS):
- test_react_loop.py 13건: G0-1 fixture shape / G0-2 counter cap / G0-3 trace
  exposure / BackendUnavailable propagation / sources dedup
- test_search_ask_react_endpoint.py 5건: 503 + run_search 호출 0 / 정상 200 /
  debug=true trace 노출 / max rounds partial
- 회귀 (test_ask_eval_auth 9 + test_search_ask_macbook_503 5 +
  test_backend_dispatcher 9) 모두 PASS

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:43:47 +00:00
hyungi a7b8f15870 feat(search): /ask backend dispatcher (qwen-macbook opt-in, no silent fallback)
PR-MacBook-RAG-Backend-1 — /api/search/ask 의 명시 backend 선택 진입점.

핵심 invariant (정정 4):
- backend 미지정 = Gemma Mac mini default, 응답 contract 변동 0
- backend="qwen-macbook" 명시 opt-in 만 MacBook M5 Max mlx-vlm.server 호출
- MacBook unavailable 시 HTTP 503 + error_reason=macbook_unavailable
- 자동 fallback 절대 금지 — 실패 path 에서 Gemma backend.generate() 호출 0

backend dispatcher (services/llm/):
- BackendBase / GemmaMacMiniBackend / QwenMacBookBackend / BackendUnavailable
- Qwen backend 는 Mac mini llm_gate 점유 X, 별 Semaphore(1) — llm_gate
  docstring 의 single-inference 영구 룰은 같은 endpoint 한정으로 scope 명시
- httpx Connect/Read/Pool/Timeout/5xx → BackendUnavailable, 4xx 전파

synthesis_service.py:
- backend 인자 추가, status="backend_unavailable" 신규
- cache key 에 backend_name 포함 (qwen ↔ gemma 캐시 충돌 차단)

config:
- search.ask.backend.{macmini_url, macbook_url, macbook_model,
  timeout_connect_s=1, timeout_read_s=30}
- MacBook endpoint = http://100.118.112.84:8810 (M5 Max Tailscale bind)

tests (14 신규):
- tests/services/test_backend_dispatcher.py (9): dispatcher 정합성 + Qwen
  generate path (mock 200 / dead port / 5xx / 4xx) + cache identity
- tests/api/test_search_ask_macbook_503.py (5): 정정 4 핵심 invariant.
  backend=qwen-macbook 비가용 시 gemma.generate.assert_not_called()

기존 ask 회귀 0 (test_ask_eval_auth 9건 등 85건 모두 PASS).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 13:10:44 +00:00
Hyungi Ahn eae1f48d62 feat(worker-pool): Registry-1C cap 1MB + deterministic compaction
사용자 결정 2026-05-19: 100KB cap 이 운영 7d 데이터 1.36MB 대비 부족 →
cap 상향만으로 raw 비대화 위험. cap 1MB + payload compaction 병행.

fetch_recap_context() 변경:
- memo payload item field 축소 = id/title/ai_tldr/ai_event_kind/created_at (5 필드)
  (ai_bullets/file_type/source_channel/category/extracted_text 등 제외)
- memo top-N = RECAP_MEMO_TOP_N env (default 200) — 초과분은 aggregate 로
- aggregate = memos_by_day + memos_by_kind + omitted_memos
- payload_compacted flag = aggregate fallback 발현 여부
- events 는 raw (운영 7d 데이터에서 통상 0~소량)

internal_worker.py:
- PAYLOAD_MAX_BYTES → _payload_max_bytes() env override
  (WORKER_RECAP_PAYLOAD_MAX_BYTES default 1_000_000)
- JobsRecapResponse 에 payload_compacted / omitted_memos 노출
- 413 detail 에 "after compaction" 명시 + RECAP_MEMO_TOP_N 조정 안내

테스트 3 항목 신규 + 기존 endpoint 413 test 업데이트:
- 700 memo → 200 kept + 500 omitted + compacted=true + < 1MB
- 10 memo → compacted=false + omitted=0
- 비정상 큰 title (compaction 후에도 cap 초과) → 413 유지

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:55:51 +09:00
Hyungi Ahn 0ea72c1aa6 feat(worker-pool): Registry-1C recap context + /jobs/recap + 100KB guard
- app/services/worker_recap_context.py — fetch_recap_context(user_id, days)
  documents file_type='note' 7d (single-user invariant) + events 7d
  (user_id 매칭 + cancelled 제외) JOIN. timezone Asia/Seoul.
- /internal/worker/jobs/recap POST — 일반 user JWT 인증 + context 조립
  + worker_jobs INSERT. job_type='recap' + payload JSONB.
- payload 100KB guard — JSON 직렬화 100_000 bytes 초과 시 413.
- 회귀 위험 0: memos/events API select 절 touch 0, read-only 쿼리만.

worker-pool-policy §B.2 invariant 보존: ProcessingQueue 무변경, 운영 자동
분기 변경 0, canonical promote 0 (worker_jobs.payload JSONB only).

Notebook-Pilot-1 entry condition 4항목 모두 충족 가능:
manual recap E2E / payload <100KB guard / residue 0 / 권한 분리 403.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 12:44:07 +09:00
Hyungi Ahn f60d6e52fc feat(worker-pool): Registry-1B Pull 활성화 (auth + worker_jobs + 5 endpoint)
worker-pool-policy §B 1B 영역 완료. 1A scaffold (mig 270~274 + 503 stub) 위에:
- mig 275/276: worker_jobs (status CHECK + user_id=owner) + pending partial index
- create_laptop_worker_bot_token + require_worker_user dependency (voice-memo 동형)
- /internal/worker/{register,heartbeat,claim,result,drain} 5 endpoint 실 구현
- /claim FOR UPDATE SKIP LOCKED + 204 body 0
- /result 소유권 검증 (worker_id 매칭, 404) + failed 재시도 (attempts/max)
- explicit failure 시 request.result 무시 (DB result NULL 유지)
- 테스트 22 항목 7 파일

policy §B.2 5 invariant 보존: voice-memo wrapper 변경 0, drain advisory,
result raw JSONB, ProcessingQueue 무변경, 운영 자동 분기 변경 0.

활용처 (recap context + /jobs/recap + payload 100KB guard) = Registry-1C 영역.
stale recovery / 노트북 client / canonical promote = Notebook-Pilot-1 영역.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 08:54:07 +09:00
Hyungi Ahn bbd92a840a feat(worker-pool): Registry-1A scaffold — worker_capabilities/heartbeats + /internal/worker/* 5 endpoint 503 stub
PR-Worker-Pool-Registry-1A (scaffold only, no runtime activation).

신규:
- migrations/270~274 (1 statement/1 file 강제): worker_capabilities + 2 idx + worker_heartbeats + 1 idx
- app/models/worker_pool.py: WorkerCapability + WorkerHeartbeat ORM (queue.py 패턴)
- app/api/internal_worker.py: 5 endpoint 모두 _stub_503() — register/heartbeat/claim/result/drain
- tests/test_internal_worker_stub.py: 503 응답 smoke (inline ASGI client, DB 의존 0)

수정:
- app/main.py: import + include_router 각 1줄 (prefix=/internal/worker, internal_study 일관)

scaffold-first + phase-gate-material-first 강제 (worker-pool-policy §1, §12):
- 인증 dependency 0 (1B 에서 JWT + require_worker_user)
- ProcessingQueue 변경 0 (방향 b: worker_jobs 별 table = 1B)
- LLM 호출 0 / canonical DB 변경 0 / 운영 자동 분기 0

회귀 0 (1주 안전망 = app/main.py.pre-registry-1a.20260518).

plan: ~/.claude/plans/floofy-exploring-mitten.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 20:24:59 +09:00
hyungi 74876b674c feat(auth): JWT iat + users.password_changed_at invalidation (PR-Docsrv-JWT-Invalidation-1)
PR-Infra-Sec-1H Phase 0 audit 에서 DS jwt invalidation 정책 부재 확정.
password rotation 으로 구 365d JWT (voice-memo-bot 등) invalidate 안 되는
hard gate STOP 진입 → 선행 PR 분리.

- migration 269: users.password_changed_at timestamptz NULL (legacy 호환)
- create_access_token / create_refresh_token: payload 에 iat (int 초) 추가
- verify_password_changed_at helper: int(password_changed_at.timestamp()) > int(iat) 시 401
- get_current_user + refresh_token route: verify helper 호출
- change_password / setup signup / seed_admin INSERT+UPDATE: password_changed_at 갱신

NULL = 검증 skip (migration 직후 운영 영향 0). 첫 password 변경 후만 iat
검증 활성. Sec-1H 의 G-token-old hard gate 통과 path 확보.
2026-05-17 06:20:46 +00:00
Hyungi Ahn a08b620894 refactor(search): swap 10 call sites to acquire_mlx_gate(Priority.*) (B-1)
DS-Mac-mini-26B-Priority-Gate-1 — 사용자-facing 7 + worker 3 = 10 site 의
`async with get_mlx_gate():` → `async with acquire_mlx_gate(Priority.*):` 교체.

Foreground 6 (user-facing path):
- app/services/search/evidence_service.py:315 (/ask evidence stage)
- app/services/search/classifier_service.py:103 (/ask classifier stage)
- app/services/search/synthesis_service.py:299 (/ask synthesis stage)
- app/api/documents.py:1306 (수동 analyze API)
- app/api/study_topics.py:1183 (subject note 동기 생성)
- app/api/study_questions.py:1560 (study explanation 동기 API)

Background 4 (worker queue / fire-and-forget):
- app/services/search/query_analyzer.py:240 (V0 grep 확인: fire-and-forget only,
  search_pipeline.py:179 trigger_background_analysis 만, docstring rule
  "analyze() 동기 호출 금지" 부합 → BACKGROUND 확정)
- app/workers/deep_summary_worker.py:110 (classify-escalate worker)
- app/workers/study_explanation_worker.py:149
- app/workers/study_session_analysis_worker.py:237

Cleanup:
- query_analyzer._get_llm_semaphore() 제거 — self-only, unused, signature 거짓말
  (이제 get_mlx_gate 가 Semaphore 아닌 context manager 반환)

기존 get_mlx_gate() legacy wrapper 는 보존 (BACKGROUND 매핑). user-facing path
잔재 0 — closure gate grep 검증 통과 (별 commit 에서).
2026-05-17 08:51:57 +09:00
Hyungi Ahn 73f328cb65 fix(search): DS RAG LLM_TIMEOUT_MS align 15s/3s → 30s/10s (B-3 Synthesis-Timeout-Calibration-1)
PR-Hermes-Docsrv-Search-1 closure 측정 (synthesis_ms=30~48s / ev_ms=15005 /
query_analyze 45s) 으로 15s LLM_TIMEOUT 빈발 timeout 확인. Mac mini 26B 동시
호출 (gate Semaphore 1 직렬화 후에도 evidence + synthesis + classifier +
query_analyzer + verifier 가 sequential 누적) 시 각 호출 30s 까지 필요.

5곳 변경:
- synthesis_service.LLM_TIMEOUT_MS 15000 → 30000
- evidence_service.LLM_TIMEOUT_MS 15000 → 30000
- verifier_service.LLM_TIMEOUT_MS 3000 → 10000
- query_analyzer.LLM_TIMEOUT_MS 15000 → 30000
- search.py:522 classifier wait_for 15.0 → 30.0 (classifier_service align)
- search.py:641 verifier wait_for 4.0 → 10.0 (verifier_service align)

classifier (이전 PR 에서 30s 로 align 완료) 와 동일 정책 — outer wait_for
가 inner LLM_TIMEOUT_MS 를 override 하지 않도록 align.

ask 응답 latency 상한 ↑ 의도된 trade-off — 안정성 (refusal_gate
conservative_refuse 회피 + grounding/verifier 정상 동작) 우선.

영향: PR-1 fixture 회귀 0 예상 (이전 timeout 이 새 한도 안). B-1 Throughput-1
(priority queue / 모델 분리) 별 PR 진입 시 latency 본격 단축 검토.
2026-05-17 08:01:22 +09:00
Hyungi Ahn 5846baedc7 fix(search): ask classifier wait_for 6s → 15s (outer wrapper override 해소)
A1 (LLM_TIMEOUT_MS 5→15→30) + config(10→15→30) 후속 진단: 8/10 fixture query 가
"classifier ok" 또는 "classifier error" 로그 없이 conservative_refuse(no_classifier)
경로. search.py:518 의 outer wrapper `asyncio.wait_for(classifier_task, timeout=6.0)`
가 classifier_service.LLM_TIMEOUT_MS 와 httpx timeout 모두 override.

6s 한계 → 동시 부하 시 거의 모든 classifier 호출 6s 안에 못 끝남 → AsyncIO TimeoutError
→ ClassifierResult("timeout") → refusal_gate 가 verdict=None 받아 conservative_refuse.

15s 로 상향 — classifier_service 내부 30s 와 align 하지 않은 이유 = ask 응답 시간 상한
유지 (evidence parallel 종료 후 추가 9s 대기 cap). Mac mini 26B 동시 부하 시 실측
elapsed 11-14s 까지 자주 발생 → 15s 가 합리 균형.

본 fix 가 진짜 closure 효과. PR-Hermes-Docsrv-Search-1 Layer 1 fixture 의 8/10
no_classifier 경로 해소 예상.
2026-05-16 19:46:49 +09:00
Hyungi Ahn 19bf5b1e38 feat(memo): Hermes input gateway — source_channel='hermes' + source_metadata jsonb
PR-Hermes-Docsrv-Bridge-1 v1. Hermes Agent (Mac mini Discord) 를 Document Server
입력 게이트웨이로 reframe — 코딩 executor X, Claude Code 변동 0.

변경:
- migration 267: source_channel enum 에 'hermes' 추가
- migration 268: documents.source_metadata jsonb NOT NULL DEFAULT '{}' 추가
- Document model: source_metadata 컬럼 ORM 매핑 + enum 'hermes' 노출
- MemoCreate: source_channel + source_metadata 필드 수용 (default='memo' 호환)
- create_memo: channel allowlist (memo/voice/hermes) + metadata jsonb 저장
- list_memos: IN tuple 에 'hermes' 추가 (inbox 노출)
- MemoResponse + _to_memo_response: source_metadata 노출 (UI 배지 준비)

LLM 호출 0 — Hermes 의 HTTP POST 만. 분류/요약은 classify_worker 비동기 처리.
promote-to-event guard (562/664) 변경 0 — v1 = hermes 메모 promote 차단 유지.

plan: ~/.claude/plans/idempotent-seeking-hollerith.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-16 13:44:15 +09:00
hyungi 5125f82d4a feat(study): Mac mini derived-worker (PR-MacMini-Derived-Worker-1)
GPU = RAG context provider, Mac mini = LLM 가공 공장.

GPU 측 변경:
- app/api/internal_study.py: GET /internal/study/explanation-context/{qid}
  Bearer auth, gather_explanation_context + _render_envelope_prompt 재호출.
  204=evidence missing, 410=deleted/ready.
- app/workers/study_queue_consumer.py: settings.study_explanation_enabled
  false 시 explanation 분기 skip (status/attempts 미변경, pending 유지 → Mac mini 흡수).
- app/core/config.py: study_explanation_enabled + internal_worker_token 2 setting.
- app/main.py: internal_study_router include (prefix /internal/study).
- docker-compose.yml: fastapi ports → 100.110.63.63:8000 Tailscale bind,
  STUDY_EXPLANATION_ENABLED + INTERNAL_WORKER_TOKEN env 추가.

Mac mini 측: ~/derived-worker/ (별도 push 0, 어제 작성).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 03:13:43 +00:00
Hyungi Ahn 52f86acda7 feat(auth): voice-memo bot 365d access token (PoC v1)
bot 계정(`voice-memo-bot`) 한정 long-expiry access token 발급 경로 추가.
일반 사용자 흐름 영향 0 (env gate default false).

- core/auth.py: create_voice_memo_bot_token() 신규 (env gate + username hard-match)
- api/auth.py: login route 에 bot 분기 (bot 이면 long token 반환, 일반은 기존 흐름)
- docker-compose.yml: 3 env (VOICE_MEMO_BOT_TOKEN_ENABLED/_USERNAME/_EXPIRE_DAYS) default false

OpenClaw `/voice-memo` plugin → DS `/memos/` Bearer proxy 의 auth 기반.
정식 service-account/api_keys 테이블은 Phase 2 (multi-service 인입 추가 시점).

plan: ~/.claude/plans/rosy-launching-otter.md
project: ~/.claude/projects/-Users-hyungiahn/memory/project_voice_memo_pipeline.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 12:24:18 +09:00
hyungi 4b8120d83f feat(briefing): date picker + 카드별 읽음/하이라이트 액션
사용자 요청 (2026-05-13):
- 오늘 briefing 만 보여주고 과거 못 보는 게 아쉬움 → 날짜 선택 UI
- 시간대 별 나열은 오히려 불편 → date dropdown 1단계 선택
- 각 카드에 읽음/하이라이트 토글

Schema (migrations 263~266, 단일 statement):
- briefing_topics.is_read BOOL NOT NULL DEFAULT false
- briefing_topics.read_at TIMESTAMPTZ
- briefing_topics.highlighted BOOL NOT NULL DEFAULT false
- briefing_topics.highlighted_at TIMESTAMPTZ

API (app/api/briefing.py):
- TopicResponse 에 id / is_read / read_at / highlighted / highlighted_at 추가
- GET /api/briefing/dates → 사용 가능 날짜 목록 (60일 cap)
  · briefing_date / total_topics / total_articles / status / read_count / highlighted_count
- PATCH /api/briefing/topics/{id}/read body {value: bool} → 읽음 토글
- PATCH /api/briefing/topics/{id}/highlight body {value: bool} → 하이라이트 토글
- 토글 시 *_at 컬럼 자동 설정/NULL

UI (frontend/src/routes/news/+page.svelte):
- 헤더 우측 <select> date dropdown — 최신 + N일치 (highlighted_count 별 표시)
- 선택 시 /api/briefing?date=… 로 해당 날짜 briefing 로드
- 카드 우측 상단 ★ (하이라이트) + 읽음 버튼
- 하이라이트 = Card class ring-2 ring-yellow-400
- 읽음 = 외부 div class opacity-60 (시각 차분화, 펴기 가능)
- 토글 즉시 PATCH 호출 + 로컬 state 갱신

each key topic.topic_rank → topic.id 변경 (이미 unique).
2026-05-12 22:05:06 +00:00
Hyungi Ahn 431d4fe010 feat(briefing): add morning briefing schema + services + api (historical off)
야간 수집 뉴스 (KST 00:00~05:00) topic×country 비교 분석 1페이지 카드.
Phase 4 Global Digest 와 코드/로직/테이블 분리, 알고리즘만 services/clustering_common 공유.

Backend 신규:
- migrations/255_morning_briefings.sql: morning_briefings + briefing_topics
  (briefing_date UNIQUE, UNIQUE(briefing_id,topic_rank), FK CASCADE,
  historical_* 3컬럼 nullable, cluster_members JSONB, country_perspectives
  JSONB, status 4-state success|partial|failed|empty)
- app/models/briefing.py: SQLAlchemy ORM
- app/services/briefing/loader.py: KST 5h 윈도우 + news_sources prefix
  fallback (Phase 4 패턴 미러) + historical candidate pool 로더
- app/services/briefing/clustering.py: cluster_global topic-first
  (LAMBDA=ln(2)/2h, MIN_COUNTRIES_PER_TOPIC=2, MAX_TOPICS=7)
- app/services/briefing/comparator.py: call_primary 26B + JSON envelope
  sanitize (cap perspectives 10 / divergences 3 / convergences 2 /
  quotes 5) + fallback row 고정 형태 + retrieve_historical cosine top-K
- app/services/briefing/pipeline.py: load→cluster→select(K=7,λ=0.6)
  →historical→compare→status 4-state→delete+insert transaction
- app/workers/briefing_worker.py: APScheduler/수동 호출 공용 진입점,
  600s hard cap
- app/prompts/briefing_comparative.txt: 한국어 비교 분석 JSON 프롬프트,
  {articles_block} + {historical_block} 2섹션, 인용 금지 라벨
- app/api/briefing.py: GET /latest, GET ?date=, POST /regenerate?date=
  (admin, sync delete+insert tx, regenerated:true)

Backend 수정:
- app/main.py: briefing_router 등록 (/api/briefing prefix). scheduler
  등록은 PR-3 에서.
- app/services/digest/selection.py: select_for_llm 매개변수화 (K, λ
  caller 주입). Phase 4 동작은 default 값으로 보존.

Historical 정책:
- BRIEFING_HISTORICAL_ENABLED env flag, default off.
- flag off → historical_* 컬럼 모두 NULL, prompt {historical_block} 빈
  라벨, retrieval 호출 안 함.
- flag on (PR-1b 에서 enable) → cluster centroid 와 과거 30일 doc
  embedding cosine top-K 5 (sim≥0.70), prompt 에 주입.

Country canonical (실측 확인 후):
- documents.country 컬럼 부재 확정
- document_chunks.country 매칭률 0% (chunks 자체가 뉴스에 안 만들어짐)
- 유일 country 신호 = news_sources prefix 매핑 (Phase 4 와 동일)

Tests:
- tests/test_briefing_historical.py: 3 경로 회귀 (flag off/on with
  fixture/on zero match) + sanitize cap + fallback row 형태.

Verification: PR-1.8 에서 GPU 컨테이너 pytest + 수동 regenerate.
2026-05-12 12:58:50 +09:00
Hyungi Ahn 3dc78e4f94 fix(memos): voice memo file_type → 'immutable' (doc_type enum 호환)
GPU 서버 main pull 후 /api/memos/?archived=false 가 500 — doc_type enum 에
'audio' 값 없음 (immutable/editable/note 만). list_memos WHERE file_type IN
('note', 'audio') 가 invalid_text_representation.

수정:
- voice upload Document.file_type = 'audio' → 'immutable' (기존 audio 컨테이너
  인입과 같은 패턴: file_type='immutable' + category='audio' + source_channel='voice')
- list_memos 필터에서 file_type 조건 제거 (source_channel IN ('memo','voice') 만으로
  분리 — file_type='immutable' 필터는 일반 PDF 까지 끌어옴, 위험)
- module docstring + voice upload 주석 업데이트

원본 plan 의 file_type='audio' 결정은 doc_type enum 미확인이 원인.
enum 확장(ALTER TYPE ADD VALUE 'audio') 대신 기존 패턴 재사용 — 안전 + 회귀 X.
2026-05-11 12:28:58 +09:00
Hyungi Ahn 6490050b04 feat(memos): promote memo to event + voice memo upload endpoint
PR-2B/2C backend 2/2. plan v9 commit 분할 2~3 통합 (memos.py 단일 파일 변경).

PR-2B promote-to-event:
- POST /api/memos/{memo_id}/promote-to-event — 메모 → events 1-click 승급
  · kind 결정: body.kind > documents.ai_event_kind > 400
  · activity_log 면 status=done + ended_at=now() 자동 (5초 행동 기록 UX)
  · calendar_event + start_at 있으면 status=scheduled
  · Event row + events_history(create) 자동 생성
  · memo_document_id 자동 link + source='memo' + raw_metadata 에 AI 추천값 보존
  · 한 메모 → N events 가능 (사용자 의도에 따라 dedup 없음)
- POST /api/memos/{memo_id}/dismiss-event-suggestion — '그냥 메모' (ai_event_kind='note' 강제)
  · MVP: AI 추천값과 사용자 확정값 같은 컬럼 (정확도 측정 흐려질 수 있음)
  · 백로그: user_event_kind 별 컬럼 분리 (plan Memo Intake Upgrade 백로그)
- MemoResponse 확장: ai_event_kind / ai_event_confidence / source_channel / file_type / file_path
- list_memos 필터 완화: file_type IN (note, audio) + source_channel IN (memo, voice)
  → voice 메모도 같은 inbox list 에 표시 (사용자 의도: 메모 = 모든 입력의 inbox)

PR-2C voice upload:
- migration 254: ALTER TYPE source_channel ADD VALUE 'voice'
- POST /api/memos/voice (multipart audio + recorded_at + device_hint)
  · 검증: Content-Type audio/* + size ≤ 50MB + 확장자 화이트리스트
  · NAS 저장: /documents/PKM/Recordings/{YYYY-MM}/{uuid}.{ext}
  · fsync + rename(atomic) 패턴 (NAS soft mount 안전)
  · Document row: file_type='audio' + source_channel='voice' + category='audio'
  · enqueue stt 큐 → 기존 stt_worker → classify (PR-2B triage) → embed → chunk
  · extract_meta 에 device_hint / recorded_at 보존
- 응답: MemoResponse (file_path 포함, frontend audio player 용)

원칙: AI worker 는 events row 직접 생성 X. 본 endpoint 가 사용자 의도 channel.
2026-05-11 12:06:41 +09:00
Hyungi Ahn 6d71116553 feat(events): PR-2 UI MVP — 4-tab + 빠른 행동 기록 + 상세/생성/이력
plan v6 PR-2 scope. 5초 행동 기록 UX 가 핵심 가설.

Backend:
- GET /api/events/{id}/history — events_history timeline 조회 (lifecycle op 자동 기록)

Frontend (SvelteKit 5 runes mode):
- /events 메인 — 4-tab (오늘/Inbox/예정/활동) + 빠른 행동 기록 widget
  · 단일 입력 + Enter → POST /api/events kind=activity_log
  · status=done + 시간 default 채워짐 (서버 측) → Activity 탭 즉시 반영
  · 새 항목을 list 최상단 prepend (refetch 불필요)
  · 연속 입력 위해 입력 ref focus 유지
  · lifecycle 버튼 (complete/defer/cancel/reactivate) — activity_log 는 lifecycle 대상 X
- /events/[id] 상세 — PATCH 허용 필드 edit (title/desc/시간/priority/project_tag) + history timeline
  · PATCH 금지 필드는 UI 노출 X (status/completed_at/cancelled_at/defer_until 은 별 버튼)
- /events/new — kind 선택 (task/calendar_event/activity_log) 후 필드 분기 form
  · task: due_at + start_at (선택, "14:00 전화" 같은 시각 task 허용 — 라운드 10)
  · calendar_event: start_at 필수 + end_at + all_day
  · activity_log: started_at/ended_at 비우면 서버 default now()
- Sidebar 메모 옆에 events 진입점 (CalendarCheck icon)

API helpers: frontend/src/lib/utils/events.ts (createEvent / logActivity / list*
/ lifecycle ops / kind&status enum label/color).

quickref doc: docs/events_api_quickref.md (이전 commit, PR-2 frontend reference).

PR-2 핵심 가설 검증 = 빠른 입력 → 저장 → Activity 즉시 반영 → 새로고침 유지.
PR-1 deferred HTTP behavior 5건도 본 UI 의 자연 사용으로 닫힘.
2026-05-11 07:56:31 +09:00
Hyungi Ahn 9d9b3359b0 feat(events): PR-1 Events Core — schema + ORM + 최소 API
개인 운영 로그 / 일정 / 할 일 / 회고용 1차 컨테이너 도메인 신설.
plan: ~/.claude/plans/beszel-tingly-sloth.md (라운드 12 v6).

Schema:
- enum 5종 (event_kind / event_status / event_source / event_actor / history_change_kind)
- events 테이블: kind(task|calendar_event|activity_log) + lifecycle 7-state status
- events_history: lifecycle op 자동 기록, FK RESTRICT (이력은 시점 사실)
- CHECK: calendar_event → start_at NOT NULL / activity_log → started_at|ended_at NOT NULL
- partial unique (source, source_ref) — 외부 source dedup (PR-4 활용)
- partial index (active status / activity_log timeline)

API:
- POST /api/events (kind=activity_log shortcut: status=done + ended_at=now() default)
- GET /api/events/{id} | /api/events?kind&status&from&to&project_tag&source
- PATCH /api/events/{id} (extra=forbid + 시간 필드 변경 시 reschedule history)
- POST /api/events/{id}/{complete,cancel,defer,reactivate} (history 자동)
- GET /api/events/today (Asia/Seoul default, deferred 는 defer_until<=now() 만)
- GET /api/events/inbox | /api/events/activity?from&to

제외 (PR-2~5 또는 백로그):
- DELETE (회고 데이터 → /cancel 일관화)
- log shortcut / upcoming endpoint (POST + GET ?from&to 로 흡수)
- /ingest (PR-4 MailPlus forward 시 정확한 요구로 추가)
- iCal export / ntfy 알림 / recurrence / 일반 edit history
2026-05-11 07:19:04 +09:00
Hyungi Ahn 8ca27eb573 fix(markdown): img auth via ?token= query param (Authorization header 미지원)
`<img src=>` 가 Authorization header 를 못 보내서 /api/documents/{id}/images/{key}/raw
가 401 반환 → 이미지 안 보임. 기존 /file?token= iframe 패턴과 동일하게 access token
쿼리 파라미터로 전달.

backend: get_current_user 의존성 제거하고 token 쿼리 파라미터 직접 검증 (기존 /file
엔드포인트와 동일 흐름).

frontend: MarkdownDoc 의 swap selector 가 img.src 에 ?token={getAccessToken()} 부여.
로그아웃 상태면 placeholder 유지.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:47:09 +09:00
Hyungi Ahn 68fa86ea52 feat(markdown): persist extracted images with auth routes
Markdown Canonical Phase 1B.5 — marker 가 추출하던 이미지를 NAS 에 영구 저장하고
DB 메타 + 인증 라우트 + 프론트 swap 까지 wiring.

핵심 변경:
- marker-service /convert 응답에 base64 image 리스트 포함 (stateless 유지, NAS write 권한 X)
- marker_worker 가 NAS `/documents/extracted_images/{doc_id}/` 에 persist + UPSERT +
  고아 row DELETE + md_content ref 를 `docimg:img_NNN` stable scheme 으로 정규화
- /api/documents/{id}/images/{key}/raw 인증 라우트 (Cache-Control private + ETag = content_hash)
- frontend MarkdownDoc 가 placeholder card 안의 docimg ref 를 실제 <img> 로 swap

원칙:
- 이미지 binary = NAS, metadata = Postgres (학습 섹션 패턴 동일)
- image_key sequence 기반 결정적 → 재변환 idempotent
- MARKDOWN_IMAGE_PERSIST=false env 로 rollback 가능 (placeholder card 폴백 자연 유지)

기존 28건 marker success 문서는 본 PR 에서 건드리지 않음 — deploy + 신규 업로드 1건 +
sample 5건 검증 후 scripts/marker_reprocess_existing_success.py 로 targeted reprocess.

plan: ~/.claude/plans/piped-humming-crystal.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 14:05:41 +09:00
Hyungi Ahn 5185501bbd feat(search): PR-RAG-Time-1 freshness decay (news/law_monitor)
뉴스/법령 알림 retrieval 결과에 시간 가중치 soft multiplier 적용.
reranker 이후 final score 합성 단계에서 운영 정책 단계로 분리.

- news (source_channel='news'): half-life 90일
- law_monitor (source_channel='law_monitor'): half-life 365일
- 비적용: manual / drive_sync / inbox_route / memo / Manual / Reference /
  Academic_Paper / Checklist / KGS Code / Study / content_origin='ai_drafted'
- formula: decay = exp(-ln(2) * age / HL); final = base * (0.7 + 0.3 * decay)
- floor 0.7 (완전 demote 금지)
- 가드: missing date / future date / unknown source 모두 no-op
- 임시 date source: documents.created_at (published_date 컬럼 부재 — 후속 PR)

debug 메타 (?debug=true 응답 + logs/search.log):
  base_score / age_days / decay_factor / freshness_adjusted_score /
  freshness_policy / freshness_date_source

신규: app/services/search/freshness_decay.py
hook: app/services/search/search_pipeline.py:303 (apply_diversity 직후, normalize 직전)
schema: app/api/search.py SearchResult.freshness_debug (Optional[dict])
tests: tests/test_freshness_decay.py 24 case (정책 디스패처 9 + age/decay/score 11 + apply integration 6 — guard 1~6 all)

Episode/Fact layer 와 contradiction detection 은 본 PR 스코프 외.
plan: ~/.claude/plans/pr-rag-time-1-freshness-decay.md
2026-05-03 08:38:09 +09:00
Hyungi Ahn 6785d53d3d feat(study): Phase 4-B v1 세션 단위 종합 분석 (자유 마크다운)
Phase 4-A 가 wrong/unsure 한 문제씩 풀이 캐시. 4-B 는 세션 전체 wrong/unsure
5~30건을 묶어 200~400자 자연어 요약 1건 생성. 결과 화면 헤더 카드.

큐 인프라는 4-A study_question_jobs 와 분리 — FK 단일 의미 + 운영 SQL 명확성
+ 4-A/4-B 가드/payload/재시도 정책 차이. 신규 study_quiz_session_jobs (큐) +
study_quiz_session_analysis (결과 캐시 PK=session_id, UPSERT) + 전용 consumer.

Backend:
- migrations/233 — study_quiz_session_jobs (FK study_quiz_sessions NOT NULL,
  status pending/processing/completed/failed/skipped, max_attempts=2)
- migrations/234 — partial unique idx (session_id) WHERE pending/processing
- migrations/235 — study_quiz_session_analysis (session_id PK, summary_md,
  confidence, model_name, generated_at, is_stale)
- models/study_quiz_session_job — ORM + enqueue_session_analysis_job() (멱등)
- models/study_quiz_session_analysis — ORM (PK = session_id)
- services/study/session_summary_guard — GUARD_PATTERN (정규식) +
  normalize_confidence() 단일 source, worker + tests 가 import 공유
- services/study/session_summary_rag — gather_session_summary_context()
  documents 만 (PR-3 _gather_document_evidence 재사용). evidence 없어도 호출
  허용 (4-A 와 다른 정책 — 세션 기록 자체가 evidence)
- services/study/session_analysis_enqueue — auto (finalize/fallback) +
  request_session_analysis_regenerate (manual). manual 은 wrong/unsure < 5
  즉시 차단, active job 차단, 기존 analysis 있으면 is_stale=true 박기
- prompts/study_session_summary_envelope.txt — envelope JSON
  {summary_md, confidence}. 정량 정수만 인용 가능, 비율/추세/범위/날짜 금지
- workers/study_session_analysis_worker — terminal status 분기:
  · wrong/unsure < 5 → status=skipped, error_code=insufficient_attempts
  · question_text/outcome 부족 → skipped, evidence_missing
  · GUARD_PATTERN match → failed, guard_fail
  · 800자 hard cap + confidence normalize
  · timeout/parse/unknown → 재시도 후보
  · UPSERT study_quiz_session_analysis ON CONFLICT DO UPDATE (PK session_id)
- workers/study_session_queue_consumer — 4-A consumer 패턴 복제. BATCH_SIZE=1
  + STALE_MINUTES=10. MLX gate 4-A 와 공유 (Semaphore(1))
- main.py — APScheduler add_job(consume_study_session_queue, ..., 1분 주기)
- session_finalize — 끝에서 enqueue_session_analysis_auto (best-effort)
- api/study_topics:
  · QuizSessionAnalysisOut + ai_session_analysis 응답 필드 (analysis row +
    최신 job status/error_code)
  · GET fallback enqueue (기존 analysis 또는 active job 없으면만, non-blocking)
  · POST /quiz-sessions/{sid}/regenerate-summary — manual 트리거

Frontend (quiz-sessions/[sid]/+page.svelte):
- 결과 헤더에 세션 요약 카드 (AI 풀이 indicator 직후, 바로 할 일 직전)
- summary_md 박혔으면 markdown 렌더, 없으면 job_status / error_code 분기:
  · pending/processing → "AI 가 세션 분석 중"
  · insufficient_attempts → "오답·모르겠음 5건 미만"
  · evidence_missing → "자료 부족"
  · guard_fail → "환각 검증 차단" + 재생성 링크
- confidence='low' 배지 + is_stale "재생성 중" 배지
- 재생성 버튼 + regenerateSummary() — reason 별 toast 분기

ship gate:
- tests/test_session_summary_guard_pattern.py — 허용 5 + 차단 7 케이스 +
  normalize_confidence 표준/비표준 검증. python3 직접 실행 패스.

Plan: ~/.claude/plans/nifty-sparking-spindle.md (Phase 4-B v1)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-02 07:20:29 +09:00
Hyungi Ahn c7630b9815 feat(study): Phase 4-A 결과 화면 inline indicator — AI 풀이 진척 노출
결과 화면에서 사용자가 [AI 해설 보기] 누를 때 캐시 hit/miss 가 불투명함.
헤더에 한 줄 indicator 추가 — 오답·모르겠음 대상 N건 중 ready 박힌 카운트
+ 진행 중/실패/자료 부족 분포.

Backend (study_topics.py get_quiz_session):
- questions[i].ai_explanation_status 응답에 추가 (q.ai_explanation_status 그대로)
  · frontend 가 attempts.outcome (wrong/unsure) 와 결합해 카운트

Frontend (quiz-sessions/[sid]/+page.svelte):
- $derived aiExplProgress — wrong/unsure attempts 와 question.ai_explanation_status
  결합 카운트 (target / ready / pending / failed / skipped)
- 헤더에 Sparkles 아이콘 + "AI 풀이 자동 생성: N/M (P%)" 한 줄
  · pending > 0: "생성 중 N" (warning 색)
  · failed > 0: "실패 N" (error 색)
  · skipped > 0: "자료 부족 N" (dim)
  · 셋 다 0인데 ready < target: "대기열 처리 대기" (worker 1분 주기 안내)

이 indicator 는 GET fallback enqueue 와 함께 작동 — 결과 화면 진입 시점에
backfill 이 누락된 wrong/unsure 가 이미 enqueue 되고, 1분 주기로 ready 박힘.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:15:35 +09:00
Hyungi Ahn 3db5d331de feat(study): Phase 4-A 운영 가시화 — 통계 대시보드 AI 풀이 카드
Phase 4-A 가 wrong/unsure 풀이를 background batch 로 캐시하는데, 사용자/운영자
입장에서 (1) 지금까지 얼마나 캐시 채워졌는지, (2) 환각 차단/파싱 실패/자료 없음
같은 worker 결과 분포를 볼 수 없었음. 통계 대시보드에 카드 추가.

Backend (study_question_progress.py /stats):
- StatsAiExplanation 신규 응답 섹션
  · status_distribution — 토픽 전체 study_questions.ai_explanation_status 분포
    (none/ready/failed/skipped/stale/pending 6 키 default 0)
  · target_total / target_ready — wrong/unsure progress 의 ready 비율
    (캐시 hit 가능성 추정 핵심 지표)
  · recent_jobs — 최근 7일 study_question_jobs 의 (status, error_code) 분포
    ('completed', 'failed:guard_fail', 'failed:parse_fail', 'skipped:evidence_missing'
    같은 합성 키)

Frontend (/study/topics/[id]/stats):
- 신규 Card "AI 풀이 캐시" — Sparkles 아이콘
  · 큰 숫자 + 진행률 바: ready / wrong+unsure
  · 토픽 전체 status 분포 inline (한국어 라벨)
  · 최근 7일 worker 결과 grid (환각 차단 / 파싱 실패 / 자료 없음 skip 등 분리)
- statusLabel / jobLabel 헬퍼 — 운영자 친화 한국어

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:59:20 +09:00
Hyungi Ahn e8da53490c feat(study): Phase 4-A wrong/unsure AI 풀이 prefetch batch
PR-3 의 결과 화면 [AI 해설 보기] 실시간 호출이 클릭 시 8~30초 대기. 풀이 직후
백그라운드 batch 로 미리 생성해 캐시 hit. 환각 가드는 PR-3 보다 강화 — envelope
JSON {answer_choice, explanation_md, confidence} + answer_choice == correct_choice
검증 + evidence 의무.

processing_queue 가 documents.id FK 라 study_questions 에 직접 재사용 불가 →
별도 study_question_jobs 테이블 + 별도 consumer.

Backend:
- migrations/231 — study_question_jobs CREATE TABLE (13컬럼, kind 권장값
  'explanation' / 'session_summary' 예약, status pending/processing/completed/
  failed/skipped, max_attempts=2)
- migrations/232 — partial unique idx (qid, kind) WHERE status IN
  (pending, processing) — active 행 중복 차단, terminal 이력 누적 허용
- models/study_question_job — ORM + enqueue_study_question_job() 헬퍼
  (on_conflict_do_nothing 멱등)
- prompts/study_explanation_envelope.txt — envelope 형식 프롬프트
  (answer_choice 1~4 강제, confidence high/medium/low)
- workers/study_explanation_worker — terminal status 분기:
  · evidence 둘 다 빈 리스트 → job/question 모두 skipped (LLM 호출 X)
  · answer_choice != correct_choice → guard_fail / failed (재시도 X)
  · timeout/parse → 재시도 후보 (max_attempts=2)
  · catch-all except → unknown 명시 + retryable 분기
  · question.ai_explanation_status='ready' 이미 박혀있으면 즉시 completed
  · confidence 는 job.payload 에 보존 (운영 분석)
- workers/study_queue_consumer — APScheduler 1분 주기, BATCH_SIZE=1, MLX gate
  Semaphore(1) 공유. STALE_MINUTES=10 자체 복구
- main.py — scheduler.add_job(consume_study_queue, ..., id='study_queue_consumer')
- services/study/explanation_enqueue — finalize + GET fallback 공유 헬퍼:
  filter_needs_explanation (study_questions status + 최신 job error_code 필터,
  guard_fail/evidence_missing 인 마지막 job 은 자동 재enqueue 제외) +
  enqueue_explanation_for_qids (max_count cap)
- session_finalize — 끝에서 wrong/unsure qid prefetch enqueue (best-effort,
  실패해도 finalize 자체 안 깨짐)
- api/study_topics get_quiz_session — done 세션에서 backfill enqueue (max=30,
  non-blocking, debug 로그)

대상 조건: ai_explanation_status IN ('none', 'failed') OR ai_explanation IS NULL.
stale / skipped / pending / ready 는 자동 enqueue 대상 X. stale 재생성은 PR-3
명시 [다시 생성] 또는 후속 Phase 에서.

Plan: ~/.claude/plans/nifty-sparking-spindle.md (Phase 4-A)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 11:42:08 +09:00
Hyungi Ahn 711b81f8f0 feat(study): Phase 2-F due_at 정체 정리 — overdue redistribute
사용자가 며칠 안 들어오면 due_today 가 누적되어 학습 페이스 압박. Phase 1
plan 위험 항목 처리. 자동 batch 대신 사용자 명시 액션으로 통제권 보장.

Backend:
- POST /study-topics/{tid}/review-queue/redistribute — overdue 를 round-robin
  분산. days_offset = i % spread_days + 1 (오늘 + 1~7일). 같은 날 안에서도
  i*7분 spread 로 시간 분산. review_stage 는 보존 (재배치만, stage 리셋 X).
  body { spread_days: 1~14, default 7 }. 응답 { redistributed_count, spread_days }.
- GET /review-queue?tab=due_today 응답에 overdue_count: int 옵션 필드 — UI 가
  경고 + [정리] 노출 판단. due_at < today 0시 (UTC) + stage<4 카운트.

Frontend (review-queue):
- due_today 탭에서 overdue_count>0 시 노란 banner — "정체 N건" + [정리] 버튼.
- 정리 클릭 → confirm → POST → toast (N건을 7일에 분산) → 카운트/목록 reload.
- 다른 탭에서는 banner 미노출 (backend 가 overdue_count=0 응답).

Plan: ~/.claude/plans/crispy-petting-dijkstra.md (Phase 2-F)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 10:48:00 +09:00