feat(safety): A-2 수집기 ingest 시점 분류 축 부여 — 레지스트리 전파 + 승인 가드 (mig 352~355)
plan safety-library-1 A-2 (classify-skip 경로 전수 커버): - news_sources 에 material_type/license_scheme/license_redistribute + 안전·공학 12행 시드 - news_collector: 레지스트리 → documents 전파 (_material_axis — paper 는 jurisdiction NULL 강제) - kosha(사례·첨부=incident, GUIDE=guide)/csb(incident·US)/api_std(standard·US)/law_monitor(law·KR) /file_watcher(KGS=law·KR 타깃 매핑) deterministic 부여 + extract_meta.license 주입 - published_date: 소스별 가용 날짜 (GUIDE 공표일·CSB lastmod·API 공지일·법령 공포일·뉴스 발행일) - classify_worker: document_type→material_type 결정적 매핑 제안 (자동 전이 금지) - accept-suggestion: material 제안 적용 + law=jurisdiction 필수(기본값 없음) + 청크 미러 1문 동기화 - chunk_worker: 비뉴스 문서 country=jurisdiction 미러 (R3-m3: 검색측 country 소비자 0 실측) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+48
-4
@@ -210,8 +210,14 @@ class DocumentDetailResponse(DocumentResponse):
|
||||
|
||||
|
||||
class AcceptSuggestionRequest(BaseModel):
|
||||
"""§1 accept-suggestion 요청 body — stale payload / doc 수정 검출."""
|
||||
"""§1 accept-suggestion 요청 body — stale payload / doc 수정 검출.
|
||||
|
||||
jurisdiction: 안전 자료실 A-2 — material_type 제안 승인 시 사용자가 지정하는 관할.
|
||||
law 승인은 필수 (기본값 없음 — KR 자동 부여 시 외국 자료가 KR 법령으로 오염되는
|
||||
경로를 차단, plan A-2 계약).
|
||||
"""
|
||||
expected_source_updated_at: datetime
|
||||
jurisdiction: str | None = None
|
||||
|
||||
|
||||
class DocumentUpdate(BaseModel):
|
||||
@@ -1244,11 +1250,49 @@ async def accept_suggestion(
|
||||
# payload 적용
|
||||
proposed_category = doc.ai_suggestion.get("proposed_category")
|
||||
proposed_path = doc.ai_suggestion.get("proposed_path")
|
||||
# 안전 자료실 A-2 — material_type 제안 (classify 의 document_type 결정적 매핑)
|
||||
proposed_material = doc.ai_suggestion.get("proposed_material_type")
|
||||
|
||||
if not proposed_category:
|
||||
raise HTTPException(status_code=422, detail="proposed_category 누락된 suggestion")
|
||||
if not proposed_category and not proposed_material:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="proposed_category/proposed_material_type 둘 다 누락된 suggestion",
|
||||
)
|
||||
|
||||
doc.category = proposed_category
|
||||
if proposed_category:
|
||||
doc.category = proposed_category
|
||||
|
||||
if proposed_material:
|
||||
_MATERIAL_TYPES = {"law", "paper", "book", "incident", "manual", "standard", "guide"}
|
||||
_JURISDICTIONS = {"KR", "US", "EU", "JP", "GB", "INT"}
|
||||
if proposed_material not in _MATERIAL_TYPES:
|
||||
raise HTTPException(
|
||||
status_code=422, detail=f"허용 밖 material_type: {proposed_material}"
|
||||
)
|
||||
jur = body.jurisdiction or doc.ai_suggestion.get("proposed_jurisdiction")
|
||||
if jur is not None and jur not in _JURISDICTIONS:
|
||||
raise HTTPException(status_code=422, detail=f"허용 밖 jurisdiction: {jur}")
|
||||
# law = 국가 필수 입력, 기본값 없음 (plan A-2 — KR 자동 부여 시 외국 법령 오염.
|
||||
# DB CHECK(chk_documents_law_jurisdiction) 도 거부하지만 422 로 명시 안내).
|
||||
if proposed_material == "law" and not jur:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail="법령(law) 승인은 jurisdiction 필수 — body.jurisdiction 으로 국가를 지정하세요 (기본값 없음)",
|
||||
)
|
||||
doc.material_type = proposed_material
|
||||
doc.jurisdiction = jur
|
||||
# 미러 동기화 1문 — jurisdiction 부여/정정 시 청크 country 동반 UPDATE
|
||||
# (leg 간 국가 불일치 방지, plan A-2 계약. 단일 지점 = 본 승인 경로).
|
||||
if jur:
|
||||
from sqlalchemy import update as sa_update
|
||||
|
||||
from models.chunk import DocumentChunk
|
||||
|
||||
await session.execute(
|
||||
sa_update(DocumentChunk)
|
||||
.where(DocumentChunk.doc_id == doc.id)
|
||||
.values(country=jur)
|
||||
)
|
||||
|
||||
# user_tags append (중복 방지, normalize + dedup 통과)
|
||||
if proposed_path:
|
||||
|
||||
@@ -53,3 +53,12 @@ class NewsSource(Base):
|
||||
name="source_channel"),
|
||||
default="news",
|
||||
)
|
||||
|
||||
# ── 안전 자료실 분류 축 (plan safety-library-1 A-2, migrations 352~355) ──
|
||||
# 자료유형 기본값 — documents.material_type 으로 ingest 시점 전파 (NULL=비대상).
|
||||
# jurisdiction 은 별도 컬럼 없이 country 전파, 단 paper 는 코드에서 NULL 강제.
|
||||
material_type: Mapped[str | None] = mapped_column(Text)
|
||||
# extract_meta.license 주입용 — kogl/ogl/public_domain/proprietary/unknown.
|
||||
# 미확정 = 보수적(unknown + redistribute=false), 근거 확보 시 완화.
|
||||
license_scheme: Mapped[str | None] = mapped_column(Text)
|
||||
license_redistribute: Mapped[bool | None] = mapped_column(Boolean)
|
||||
|
||||
@@ -175,10 +175,16 @@ async def _ingest_detail(session, source: NewsSource, url: str) -> str:
|
||||
ai_domain="Engineering",
|
||||
ai_sub_group=_SOURCE_NAME,
|
||||
ai_tags=["Engineering/API 표준 공지"],
|
||||
# 안전 자료실 A-2 — 표준 '공지' = standard (코드 본문 아님 — ASME/API 본문은 paywall)
|
||||
material_type="standard",
|
||||
jurisdiction="US",
|
||||
published_date=pub_dt.date() if pub_dt else None,
|
||||
extract_meta={
|
||||
"source_id": source.id,
|
||||
"source_name": _SOURCE_NAME,
|
||||
"published_at": pub_dt.isoformat() if pub_dt else None,
|
||||
"license": {"scheme": "proprietary", "redistribute": False,
|
||||
"attribution": "American Petroleum Institute"},
|
||||
"fulltext": {
|
||||
"status": "api_announcement",
|
||||
"engine": engine,
|
||||
|
||||
@@ -311,6 +311,10 @@ async def process(document_id: int, session: AsyncSession) -> None:
|
||||
country, source, src_lang = await _lookup_news_source(session, doc)
|
||||
if src_lang:
|
||||
language = src_lang
|
||||
# 안전 자료실 A-2 — 뉴스 lookup 미해당(crawl/law/업로드) 문서는 jurisdiction 을
|
||||
# chunk.country 미러로 (leg 간 국가 일치. EU/INT 도 이 경로로 첫 유입 — String(10) 수용).
|
||||
if country is None and doc.jurisdiction:
|
||||
country = doc.jurisdiction
|
||||
domain_category = "news" if doc.source_channel == "news" else "document"
|
||||
|
||||
# 기존 chunks 삭제 (재처리)
|
||||
|
||||
@@ -62,6 +62,15 @@ FACET_DOCTYPES = {"발주서", "세금계산서", "명세표", "도면", "증명
|
||||
# 자료실 자동 분류 제안 대상 (거래 하위)
|
||||
LIBRARY_SUGGESTION_DOCTYPES = {"발주서", "세금계산서", "명세표"}
|
||||
|
||||
# 안전 자료실 A-2 — document_type → material_type 결정적 매핑 (제안 전용, 자동 전이 금지).
|
||||
# 모호한 doctype(Reference/Report 등)은 매핑하지 않음 — 무리한 전수 분류 시도 금지 (plan 0-1).
|
||||
_DOCTYPE_TO_MATERIAL = {
|
||||
"Law_Document": "law",
|
||||
"Academic_Paper": "paper",
|
||||
"Manual": "manual",
|
||||
"Standard": "standard",
|
||||
}
|
||||
|
||||
# PR-B prompt_version task 이름
|
||||
SUMMARY_TRIAGE_TASK = "p3a_short_summary"
|
||||
|
||||
@@ -492,6 +501,24 @@ async def process(
|
||||
if not doc.document_type:
|
||||
doc.document_type = doc_type if doc_type in DOCUMENT_TYPES else "Note"
|
||||
|
||||
# ─── 안전 자료실 A-2: material_type 제안 (업로드 경로 — LLM 직접 부여 금지) ───
|
||||
# document_type → material_type 결정적 매핑만 제안으로 적재 (프롬프트 변경 0).
|
||||
# 승인(accept-suggestion) 시에만 전이 — law 는 국가 필수 입력 (KR 기본값 오염 차단,
|
||||
# 자동 전이 금지 사상은 category 와 동일). 수집기 deterministic 경로는 이미 채워져
|
||||
# 있어(material_type IS NOT NULL) 본 제안 비대상. 거래문서 제안(ai_suggestion 점유)과
|
||||
# 충돌 시 기존 제안 우선 (두 제안이 겹치는 문서는 실무상 없음 — 거래 vs 안전자료).
|
||||
_mt_prop = _DOCTYPE_TO_MATERIAL.get(doc.document_type or "")
|
||||
if _mt_prop and doc.material_type is None and doc.ai_suggestion is None:
|
||||
doc.ai_suggestion = {
|
||||
"proposed_material_type": _mt_prop,
|
||||
"proposed_jurisdiction": None,
|
||||
"confidence": doc.ai_confidence,
|
||||
"source_updated_at": (
|
||||
doc.updated_at.isoformat() if doc.updated_at else None
|
||||
),
|
||||
"reason": "document_type→material_type 결정적 매핑",
|
||||
}
|
||||
|
||||
# confidence
|
||||
confidence = parsed.get("confidence", 0.5)
|
||||
doc.ai_confidence = max(0.0, min(1.0, float(confidence)))
|
||||
|
||||
@@ -202,7 +202,12 @@ async def _ingest_pdf(session, page_slug: str, pdf_url: str) -> bool:
|
||||
import_source="csb_sitemap",
|
||||
edit_url=pdf_url,
|
||||
ai_tags=["Safety/CSB/보고서"],
|
||||
extract_meta={"csb": {"page_slug": page_slug, "kind": "report_pdf"}},
|
||||
# 안전 자료실 A-2 — ingest 시점 deterministic. CSB = 미 연방기관 = public domain.
|
||||
material_type="incident",
|
||||
jurisdiction="US",
|
||||
extract_meta={"csb": {"page_slug": page_slug, "kind": "report_pdf"},
|
||||
"license": {"scheme": "public_domain", "redistribute": True,
|
||||
"attribution": "U.S. Chemical Safety Board"}},
|
||||
)
|
||||
session.add(doc)
|
||||
await session.flush()
|
||||
@@ -290,10 +295,16 @@ async def _ingest_url(session, source: NewsSource, url: str, lastmod: datetime)
|
||||
ai_domain="Safety",
|
||||
ai_sub_group=_SOURCE_NAME,
|
||||
ai_tags=["Safety/CSB"],
|
||||
# 안전 자료실 A-2 — ingest 시점 deterministic (classify-skip 경로)
|
||||
material_type="incident",
|
||||
jurisdiction="US",
|
||||
published_date=lastmod.date() if lastmod else None,
|
||||
extract_meta={
|
||||
"source_id": source.id,
|
||||
"source_name": _SOURCE_NAME,
|
||||
"published_at": lastmod.isoformat(),
|
||||
"license": {"scheme": "public_domain", "redistribute": True,
|
||||
"attribution": "U.S. Chemical Safety Board"},
|
||||
"fulltext": {
|
||||
"status": "csb_sitemap",
|
||||
"engine": engine,
|
||||
|
||||
@@ -58,6 +58,13 @@ SCAN_TARGETS: list[tuple[str, str | None]] = [
|
||||
("Videos", "video"),
|
||||
]
|
||||
|
||||
# 안전 자료실 A-2 — additional watch 타깃별 자료유형 (폴더 = deterministic 축).
|
||||
# 키 = 타깃 경로의 마지막 성분. KGS Code = 법정 위임 상세기준 = law/KR (plan 0-1 확정).
|
||||
# B-4 에서 Books(book)/Manuals(manual)/Papers_Purchased(paper) + license 주입 표로 확장.
|
||||
_TARGET_MATERIAL: dict[str, tuple[str, str | None]] = {
|
||||
"KGS_Code": ("law", "KR"),
|
||||
}
|
||||
|
||||
|
||||
def should_skip(path: Path) -> bool:
|
||||
if path.name in SKIP_NAMES or path.name.startswith("._"):
|
||||
@@ -242,6 +249,11 @@ async def watch_inbox():
|
||||
if not scan_root.exists():
|
||||
continue
|
||||
|
||||
# 안전 자료실 A-2 — 타깃 폴더 기반 자료유형 (없으면 (None, None))
|
||||
target_mt, target_jur = _TARGET_MATERIAL.get(
|
||||
Path(sub).name, (None, None)
|
||||
)
|
||||
|
||||
for file_path in scan_root.rglob("*"):
|
||||
if not file_path.is_file() or should_skip(file_path):
|
||||
continue
|
||||
@@ -275,6 +287,9 @@ async def watch_inbox():
|
||||
source_channel="drive_sync",
|
||||
category=category,
|
||||
needs_conversion=needs_conversion,
|
||||
# 안전 자료실 A-2 — watch 타깃 매핑 (KGS=law/KR 등, 비대상=NULL)
|
||||
material_type=target_mt,
|
||||
jurisdiction=target_jur,
|
||||
)
|
||||
session.add(doc)
|
||||
await session.flush()
|
||||
|
||||
@@ -23,7 +23,7 @@ import hashlib
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
@@ -60,6 +60,21 @@ _GUIDE_DAILY_CAP = int(os.getenv("KOSHA_GUIDE_DAILY_CAP", "25"))
|
||||
_MAX_FILE_BYTES = 50 * 1024 * 1024
|
||||
_DOWNLOAD_DELAY = (2.0, 5.0) # portal.kosha.or.kr 파일서버 — 연속 다운로드 간격
|
||||
|
||||
# 안전 자료실 A-2 — KOSHA 산출물 라이선스 (KOGL 유형 미확정 → 보수적 redistribute=False,
|
||||
# 근거 확보 시 완화. 0-3 license 메타 deterministic 주입).
|
||||
_KOSHA_LICENSE = {"scheme": "kogl", "redistribute": False, "attribution": "한국산업안전보건공단(KOSHA)"}
|
||||
|
||||
|
||||
def _ymd_to_date(ymd: str | None) -> date | None:
|
||||
"""'YYYYMMDD'/'YYYY-MM-DD' → date. 형식 불일치는 None (fail-quiet — 날짜는 보조 축)."""
|
||||
digits = re.sub(r"\D", "", ymd or "")
|
||||
if len(digits) != 8:
|
||||
return None
|
||||
try:
|
||||
return date(int(digits[:4]), int(digits[4:6]), int(digits[6:8]))
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _api_key() -> str:
|
||||
key = os.getenv("KOSHA_API_KEY", "")
|
||||
@@ -155,7 +170,11 @@ async def _ingest_attachment(session, boardno: str, filenm: str, filepath: str)
|
||||
import_source="kosha_api",
|
||||
edit_url=filepath,
|
||||
ai_tags=["Safety/KOSHA재해사례/첨부"],
|
||||
extract_meta={"kosha": {"boardno": boardno, "kind": "case_attachment"}},
|
||||
# 안전 자료실 A-2 — ingest 시점 deterministic (classify 경유해도 LLM 비의존)
|
||||
material_type="incident",
|
||||
jurisdiction="KR",
|
||||
extract_meta={"kosha": {"boardno": boardno, "kind": "case_attachment"},
|
||||
"license": dict(_KOSHA_LICENSE)},
|
||||
)
|
||||
session.add(doc)
|
||||
await session.flush()
|
||||
@@ -213,12 +232,16 @@ async def collect_disaster_cases(session) -> int:
|
||||
ai_domain="Safety",
|
||||
ai_sub_group=_CASE_SOURCE,
|
||||
ai_tags=[f"Safety/KOSHA재해사례/{business or '기타'}"],
|
||||
# 안전 자료실 A-2 — ingest 시점 deterministic (classify-skip 경로)
|
||||
material_type="incident",
|
||||
jurisdiction="KR",
|
||||
extract_meta={
|
||||
"source_id": source.id,
|
||||
"source_name": _CASE_SOURCE,
|
||||
"published_at": None,
|
||||
"kosha": {"boardno": boardno, "business": business,
|
||||
"atcflcnt": item.get("atcflcnt")},
|
||||
"license": dict(_KOSHA_LICENSE),
|
||||
},
|
||||
)
|
||||
session.add(doc)
|
||||
@@ -307,8 +330,13 @@ async def collect_kosha_guide(session, cap: int = _GUIDE_DAILY_CAP) -> int:
|
||||
import_source="kosha_api",
|
||||
edit_url=spec["url"],
|
||||
ai_tags=["Safety/KOSHA GUIDE"],
|
||||
# 안전 자료실 A-2 — GUIDE = 구속력 없는 권고 기술지침 (law 아님, plan 0-1)
|
||||
material_type="guide",
|
||||
jurisdiction="KR",
|
||||
published_date=_ymd_to_date(spec["ymd"]),
|
||||
extract_meta={"kosha": {"kind": "guide", "techGdlnNo": spec["no"],
|
||||
"ofancYmd": spec["ymd"]}},
|
||||
"ofancYmd": spec["ymd"]},
|
||||
"license": dict(_KOSHA_LICENSE)},
|
||||
)
|
||||
session.add(doc)
|
||||
await session.flush()
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import os
|
||||
import re
|
||||
from datetime import datetime, timezone
|
||||
from datetime import date, datetime, timezone
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
@@ -262,6 +262,16 @@ async def _save_law_split(
|
||||
f"개정구분: {revision_type}"
|
||||
)
|
||||
|
||||
# 안전 자료실 A-2 — 공포일 파싱 (law published_date = COALESCE(시행일, 공포일) 계약,
|
||||
# 본 레거시 워커는 공포일만 보유 — 시행일 기반 버전 체인은 B-1 statute_collector 소관)
|
||||
_digits = re.sub(r"\D", "", str(proclamation_date or ""))
|
||||
pub_date = None
|
||||
if len(_digits) == 8:
|
||||
try:
|
||||
pub_date = date(int(_digits[:4]), int(_digits[4:6]), int(_digits[6:8]))
|
||||
except ValueError:
|
||||
pub_date = None
|
||||
|
||||
doc = Document(
|
||||
file_path=rel_path,
|
||||
file_hash=file_hash(file_path),
|
||||
@@ -272,6 +282,13 @@ async def _save_law_split(
|
||||
source_channel="law_monitor",
|
||||
data_origin="work",
|
||||
category="law",
|
||||
# 안전 자료실 A-2 — ingest 시점 deterministic. 법령 텍스트 = 저작권법 제7조
|
||||
# 비보호 저작물 (public domain). 본 워커는 휴면(LAW_OC 미설정)이나 코드 경로 유지.
|
||||
material_type="law",
|
||||
jurisdiction="KR",
|
||||
published_date=pub_date,
|
||||
extract_meta={"license": {"scheme": "public_domain", "redistribute": True,
|
||||
"attribution": "국가법령정보센터"}},
|
||||
user_note=note or None,
|
||||
)
|
||||
session.add(doc)
|
||||
|
||||
@@ -341,11 +341,35 @@ def _entry_body(source: NewsSource, entry, summary: str) -> tuple[str, str]:
|
||||
|
||||
def _build_extract_meta(source: NewsSource, pub_dt: datetime) -> dict:
|
||||
"""fulltext_worker / 패널이 쓰는 출처 메타 (documents 에 source FK 가 없어 여기 기록)."""
|
||||
return {
|
||||
meta = {
|
||||
"source_id": source.id,
|
||||
"source_name": source.name,
|
||||
"published_at": pub_dt.isoformat(),
|
||||
}
|
||||
# 안전 자료실 A-2: 소스 레지스트리의 라이선스를 deterministic 주입 (0-3 license 메타).
|
||||
# P3 다이제스트/발행류가 redistribute=false 소스를 구조적으로 제외하는 게이트 입력.
|
||||
if source.license_scheme:
|
||||
meta["license"] = {
|
||||
"scheme": source.license_scheme,
|
||||
"redistribute": bool(source.license_redistribute),
|
||||
"attribution": source.name,
|
||||
}
|
||||
return meta
|
||||
|
||||
|
||||
def _material_axis(source: NewsSource) -> tuple[str | None, str | None]:
|
||||
"""안전 자료실 분류 축 (material_type, jurisdiction) — 레지스트리 deterministic.
|
||||
|
||||
- material_type = news_sources.material_type (NULL = 비대상, 뉴스/철학 등)
|
||||
- jurisdiction = source.country 전파. 단 paper 는 NULL 강제
|
||||
(국제 학술지에 관할 개념 부적합 — plan 0-1 계약. 레지스트리 country=US 여도 미전파).
|
||||
"""
|
||||
mt = source.material_type
|
||||
if not mt:
|
||||
return None, None
|
||||
if mt == "paper":
|
||||
return mt, None
|
||||
return mt, source.country
|
||||
|
||||
|
||||
def _doc_identity(source: NewsSource, source_short: str, category: str) -> dict:
|
||||
@@ -354,17 +378,22 @@ def _doc_identity(source: NewsSource, source_short: str, category: str) -> dict:
|
||||
file_path 접두사가 곧 채널 디렉토리. ai_domain 은 다이제스트/검색 필터의 분기 축이라
|
||||
crawl 채널이 'News' 를 오염시키지 않게 분리 (0-5 채널 레벨 분리 사상).
|
||||
"""
|
||||
material_type, jurisdiction = _material_axis(source)
|
||||
if source.source_channel == "crawl":
|
||||
domain = category if category and category != "Other" else "Domain"
|
||||
return {
|
||||
"path_prefix": "crawl",
|
||||
"ai_domain": domain,
|
||||
"ai_tags": [f"{domain}/{source_short}"],
|
||||
"material_type": material_type,
|
||||
"jurisdiction": jurisdiction,
|
||||
}
|
||||
return {
|
||||
"path_prefix": "news",
|
||||
"ai_domain": "News",
|
||||
"ai_tags": [f"News/{source_short}/{category}"],
|
||||
"material_type": material_type,
|
||||
"jurisdiction": jurisdiction,
|
||||
}
|
||||
|
||||
|
||||
@@ -528,6 +557,10 @@ async def _fetch_rss(session, source: NewsSource) -> tuple[int, str]:
|
||||
ai_domain=ident["ai_domain"],
|
||||
ai_sub_group=source_short,
|
||||
ai_tags=ident["ai_tags"],
|
||||
# 안전 자료실 A-2 — 레지스트리 deterministic (classify-skip 경로라 ingest 시점 필수)
|
||||
material_type=ident["material_type"],
|
||||
jurisdiction=ident["jurisdiction"],
|
||||
published_date=pub_dt.date() if pub_dt else None,
|
||||
extract_meta=_build_extract_meta(source, pub_dt),
|
||||
)
|
||||
session.add(doc)
|
||||
@@ -661,6 +694,10 @@ async def _fetch_api_guardian(session, source: NewsSource) -> tuple[int, str]:
|
||||
ai_domain=ident["ai_domain"],
|
||||
ai_sub_group=source_short,
|
||||
ai_tags=ident["ai_tags"],
|
||||
# 안전 자료실 A-2 — 레지스트리 deterministic (classify-skip 경로라 ingest 시점 필수)
|
||||
material_type=ident["material_type"],
|
||||
jurisdiction=ident["jurisdiction"],
|
||||
published_date=pub_dt.date() if pub_dt else None,
|
||||
extract_meta=_build_extract_meta(source, pub_dt),
|
||||
)
|
||||
session.add(doc)
|
||||
@@ -757,6 +794,10 @@ async def _fetch_api_nyt(session, source: NewsSource) -> tuple[int, str]:
|
||||
ai_domain=ident["ai_domain"],
|
||||
ai_sub_group=source_short,
|
||||
ai_tags=ident["ai_tags"],
|
||||
# 안전 자료실 A-2 — 레지스트리 deterministic (classify-skip 경로라 ingest 시점 필수)
|
||||
material_type=ident["material_type"],
|
||||
jurisdiction=ident["jurisdiction"],
|
||||
published_date=pub_dt.date() if pub_dt else None,
|
||||
extract_meta=_build_extract_meta(source, pub_dt),
|
||||
)
|
||||
session.add(doc)
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
-- 352_news_sources_material_type.sql
|
||||
-- 안전 자료실 A-2 (1/4) — 소스 레지스트리에 자료유형 기본값.
|
||||
-- plan: safety-library-1 A-2. 수집기 ingest 시점 deterministic 부여의 단일 진실 =
|
||||
-- 레지스트리 행 (country 와 동일 패턴 — 코드 하드코딩/이름 매칭 회피).
|
||||
-- NULL = 자료유형 비대상 (뉴스/철학 등). paper 소스는 country 가 있어도
|
||||
-- documents.jurisdiction 은 NULL (국제 학술지 — 코드 레벨 규칙).
|
||||
ALTER TABLE news_sources ADD COLUMN IF NOT EXISTS material_type TEXT
|
||||
CHECK (material_type IN ('law', 'paper', 'book', 'incident', 'manual', 'standard', 'guide'));
|
||||
@@ -0,0 +1,6 @@
|
||||
-- 353_news_sources_license_scheme.sql
|
||||
-- 안전 자료실 A-2 (2/4) — 소스별 라이선스 scheme (0-3 license 메타 deterministic 주입).
|
||||
-- kogl(공공누리류) / ogl(UK) / public_domain(미 연방) / proprietary / unknown.
|
||||
-- 미확정 소스는 보수적으로 unknown/proprietary + redistribute=false 에서 시작
|
||||
-- (갱신은 근거 확보 시 완화 방향 — 보수적=빡빡 원칙).
|
||||
ALTER TABLE news_sources ADD COLUMN IF NOT EXISTS license_scheme TEXT;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 354_news_sources_license_redistribute.sql
|
||||
-- 안전 자료실 A-2 (3/4) — 재배포 가능 여부. P3 다이제스트/발행류의 구조 게이트 입력
|
||||
-- (redistribute=false 소스 제외 — 사람 기억 의존 차단, 0-3).
|
||||
ALTER TABLE news_sources ADD COLUMN IF NOT EXISTS license_redistribute BOOLEAN;
|
||||
@@ -0,0 +1,45 @@
|
||||
-- 355_news_sources_material_seed.sql
|
||||
-- 안전 자료실 A-2 (4/4) — 기존 안전/공학 소스 12행 material_type + license 시드.
|
||||
-- 매핑 근거 = plan safety-library-1 0-1 경계 확정 (2026-06-12 prod 레지스트리 실측 대조):
|
||||
-- law=입법예고(신호) / incident=HSE·KOSHA사례·CSB·CCPS / guide=KOSHA GUIDE·TWI
|
||||
-- / standard=NB·API 공지 / paper=JPVT·arXiv (jurisdiction 은 코드에서 NULL 강제).
|
||||
-- 뉴스/철학 소스는 NULL 유지 (자료유형 비대상). 이름 키 = 시드 마이그레이션이 부여한 고정값.
|
||||
UPDATE news_sources SET
|
||||
material_type = CASE name
|
||||
WHEN '고용노동부 입법행정예고' THEN 'law'
|
||||
WHEN 'UK HSE Press' THEN 'incident'
|
||||
WHEN 'KOSHA 재해사례' THEN 'incident'
|
||||
WHEN 'US CSB 사고조사보고서' THEN 'incident'
|
||||
WHEN 'CCPS Process Safety Beacon' THEN 'incident'
|
||||
WHEN 'KOSHA GUIDE' THEN 'guide'
|
||||
WHEN 'TWI Job Knowledge' THEN 'guide'
|
||||
WHEN 'National Board 기술 아티클' THEN 'standard'
|
||||
WHEN 'API 표준 공지' THEN 'standard'
|
||||
WHEN 'ASME J. Pressure Vessel Technology' THEN 'paper'
|
||||
WHEN 'arXiv physics.app-ph' THEN 'paper'
|
||||
WHEN 'arXiv cond-mat.mtrl-sci' THEN 'paper'
|
||||
END,
|
||||
license_scheme = CASE name
|
||||
WHEN '고용노동부 입법행정예고' THEN 'kogl'
|
||||
WHEN 'KOSHA 재해사례' THEN 'kogl'
|
||||
WHEN 'KOSHA GUIDE' THEN 'kogl'
|
||||
WHEN 'UK HSE Press' THEN 'ogl'
|
||||
WHEN 'US CSB 사고조사보고서' THEN 'public_domain'
|
||||
WHEN 'TWI Job Knowledge' THEN 'proprietary'
|
||||
WHEN 'National Board 기술 아티클' THEN 'proprietary'
|
||||
WHEN 'API 표준 공지' THEN 'proprietary'
|
||||
WHEN 'CCPS Process Safety Beacon' THEN 'proprietary'
|
||||
WHEN 'ASME J. Pressure Vessel Technology' THEN 'proprietary'
|
||||
WHEN 'arXiv physics.app-ph' THEN 'unknown'
|
||||
WHEN 'arXiv cond-mat.mtrl-sci' THEN 'unknown'
|
||||
END,
|
||||
license_redistribute = CASE name
|
||||
WHEN 'UK HSE Press' THEN TRUE
|
||||
WHEN 'US CSB 사고조사보고서' THEN TRUE
|
||||
ELSE FALSE
|
||||
END
|
||||
WHERE name IN ('고용노동부 입법행정예고', 'UK HSE Press', 'KOSHA 재해사례',
|
||||
'US CSB 사고조사보고서', 'CCPS Process Safety Beacon', 'KOSHA GUIDE',
|
||||
'TWI Job Knowledge', 'National Board 기술 아티클', 'API 표준 공지',
|
||||
'ASME J. Pressure Vessel Technology', 'arXiv physics.app-ph',
|
||||
'arXiv cond-mat.mtrl-sci');
|
||||
Reference in New Issue
Block a user