Commit Graph

24 Commits

Author SHA1 Message Date
hyungi 2fedaa065b fix(study): subject_note_rag 에 licensed_restricted 필터 누락 — 구매자료 분야노트 RAG 누수
explanation_rag 는 restricted_exclude_orm() 으로 licensed_restricted 문서를 제외하는데(B-4, a안 U-2① 단일술어), 복제된 subject_note_rag._gather_document_evidence 는 이 술어를 빠뜨려 구매 자료 verbatim 이 분야노트 RAG 로 샐 수 있었음(services 리뷰 P1 보안 drift). doc_meta 쿼리에 필터 추가 → valid_doc_ids → 청크 쿼리까지 자동 전파(explanation_rag 동일 구조).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 07:10:24 +09:00
hyungi 08c5213168 feat(publish): S-4 pub_card_progress 발행 — 카드 SR 상태 read model (study→viewer)
DS 가 가진 카드 SR progress row 를 발행(kind=study_card_progress) = read model.
viewer C-4 복습큐/미확인 set-difference 재료. plan study-viewer-port S-4.
- projection: KIND_CARD_PROGRESS + project_card_progress(card_id·topic_id·last_outcome·
  last_reviewed_at·due_at·review_stage). ★ALL row(due_at NULL sentinel=암-on-new·terminal
  포함) — due-only 발행 금지(sentinel 누락→viewer 미확인 오분류).
- enqueue: enqueue_card_progress_publish + backfill_publish_card_progress(필터 없음).
- 훅: /study-cards/{id}/rate 의 rate_card 직후(같은 tx·flag 게이트). 단일 write 사이트.
  SR 계산=DS(sr_schedule 무변경), 발행=결과만.
- 카드 삭제 시 progress tombstone 안 함 = DS SR 보존(재승인 복원), orphan 은 viewer C-4 가 로컬 드롭.
- scripts/backfill_publish_card_progress.py.

py_compile PASS · project_card_progress 단위검증(sentinel due_at=None 보존).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 16:00:10 +09:00
hyungi af5640ef49 feat(publish): S-2 pub_card 발행 — 검수완료 암기카드 (study→viewer)
검수완료(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>
2026-06-25 15:58:16 +09:00
hyungi 63457e6afc feat(publish): S-1 pub_topics 발행 — projection+저작훅+백필 (study→viewer)
주제(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>
2026-06-25 13:48:08 +09:00
hyungi 8d3b648b5f feat(ingest): P2 DS write-back — /ingest/study/attempts 멱등 finalize 재생 (study→viewer)
뷰어 로컬 풀이 세션을 DS 로 흘려 학습엔진(SR/pattern/오답/4-A·4-B) 재생. 기본 inert(flag off).
- 마이그 373~376: study_quiz_sessions 에 finalized_at(멱등 마커)·client_session_uuid·source
  + UNIQUE(client_session_uuid, study_topic_id) partial.
- outcome.py derive_outcome = 채점 단일 소스(라이브 submit_attempt 도 이걸로 리팩터 → 정오 어휘
  한 곳, ingest 는 raw 신호 selected+unsure 만 싣고 DS 산출 = '무수정 재생' 성립).
- ingest_study.py: Bearer(VIEWER_SYNC_TOKEN)+study_ingest_enabled gate. pub_id→source_id→question
  해소(graceful skip)·principal=question.user_id(mixed 거부)·topic 별 DS 세션(source=viewer·uuid)
  생성+attempt+finalize_session 무수정 재생+finalized_at, 1-tx 원자. uuid 존재=already_ingested
  캐시반환(멱등 → at-least-once 재전송에도 SR 이중 advance 0).
- config study_ingest_enabled + compose 매핑 + main 등록.

검증: py_compile·ephemeral 마이그(373~376 라이브스키마 위 클린)·single-statement. 배포 후
합성 세션 멱등/무이중SR 실측 예정. 배포=inert(STUDY_INGEST_ENABLED 미설정=503).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 07:27:34 +09:00
hyungi 642c1b7c36 feat(publish): P0-1 발행 레이어 스키마+projection+워커 (study→viewer)
docsrv-viewer-publish 발행 인프라 — 뷰어가 read API로 당길 published projection
+ transactional outbox + 단일 라이터 발행 워커. study_publish_enabled=false 기본
(저자/4-A enqueue 결선 P0-1b 전까지 inert). read-only 경로·additive·소프트락 무관.

- migrations 365~370: published(kind·pub_id opaque+stable·rev·payload_hash·deleted·schema_version)
  + UNIQUE(kind,pub_id)/(kind,source_id) + rev idx + publish_outbox + 미처리 부분 idx
- models/published.py: Published·PublishOutbox (관계 없음=mapper 안전)
- services/study/publish_projection.py: project_question/explanation + payload_hash(정렬 sha256)
- services/study/publish_enqueue.py: enqueue_publish/question + backfill(bounded page)
- workers/study_publish_worker.py: outbox drain → pg_advisory_xact_lock 단일라이터 rev 부여
  + (payload_hash,deleted) 디둡 + 배치내 중복 flush
- config: study_publish_enabled(기본 false) · main: publish_outbox_consumer 1m max_instances=1

plan: plans/2026-06-23-study-to-viewer-slice1-plan.html (P0-1, 3R 적대리뷰 통과)
검증: py_compile·payload_hash 단위·마이그 1문/파일·매퍼 standalone. 전체 매퍼/마이그 apply=배포 게이트.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:40:59 +09:00
hyungi 9c22337647 fix(search): 공유 AsyncSession 동시 쿼리 직렬화/세션 분리 + rewrite axis 누락 (R2)
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>
2026-06-16 13:18:17 +09:00
hyungi a6db6c999b fix(safety): B-4 리뷰 반영 — 단일 술어 중앙화 + study/briefing 경로 커버
적대 리뷰(10에이전트) 확정 반영:
- license_filter.py 신설 — restricted_exclude_sql(raw)/restricted_exclude_orm(ORM)
  단일 정의. retrieval _license_sql·digest·briefing·study 풀이가 공유(드리프트 방지).
- major: explanation_rag(study 문제 AI 풀이 RAG)에 술어 누락 → doc_meta 쿼리에 ORM
  적용(valid_doc_ids 경유로 청크도 차단). briefing/loader 2쿼리에 누락 → digest 와
  동일 술어 추가(news restricted 부재=방어적·경로 일관성).
- blocker(low-impact): file_watcher changed-doc 경로 material/license 보정(merge 주입·
  license 부재 시만 — extract_meta clobber 회피, pre-B-4 적재분 동기화).
- 테스트: 단일-source 검증 + ORM 구성 스모크 2건 추가.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-13 14:52:04 +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 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 0a7402b327 feat(study): 공부 암기노트 Phase 1 — card_extract 추출 파이프라인 (순수 additive)
study_memo_cards 추출 파이프라인 + 버전키 폴러 + needs_review 컬럼. 운영 SR 코드(session_finalize/quiz_selection) 무수정.

- migrations 287~298: study_memo_cards/_evidence/_jobs/_progress(P1 휴면)·study_reminders·study_topics.focused_at·study_questions needs_review 3컬럼. dedup PARTIAL UNIQUE(deleted_at IS NULL).
- 워커: in-process RAG gather → MLX {cards} → 카드 가드(정량=evidence 원문 등장·cue/cloze 누출·dedup) → supersede 구버전 retire → append. 별 consumer 로 기존 study_queue 격리.
- 폴러 study_card_enqueue: 버전키 NOT EXISTS(source_version) 멱등 + ai_explanation_generated_at NOT NULL 가드 + per-poll LIMIT(thundering-herd).
- 검증: 실 prod 스키마 덤프 위 12 마이그 적용 OK + dedup/supersede/active-unique 기능 7/7 PASS + 정규화 util 15/15.

plan: PKM plans/2026-06-05-study-memo-card-p1-plan.html

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 21:33:12 +09:00
hyungi f940f50c60 feat(search): route retrieval through corpus_chunks view (Hier-Decomp-1 c2)
baseline chunk 벡터검색을 document_chunks → corpus_chunks 뷰(in_corpus=true)로 rewire.
in_corpus=false(비활성 hier leaf 등) 자동 제외 = 검색 오염 구조적 차단(B choke point).

- retrieval_service: baseline chunks_table=corpus_chunks, _VALID_CHUNKS_TABLE 에 corpus_chunks 허용,
  snapshot_clause 조건 corpus_chunks 포함(eval snapshot 보존). candidate(cand_*) 경로 불변.
  documents 측(FTS+doc embedding) 무변경 — doc row 는 교체 무관.
- models/chunk: 5 신규 컬럼 매핑(parent_id/level/node_type/is_leaf/in_corpus). server_default 로
  기존 chunk_worker INSERT 무영향(legacy=in_corpus true/is_leaf false).
- subject_note_rag/explanation_rag: RAG chunk 로드에 in_corpus=true 필터(교체 doc legacy 중복 방지).

게이트: G4b(rewire 불변) before/after IDENTICAL(현재 view==table no-op) / G4a(누출) synthetic
in_corpus=false leaf 가 corpus_chunks 0건·document_chunks raw top(dist 0.0) 양방향 증명. /health 200.

plan: hierarchical-decomposition-tiered-nesting-marmot.md

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:58:28 +00:00
Hyungi Ahn 8074be6b6d feat(study): Phase 4-D 운영 관찰 + confidence calibration
Phase 4-B v1 첫 검증 결과 자료 부족 토픽인데도 모델이 confidence='high'
박는 케이스 발견. 정의 (high = 자료 + 다른 ai_explanation 으로 패턴 명확)
보다 과신 — UX 신뢰도 위험. 자동 cap 보정 + 운영 관찰 SQL 추가.

confidence calibration (services/study/session_summary_guard):
- calibrate_confidence(c, ctx_docs_count, ready_explanation_count) 신규
  · ctx_docs_count == 0 AND ready_explanation_count == 0 → 'low' cap
  · ctx_docs_count == 0 (ready 만 있음)  → 'medium' cap
  · ctx_docs_count >= 1                  → 모델 값 그대로
- 모델이 정의보다 더 보수적인 값 박은 경우 (모델 'low' + cap 'medium') 는
  보존 — 더 보수적인 값을 절대 올리지 않음

worker 적용 (study_session_analysis_worker):
- ctx_docs_count = len(ctx_docs)
- ready_explanation_count = sum(1 for a in prompt_attempts if a.get('ai_explanation'))
- calibrate_confidence 호출 → study_quiz_session_analysis.confidence 박힘
- job.payload 에 운영 분석 metadata 보존:
  · ctx_docs_count / ready_explanation_count
  · model_confidence_raw (모델 응답) vs calibrated_confidence (cap 후)
  · prompt_attempts / valid_attempts_total / summary_len
  → SQL 4 번 쿼리가 cap 작동 빈도 측정

scripts/phase4_health.sql (신규 운영 점검 SQL 7 섹션):
1. 4-A study_question_jobs status × error_code 분포
2. 4-B study_quiz_session_jobs status × error_code 분포
3. 4-B confidence 분포 (calibrated)
4. 4-B model_confidence_raw vs calibrated 차이 (cap 작동 빈도)
5. 4-A/4-B 최근 7일 처리 지연 p50/p95/max/avg
6. 4-A/4-B skipped 사유 분포
7. 4-B guard_fail / parse_fail / llm_timeout 비율

ship gate (단위 테스트):
- test_calibrate_confidence_no_evidence_caps_to_low (3 케이스)
- test_calibrate_confidence_only_explanations_caps_to_medium (3 케이스)
- test_calibrate_confidence_with_documents_passthrough (3 케이스)
- test_calibrate_confidence_normalizes_invalid_first (2 케이스)

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:33:57 +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 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 d3bf963a66 feat(study): Phase 2-B 결과 화면 변화 카운트 + 확인완료 progress 통합
Phase 1 finalize 가 계산하던 SessionSummary 가 응답에 포함되지 않고 discard
되던 것을 quiz_session row 4 컬럼으로 영속화. 결과 화면 헤더에 회복/퇴행/
새로 맞힘/반복 오답 누적 변화 카운트 + "바로 할 일" 콜아웃 (지금 시점
progress 기반 동적 카운트 — pending_review/chronic/regressed). 동적 카운트는
결과 GET 호출 시점에만 계산 (목록 endpoint 비용 회피).

확인완료 통합 — 결과 카드의 [학습완료] 버튼이 attempts.reviewed_at 만 박던
것을 progress.last_reviewed_at + (wrong/unsure 면 due_at 최초 부여) 도 같이
박도록. reviewed=false 토글은 attempts 만 되돌림 (다른 attempt 가 검토 표시
했을 수 있어 progress 의 last_reviewed_at 은 보존).

- migrations/230 — quiz_sessions 4 컬럼 ADD (단일 ALTER TABLE)
- StudyQuizSession 모델 + finalize_session 가 row 영속화
- QuizSessionSummary 응답에 4 스냅샷 + 3 동적 필드 (default 0)
- _build_session_summary include_progress_counts=True 시 SQL 3회
- review-mark 가 reveiwed=true 시 progress 동기화
- 결과 화면: 헤더 변화 카운트 줄 + 바로 할 일 콜아웃 (값 있을 때만)

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

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:49:01 +09:00
Hyungi Ahn 9094b2dbc5 feat(study): Phase 1-E 풀이 선별 알고리즘 — bucket + stage 비율
vision 의 단일 풀이 진입점 — stage (intro/learning/pre_exam) + size 옵션으로
같은 endpoint 가 다른 분포의 문제 출제.

services/study/quiz_selection.py:
- bucket: unattempted / wrong_or_unsure / due_review / regressed / frequent / random
- stage 별 비율:
  - intro:    unattempted 55, wrong_or_unsure 30, frequent 15
  - learning: due_review 20, wrong_or_unsure 40, unattempted 30, frequent 10
  - pre_exam: due_review 20, wrong_or_unsure 30, regressed 10, frequent 20, random 20
- bucket 우선순위 (dict 순서) — 다음 bucket 은 이미 뽑힌 qid 제외
- 후보 부족 시 random backfill, 그래도 부족 시 ValueError

api/study_topics.py:
- QuizSessionStartRequest 에 stage / size 옵션 추가
- stage 명시 시 select_questions_for_quiz 사용
- stage 미명시 시 기존 PR-12-B 경로 (subject bucket + spacing) 호환 유지

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:30:11 +09:00
Hyungi Ahn e5982ebde4 feat(study): Phase 1 학습 루프 데이터 계층 — progress 캐시 + finalize + review API
vision (풀이 → 확인 → 학습 → 복습 → 다음 풀이 가중치) 의 데이터 계층.

데이터 모델 (migrations 222~225):
- study_question_progress 테이블 — user × topic × question 단위 현재 상태 캐시
  - 마지막 시도: last_outcome, last_attempted_at, last_attempt_id
  - 검토 상태: last_reviewed_at
  - 복습 큐: due_at, review_stage
  - 패턴 분류 (derived): pattern_state, pattern_updated_at, pattern_window_attempts
- 3 partial idx (due / topic_pattern / pending_review) — 탭별 빠른 조회

패턴 분류 (services/study/learning_pattern.py):
- 7 분류: unattempted/unsure/chronic_wrong/regressed/recovered/stable/unstable
- 윈도우 = 최근 3회 + 과거 correct/wrong 존재 여부
- chronic_wrong > regressed > recovered 우선순위 (보수적 학습)
- 가드: wrong 1회만으로 regressed 안 됨 (이전 correct 이력 필요)
- stable 은 3 연속 correct 부터

세션 종료 집계 (services/study/session_finalize.py):
- attempts append-only 원본 보존, progress upsert 만
- 마지막 attempt 직후 finalize hook 자동 발동
- finalize 는 last_* + pattern_state 만 갱신, due_at 미진입 문제는 NULL 유지
- 이미 due_at 박힌 문제는 finalize 가 stage 갱신 (correct → +1 / wrong → 리셋)

API (api/study_question_progress.py):
- POST /study-topics/{tid}/questions/{qid}/review-complete
  → last_reviewed_at + (wrong/unsure 인 경우만) due_at 최초 부여
- GET /study-topics/{tid}/review-queue?tab=due_today|pending_review|chronic|regressed|mastered
  → 5 탭 paginated 조회
  → pending_review 는 last_reviewed_at < last_attempted_at 까지 포함 (이전 확인완료 후 다시 wrong 잡힘)

Phase 1-E (풀이 선별 알고리즘) 은 후속 commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 09:28:46 +09:00
Hyungi Ahn 219e233a48 feat(study): related-types DB 캐시 — HNSW 매번 재계산 제거
- migrations 220/221: study_questions 에 related_repeat/similar JSONB + 카운트/grade/computed_at/threshold_version + partial idx
- 임베딩 워커: ready 처리 직후 같은 트랜잭션에서 related 계산·저장 + 같은 토픽 ready 행들의 related_computed_at=NULL invalidation
- 신규 cron study_q_related_refresh (1분, batch=20) — stale 캐시 일괄 재계산
- API list_related_types: cache hit (computed_at + threshold version 일치) 시 SELECT 1번으로 응답. miss 면 즉시 계산+저장 후 응답
- update_question PATCH: 본문/exam_round 변경 시 related_computed_at=NULL
- soft delete: 같은 토픽 ready 행 invalidation

threshold 변경 시: related_types.THRESHOLD_VERSION 갱신 + UPDATE WHERE version != '<신>' SET computed_at=NULL 한 번이면 cron 자동 일괄 재계산.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 07:22:31 +09:00
Hyungi Ahn fc8aea1649 feat(study): 반복 출제 라벨 등급 + cosine 임계값 0.85 조정
- round_count 별 등급 매핑 (단골/잘 나오는 반복 출제/반복 출제/신출/빈출)
  - ≥7 단골, 5–6 잘 나오는 반복 출제, 3–4 반복 출제,
    2 + max(연도)≥2024 신출, 2 + 모두 옛 빈출
- SIMILAR_THRESHOLD 0.88 → 0.85 (5-source 분포 측정 결과 자연 갭 위치 반영)
- API 응답 + 프론트 3곳 (보기/통합뷰/결과 카드) 라벨 일괄 통일

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 08:50:39 +09:00
Hyungi Ahn 13404cd366 feat(study): 같은 유형 과밀 방지 — 출제 단계 spacing (PR-12-B)
학습 의미: 한 quiz 세션 안에서 같은 유형 문제가 과도하게 몰리지 않게 분산.
같은 유형을 없애는 게 아니라 펼치는 것 — dedup/제거 프레임 금지.

- 마이그레이션 210: study_quiz_sessions.quiz_mode VARCHAR(30) DEFAULT 'random'
- ORM: StudyQuizSession.quiz_mode 필드
- service.related_types: apply_type_spacing helper 추가
  - SPACING_THRESHOLD=0.88 (회차 무관 — PR-12-A 회차 필터 재사용 X)
  - PER_TYPE_CAP=2 (local neighbor cap, transitive cluster 보장 X)
  - SPACING_BUFFER_RATIO=2.0
  - 3단계 fallback: ready spacing → pending 보충 → hold cap 위반 fallback
  - debug 로그 type_spacing_applied subject=... ready=N selected=M ...
- _select_questions_for_topic: subject bucket 단위 spacing (과목 균등 보호)
- QuizMode Enum (random) — 향후 frequent_focus/wrong_variants 예약
- start_quiz_session 에 quiz_mode 받기 + apply_spacing 전달
- 프론트 startNewQuiz body 에 quiz_mode='random' 명시

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:45:15 +09:00
Hyungi Ahn cbe852bb37 feat(study): 반복 출제 / 유사 유형 분리 표시 (PR-12-A)
학습 의미가 회차 간 반복성 — 차단/제거가 아니라 패턴 표시 frame.

- 신규 service `related_types.py` — threshold/회차 필터/round_count 계산 공유
  - REPEAT >= 0.95 / SIMILAR 0.88~0.95
  - 회차 조건 백엔드 강제 (자기 자신/같은 회차/null exam_round candidate 제외)
  - round_count: related_count == 0 → 0 (현재 회차만 1로 채우지 않음)
- GET /study-questions/{qid}/related-types — 단건 분류 (repeat_questions / similar_questions)
- POST /study-topics/{tid}/related-types-bulk — 카드 배지용 카운트 batch
  - 비교 대상 = 토픽 전체 ready pool (입력 qid 끼리 비교 X)
  - 응답 키 보존 — 권한 없음/임베딩 미준비 등도 (0,0,0,0)
- 보기 페이지: PR-11 비슷한 문제 토글 제거 + 🔥 반복 출제 / 🧩 유사 유형 두 섹션 자동 노출
  - 헤더 = round_count "N개 회차", 본문 위 = related_count "관련 N문제"
  - source_status / source_exam_round 안내 분기
- 결과 페이지 (틀린/모르겠음 카드): bulk 호출 후 round_count >= 2 일 때만 배지
- 통합뷰 회차 expand 시 lazy bulk 호출 — 같은 회차 캐시
- 기존 /similar 엔드포인트 유지 (raw 디버깅용)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 08:09:14 +09:00
Hyungi Ahn d968b2d901 feat(study): 문제풀이 모드 개편 + 결과 분류 + 분야 설명 (PR-9)
- 라벨 "복습 시작" → "문제풀이"
- attempts.outcome 컬럼 + selected_choice nullable (correct/wrong/unsure)
- 풀이 중 정답·해설·AI·비슷한 문제 모두 비노출, 답 클릭 시 자동 진행
- "모르겠음" 5번째 옵션 추가
- 결과 화면 = 정답/틀린/모르겠음 3 카테고리 탭, 카드 클릭 expand
  - 틀린 → PR-3 AI 해설 (RAG)
  - 모르겠음 → 분야(subject+scope) 설명 AI 즉석 생성 + 캐시 (PR-9 신규)
- 분야 설명 RAG: 매핑 documents 청크 + 같은 분야 다른 문제·해설 → bge-reranker
- 마이그레이션 200~205 (single-statement, asyncpg 호환)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:58:35 +09:00
Hyungi Ahn e1a2cdc677 feat(study): AI 풀이 생성 — 수동 트리거 + RAG (PR-3)
복습 답 제출 후 또는 편집 화면에서 사용자가 명시적으로 누를 때만 AI 가
4지선다 풀이 생성. 자동 일괄 생성 금지 (하루 100문제 입력 시 MLX 부하·
잘못 입력 문제 해설 위험).

데이터 모델 (migrations 191~192):
- study_questions 4 컬럼 추가: ai_explanation TEXT, ai_explanation_status
  VARCHAR(20) DEFAULT 'none' (none/pending/ready/failed/stale),
  ai_explanation_generated_at, ai_explanation_model
- partial idx (study_topic_id, ai_explanation_status) WHERE status != 'none'

PATCH stale 자동 전이: question_text/choice_*/correct_choice 변경 시
status='ready' 만 'stale' 로. 본문은 보존, UI 배지 + "다시 생성" 동선.

신규 엔드포인트: POST /api/study-questions/{id}/ai-explanation
- regenerate=false + ready/stale → 캐시 즉시 (MLX 호출 없음, is_stale 플래그)
- pending → 409 (race-safe 조건부 UPDATE 로 동시 호출 차단)
- 그 외 → 새 생성

RAG 입력 풀:
- 1순위: study_topic 매핑 documents 청크 + ai_summary, bge-reranker top-5
- 2순위: 같은 토픽 다른 questions (자기 자신 제외, ai_explanation 은 ready
  상태만 포함 — 재귀적 hallucination 방지), reranker top-3
- 제외: 필기 OCR / 외부 웹 / Premium 모델

모델: Mac mini MLX gemma-4-26b primary 단독. get_mlx_gate() Semaphore(1) 경유,
30s timeout. 실패 시 status='failed' + 직전 본문 보존.

프롬프트 (app/prompts/study_question_explanation.txt): 자료 우선순위·인용
형식·할루시네이션 방지 절대 규칙 (법령명·조항·수치·표준 번호 단정 금지,
"자료에서 확인되지 않음" 명시).

프론트:
- 복습 화면 답 제출 후 인라인 expand. status별 버튼 분기 (ready 캐시 /
  stale "이전 풀이"+"다시 생성" / failed "다시 시도")
- 편집 화면 별도 카드. 상태 배지 + "이전 풀이 보기" / "다시 생성" 분리
- 참고 근거 토글 (source_type 별 아이콘 📄/ + 제목 + snippet)

후속 PR 보류: 오답노트/통계, AI 일괄 백그라운드 생성, 필기 OCR RAG,
Premium/Claude 재생성, /api/search/ask retrieval scope 통합.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 08:41:46 +09:00