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>
This commit is contained in:
hyungi
2026-06-25 07:27:34 +09:00
parent f0c55c21ff
commit 8d3b648b5f
11 changed files with 287 additions and 12 deletions
@@ -0,0 +1,4 @@
-- 373_quiz_session_finalized_at.sql
-- 발행 ingest(study-to-viewer P2) finalize 멱등 마커. finalize 성공 후 스탬프 →
-- 같은 세션 재전송(at-least-once outbox) 시 SR 이중 advance 차단. 라이브 세션은 NULL 유지(무영향).
ALTER TABLE study_quiz_sessions ADD COLUMN IF NOT EXISTS finalized_at TIMESTAMPTZ;
@@ -0,0 +1,3 @@
-- 374_quiz_session_client_uuid.sql
-- 뷰어 로컬 세션 UUID. ingest 가 (uuid, topic) 로 DS 세션 find-or-create = 멱등 키. 라이브=NULL.
ALTER TABLE study_quiz_sessions ADD COLUMN IF NOT EXISTS client_session_uuid TEXT;
+3
View File
@@ -0,0 +1,3 @@
-- 375_quiz_session_source.sql
-- 세션 출처 구분(live | viewer). 감사/필터용. 기존 행=live.
ALTER TABLE study_quiz_sessions ADD COLUMN IF NOT EXISTS source VARCHAR(20) NOT NULL DEFAULT 'live';
@@ -0,0 +1,3 @@
-- 376_quiz_session_client_uuid_uq.sql
-- (client_session_uuid, study_topic_id) 유일 — 뷰어 1세션이 topic 별 1 DS세션. partial(uuid 있는 viewer 행만).
CREATE UNIQUE INDEX IF NOT EXISTS study_quiz_sessions_client_uuid_topic_uq ON study_quiz_sessions (client_session_uuid, study_topic_id) WHERE client_session_uuid IS NOT NULL;