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>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
"""S-2 초기 백필 — 검수완료(needs_review=False)·미삭제 study_memo_cards 를 발행 outbox 에 적재.
|
||||
|
||||
publish_outbox 에만 적재(멱등: 워커 (payload_hash, deleted) 디둡). study_publish_enabled=True
|
||||
일 때 발행 워커가 drain → published(kind=study_card) rev 부여 → viewer pull-sync.
|
||||
|
||||
실행 (GPU 서버):
|
||||
docker exec hyungi_document_server-fastapi-1 python /app/scripts/backfill_publish_cards.py
|
||||
docker exec hyungi_document_server-fastapi-1 python /app/scripts/backfill_publish_cards.py --dry-run
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
||||
|
||||
from sqlalchemy import func, select
|
||||
|
||||
from core.config import settings
|
||||
from core.database import async_session
|
||||
from models.study_memo_card import StudyMemoCard
|
||||
from services.study.publish_enqueue import backfill_publish_cards
|
||||
|
||||
# 개인 학습툴 카드 수 대비 넉넉(단일 outbox 적재 tx, 워커는 BATCH_SIZE 로 drain). 도달 시 가드 경보.
|
||||
PAGE = 100000
|
||||
|
||||
|
||||
async def run(dry_run: bool) -> None:
|
||||
async with async_session() as session:
|
||||
active = (
|
||||
await session.execute(
|
||||
select(func.count())
|
||||
.select_from(StudyMemoCard)
|
||||
.where(
|
||||
StudyMemoCard.deleted_at.is_(None),
|
||||
StudyMemoCard.needs_review.is_(False),
|
||||
)
|
||||
)
|
||||
).scalar() or 0
|
||||
|
||||
print(f"[info] study_publish_enabled={settings.study_publish_enabled} "
|
||||
f"(False 면 적재는 되나 워커가 drain 안 함)")
|
||||
print(f"[info] 검수완료·미삭제 카드 {active}건")
|
||||
if dry_run:
|
||||
print("[dry-run] 적재 안 함. 실제 실행은 --dry-run 제거.")
|
||||
return
|
||||
|
||||
async with async_session() as session:
|
||||
n = await backfill_publish_cards(session, after_id=0, limit=PAGE)
|
||||
await session.commit()
|
||||
|
||||
print(f"\n[ok] outbox 적재 {n}건 — 발행 워커가 drain(flag on 시) 하며 rev 부여.")
|
||||
if n >= PAGE:
|
||||
print(f"[warn] PAGE({PAGE}) 도달 — 카드가 더 있을 수 있음. after_id 페이징 추가 필요.")
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="S-2 pub_card 초기 백필")
|
||||
parser.add_argument("--dry-run", action="store_true", default=False)
|
||||
args = parser.parse_args()
|
||||
asyncio.run(run(args.dry_run))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user