bizcrush-pipeline · 개발 회의 참고자료
요지 — 재구성·digest·다국어(en, 향후 ja·zh)·원문 응답 전부 단일 articles 테이블로 대응 가능하다.
테이블 분리·대규모 마이그레이션이 필요한 지점은 없고, 스키마에서 실제로 걸리는 곳은
둘이다: ① 언어가 컬럼명에 박힌 fast_track_en_correction_id
② (translated_from_article_id, lang) 유니크 인덱스 부재.
digest는 별도 테이블도, 별도 writer_mode 값도 아니다.
CHECK 제약이 writer_mode IN ('original','reconstruct') 둘만 허용하고, 라이브 735행 전부 reconstruct다.
digest 기사의 정체는 reconstruct 행 중 digest jsonb 컬럼이 채워진 것(74행 · 전부 ko · 인공지능신문 트랙)이다.
| 구분 | 실측값 |
|---|---|
| 테이블 | public.articles — 29컬럼 단일 테이블 · 총 735행 (ko 469 · en 266) |
digest jsonb 구조 | { perspective: string, source: { url, author, publisher, published_at } } |
| digest 채움 | 74행 / 735 — 전부 lang='ko' · en 형제 0건 (aitimes는 translate_to 없음) |
| API 노출 | /v1/published의 digest·perspective_notice는 DB 컬럼이 아니라 이 jsonb에서 API 레이어 파생 (apps/api/engine_api/attribution.py) |
공통 코어 위에 digest·번역·패스트트랙이 각각 nullable 컬럼 그룹으로 얹힌 구조다. 행의 성격(재구성/digest/번역본)은 그룹의 채움 여부로 갈린다.
| 그룹 | 컬럼 | 비고 |
|---|---|---|
| 공통 코어 | id · cluster_id · lang · market · slug · title · body_md · refs · target_keywords · writer_mode · status · regen_count · review_note · published_at · created_at · updated_at · generated_title · summary · verify_detail · batch_stats · published_images | refs=원문 URL 배열(jsonb) · slug 전역 UNIQUE · lang은 CHECK 없는 text · status CHECK 7종 |
| digest | digest | jsonb 1개 — perspective + source 메타. 채워지면 digest 기사 |
| 번역 (en) | translated_from_article_id · translation_source_hash · translation_issues | translated_from_article_id는 self-FK(부모 ko) — en 266행 전건 채움, ko 0건 |
| 패스트트랙 | fast_track_correction_id · fast_track_requested_at · fast_track_published_at · fast_track_en_correction_id | fast_track_published_at은 「사람 검수 없는 발행」 감사 축(양 언어 공용) · …_en_…은 유일하게 언어가 컬럼명에 박힌 자리 → 블로커 ① |
스키마는 동일하고 채워지는 컬럼만 다르다. en을 구별하는 것은 lang='en' + 번역 계보 3컬럼뿐이다.
| 컬럼 | ko (469행) | en (266행) | 해석 |
|---|---|---|---|
| translated_from_article_id | 0 | 266 | en 전건이 ko 부모를 가리킨다 |
| translation_source_hash | 0 | 266 | 부모 본문 해시 — 부모 변경 감지 |
| translation_issues | 0 | 31 | en gate_failed 31행과 정확히 일치 |
| digest | 74 | 0 | digest 소스는 translate_to가 없어 en 형제 미생성 |
| summary | 395 | 266 | |
| fast_track_published_at | 401 | 224 | 자동 발행 감사 축 — 양 언어 공용 |
상태 분포: ko는 published 448 · in_review 16 · unpublished 5, en은 published 235 · gate_failed 31 (in_review 0). 9/1 실측 문서 대비 크게 달라진 값으로, ko 자동 발행 활성 사고분 620건 사후 수용(9/2 유지 판정)이 반영된 상태다.
원문 본문은 이미 매거진 DB 안에 있다 — raw_items.body_text. 연결은 FK 두 홉으로 완결된다.
sources
소스(=계약) 1건
license_tag · 스위치
raw_items
원문 사본 — body_text ·
title · publisher · author ·
images · lang(현재 ko만)
clusters
클러스터 1 : raw_items N
(2소스 confirmed 경로)
articles (ko)
재구성 / digest 본문
refs = 원문 URL 배열
articles (en·ja·zh…)
번역 형제 — self-FK 1:N
언어별 행 추가만으로 확장
⚠️ 정본은 엔진 쪽이다 — engine.articles(파싱 본문) + GCS raw HTML. raw_items는 어댑터 사본이라
backfill(body_hash 변경) 시 어긋날 수 있다. 라이브 기준 클러스터 469 = ko 기사 469로 1:1이지만, 스키마상 클러스터당 raw_items는 N건이다.
패스트트랙은 테이블이 아니라 두 DB에 걸친 스위치 + 컬럼 4개다.
스위치는 엔진 DB engine.sources.request_fast_track(엔진 0012)에 살고, 대기표·감사 축은
매거진 articles 행 자체에 있다(매거진 0026) — 별도 큐 테이블은 없다(매거진 DB 12테이블 전수 확인).
| slug | 매체 | enabled | request_fast_track |
|---|---|---|---|
| itdaily | 아이티데일리 | true | true |
| aitimes | 인공지능신문 | true | true |
| epnc | 테크월드뉴스 | true | true |
| itdaily-emptyprobe | AC⑥ 테스트 프로브 (제휴사 아님) | false | false |
제휴 3사 전부 적용 — ADR-0022 rev1(패스트트랙 3사 · 근거는 협약 소스 운영 정책)의 문면과 라이브가 일치한다. 프로브 행은 T6 데모용 복제 소스로 비활성이다.
| 컬럼 | 역할 |
|---|---|
| fast_track_correction_id | 이 컬럼이 곧 대기표(queue)다 — non-null = 처리 중, 스윕이 발행·통지까지 끝내면 NULL 복귀. 엔진 correction_requests.id의 크로스-DB 참조라 FK가 없다. 라이브 pending 0건 |
| fast_track_requested_at | 접수 시각(api가 찍음) — 계약 제8조① 「즉시」의 증빙 시작점. 발행 시각과의 차가 지연 실측 |
| fast_track_published_at | 「사람 검수 없는 발행」의 유일한 감사 축 — 불변 원칙 4의 예외가 몇 건인지 여기서만 센다. published_at과 다른 사실(저쪽은 최초 발행 시각) |
| fast_track_en_correction_id | en 형제로 정정을 잇는 일회용 배턴(ADR-0022 D6) — 블로커 ①의 그 컬럼 |
같은 fast_track_published_at이라도 fast_track_requested_at 유무로 경로가 갈린다:
있음 = 정정 패스트트랙(ADR-0022), 없음 = 자동 발행(ko는 ADR-0025 · en은 스윕 ②).
| lang | ft 발행 계 | 정정 경로 | 자동 발행 경로 | 처리 중(pending) |
|---|---|---|---|---|
| ko | 401 | 2 | 399 | 0 |
| en | 224 | 0 | 224 | 0 |
정정 패스트트랙이 실제로 쓰인 것은 ko 2건뿐이고, 나머지 623건은 자동 발행 경로다.
fast_track_en_correction_id는 라이브 채움 0건(일회용 배턴 — 스윕이 소비 후 NULL 복귀)이지만,
경로 자체는 살아 있는 코드라 블로커 ①의 재설계 대상이다.
| 요구 | 판정 | 스키마 작업 | 스키마 밖 작업 |
|---|---|---|---|
| 원문을 응답에 포함 | JOIN으로 가능 | 없음 — articles.cluster_id → raw_items |
정책 결정 3건 (아래) + 응답 조립 |
| digest 구분 제공 | 이미 충족 | 없음 — digest jsonb + API 파생 필드 기존재 |
없음 |
| ja·zh 언어 추가 | 구조는 됨 · 걸림 2 | 블로커 ①② (아래) — 그 외 DDL 0 | 화이트리스트·프롬프트·SERVED_LANGS·models.yaml |
raw_items.body_text) vs 엔진 정본(engine.articles) — backfill로 어긋날 수 있어 서빙 기준을 정해야 한다.translate_to처럼 소스별 opt-in 스위치가 맞다. 현 라이브 3사는 전부 license_tag=contract라 후보는 된다.불변 원칙 1(writer 원문 차단)은 모듈 경계 문제라 API가 원문을 응답에 싣는 것과 무관하다 — 위반 아님.
fast_track_en_correction_id — 언어가 컬럼명에 박힌 유일한 자리
ko 부모 행에서 en 형제의 정정 배턴을 거는 용도다. ja·zh를 더하면
fast_track_ja_correction_id·fast_track_zh_correction_id… 로 언어마다 컬럼이 증식하는 구조라,
언어 확장 전에 재설계가 필요하다.
대안 축: 배턴을 부모 행의 언어별 컬럼이 아니라 형제 행 자신의 fast_track_correction_id로 옮기거나,
jsonb / 별도 테이블로 정규화. 29컬럼 중 이 하나만 언어 중립이 아니다.
(translated_from_article_id, lang) 유니크 인덱스 부재
현재 articles_translated_from_idx는 비유니크 부분 인덱스다. 언어가 en 하나일 때는 스윕 코드가
중복 형제를 막지만, 언어별 스윕이 병행되면 같은 부모에 형제가 이중 생성되는 것을 DB가 막아주지 않는다.
이번 확장에서 유일하게 권장하는 DDL 1건:
create unique index articles_sibling_lang_uidx
on articles (translated_from_article_id, lang)
where translated_from_article_id is not null;
라이브 데이터는 부모×언어 중복 0건이라 즉시 적용 가능. DDL은 root + dbname=magazine
(magazine_app은 owner 아님 — deploy.md 소유자 함정).
언어 확장의 나머지는 전부 코드/설정이다. 번역 스윕 자체는 이미 lang_to 파라미터로 짜여 있어 언어별 호출로 확장된다.
| 위치 | 현재 | ja·zh 추가 시 |
|---|---|---|
| packages/connectors/connectors/config.py | TRANSLATE_TO_ALLOWED = {"en"} 화이트리스트 · translate_to는 이미 list[str] | 화이트리스트 확장 (계약 판단 선행 — ADR-0020 D8) |
| pipeline/writer/translate.py | _PROMPTS에 ("ko","en") 쌍만 · 부록 로컬라이즈에 lang_to=="en" 분기 (551·632행) | 언어쌍 지시문 + 로컬라이즈 분기 추가 |
| apps/api/engine_api/magazine.py | SERVED_LANGS · DEFAULT_LANG="ko" | 서빙 언어 추가 |
| config/models.yaml | translate 단계 모델 (정본 — ADR-0003 rev3) | 언어쌍별 모델이 갈리면 여기서 (ADR-0020 소관) |
ja 추가는 JP 시장 트랙 동결과 별개 축이다 — KR 시장 기사의 일본어 번역(lang=ja · market=KR)이지 JP 시장 자체 생성이 아니고,
translate 경로는 langpack/ja를 거치지 않는다(무접촉 유지). 단 새 언어쌍은 계약 판단이 선행한다.