af5640ef49
검수완료(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>
93 lines
3.9 KiB
Python
93 lines
3.9 KiB
Python
"""발행 projection — 소스 행을 render-ready payload + 안정 해시로 변환 (순수 함수).
|
|
|
|
뷰어가 보는 '단일 진실'은 이 payload 까지 (DS 내부 실험 스키마는 계약 뒤 격리).
|
|
kind 별 projector. payload_hash = 정렬된 JSON 의 sha256 = (payload_hash, deleted) 디둡 키.
|
|
|
|
★주의(plan study-to-viewer-slice1 r2): 과목/시험메타를 per-question payload 에 인라인 —
|
|
bulk subject rename 시 N행 churn. 정규화(과목=별 kind subject ref)는 churn 최적화 후속(P0-1b),
|
|
읽기 정합엔 무영향. 지금은 인라인(상관관계 단순)으로 두고 후속 PR 에서 분리.
|
|
SCHEMA_VERSION = 엔벨로프 버전. payload 모양 진화 시 bump + 뷰어 range 수용(P0-2).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from typing import Any
|
|
|
|
SCHEMA_VERSION = 1
|
|
|
|
KIND_QUESTION = "study_question"
|
|
KIND_EXPLANATION = "study_explanation"
|
|
KIND_TOPIC = "study_topic"
|
|
KIND_CARD = "study_card" # ★뷰어 pubstudy.ts 의 KIND_CARD 와 일치 필수(S-3 forward-contract).
|
|
|
|
|
|
def payload_hash(payload: dict[str, Any]) -> str:
|
|
"""정렬 JSON 의 sha256 — (payload_hash, deleted) 디둡 키. 키 순서/공백 비의존."""
|
|
canonical = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
|
|
return hashlib.sha256(canonical.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def project_question(q: Any) -> dict[str, Any]:
|
|
"""study_question → 발행 payload. 정답 포함(개인 학습툴, plan Q2). 이미지는 ref 만(P0-4, 후속)."""
|
|
return {
|
|
"topic_id": q.study_topic_id,
|
|
"question_text": q.question_text,
|
|
"choices": [q.choice_1, q.choice_2, q.choice_3, q.choice_4],
|
|
"correct_choice": q.correct_choice,
|
|
"subject": q.subject,
|
|
"scope": q.scope,
|
|
"exam_name": q.exam_name,
|
|
"exam_round": q.exam_round,
|
|
"exam_question_number": q.exam_question_number,
|
|
"explanation": q.explanation, # 수동 해설(있으면). AI 해설은 별 kind.
|
|
}
|
|
|
|
|
|
def project_explanation(q: Any) -> dict[str, Any] | None:
|
|
"""study_question 의 AI 해설 → 별 발행 kind. ready 일 때만(없으면 None=발행 안 함).
|
|
|
|
재조우 표시용 선발행. 신규 오답은 4-A 워커가 ~90s 후 ready→재발행(P2-3 결선, P0-1b).
|
|
"""
|
|
if getattr(q, "ai_explanation_status", None) != "ready" or not getattr(q, "ai_explanation", None):
|
|
return None
|
|
gen = getattr(q, "ai_explanation_generated_at", None)
|
|
return {
|
|
"question_source_id": q.id,
|
|
"explanation_md": q.ai_explanation,
|
|
"model": getattr(q, "ai_explanation_model", None),
|
|
"generated_at": gen.isoformat() if gen else None,
|
|
}
|
|
|
|
|
|
def project_card(c: Any) -> dict[str, Any]:
|
|
"""study_memo_card → 발행 payload (S-2). 순수 변환 — 발행 자격(needs_review=false &
|
|
미삭제) 판단은 호출측(enqueue_card_publish)이 카드 상태로. payload 계약 = 뷰어
|
|
pubstudy.ts getCards 와 동형(format·cue·fact·cloze_text·source_question_id·source_generated_at).
|
|
"""
|
|
gen = getattr(c, "source_generated_at", None)
|
|
return {
|
|
"format": c.format,
|
|
"cue": c.cue,
|
|
"fact": c.fact,
|
|
"cloze_text": c.cloze_text,
|
|
"source_question_id": c.source_question_id,
|
|
"source_generated_at": gen.isoformat() if gen else None,
|
|
}
|
|
|
|
|
|
def project_topic(t: Any) -> dict[str, Any]:
|
|
"""study_topic → 발행 payload (S-1, plan study-viewer-port).
|
|
|
|
topic 메타만 신규 발행 — viewer 가 주제 단위 퀴즈를 만들 최소 정보.
|
|
회차 목록은 발행 안 함 = viewer 가 pub_content(study_question) 의 exam_name/exam_round 로
|
|
파생(추가 발행 불요, plan S-1 결정). topic_id 는 project_question 의 topic_id(=study_topic_id)
|
|
와 동일 DS 식별자라 viewer 가 문항→주제 상관에 사용(pub_id 는 opaque 라 상관 키 아님).
|
|
"""
|
|
return {
|
|
"topic_id": t.id,
|
|
"name": t.name,
|
|
"exam_round_size": t.exam_round_size,
|
|
}
|