feat(safety): C-1 freshness — law_365d 폐기 + incident(재해사례) 흡수

★ranking 변경(의도 기록): freshness soft multiplier(floor 0.7) 정책 갱신.
- law_365d 폐기: 법령 현행성은 version_status(B-1 버전체인 current/superseded)가 처리.
  age-decay 는 current 법령을 부당 강등 → law_monitor/law 비적용으로 전환.
- incident 흡수(1행): material_type='incident'(KOSHA 재해사례/사망사고) → news_90d.
  시간 민감(최근 재해 가중), source_channel 무관(업로드 incident 포함).
- _DocMeta/_fetch_meta 에 material_type 추가(getattr 로 mock-safe).
테스트: law 3건(policy/decay/apply) 비적용 전환 + incident 2건 신규.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
hyungi
2026-06-13 15:23:04 +09:00
parent 3db351002c
commit f66b6e2f17
2 changed files with 58 additions and 27 deletions
+39 -13
View File
@@ -37,7 +37,8 @@ from services.search.freshness_decay import (
NOW = datetime(2026, 5, 3, 12, 0, 0, tzinfo=timezone.utc)
def _meta(channel: str | None, *, days_ago: float | None = 30.0, origin: str | None = None) -> _DocMeta:
def _meta(channel: str | None, *, days_ago: float | None = 30.0, origin: str | None = None,
material_type: str | None = None) -> _DocMeta:
if days_ago is None:
created = None
elif days_ago < 0:
@@ -45,7 +46,8 @@ def _meta(channel: str | None, *, days_ago: float | None = 30.0, origin: str | N
created = NOW + timedelta(days=-days_ago)
else:
created = NOW - timedelta(days=days_ago)
return _DocMeta(source_channel=channel, content_origin=origin, created_at=created)
return _DocMeta(source_channel=channel, content_origin=origin, created_at=created,
material_type=material_type)
# ─── policy dispatcher ────────────────────────────────────────────
@@ -55,8 +57,15 @@ def test_policy_news():
assert freshness_policy(_meta("news")) == "news_90d"
def test_policy_law_monitor():
assert freshness_policy(_meta("law_monitor")) == "law_365d"
def test_policy_law_monitor_now_unaffected():
# C-1 후속: law_365d 폐기 → law_monitor 비적용 (현행성은 version_status 가 처리)
assert freshness_policy(_meta("law_monitor")) is None
def test_policy_incident():
# C-1 후속: 재해사례/사망사고(material_type='incident') → news_90d 흡수 (source 무관)
assert freshness_policy(_meta("crawl", material_type="incident")) == "news_90d"
assert freshness_policy(_meta("inbox_route", material_type="incident")) == "news_90d"
def test_policy_manual_unaffected():
@@ -123,8 +132,9 @@ def test_decay_at_half_life_news():
assert compute_decay(90.0, "news_90d") == pytest.approx(0.5, rel=1e-6)
def test_decay_at_half_life_law():
assert compute_decay(365.0, "law_365d") == pytest.approx(0.5, rel=1e-6)
def test_decay_law_365d_removed_returns_one():
# C-1 후속: law_365d 폐기 → HALF_LIFE_DAYS 미등록 policy → decay 1.0 (no-op)
assert compute_decay(365.0, "law_365d") == 1.0
def test_decay_age_zero_full():
@@ -212,22 +222,38 @@ async def test_apply_news_recent_vs_old_recent_higher():
@pytest.mark.asyncio
async def test_apply_law_monitor_recent_vs_old_recent_higher():
# 가드 2: law_monitor recent 가 위
async def test_apply_law_monitor_now_unaffected():
# C-1 후속: law_monitor freshness 폐기 → recent/old 동일 score (재정렬 없음)
base = 0.50
rows = [
{"id": 1, "source_channel": "law_monitor", "content_origin": "extracted",
"created_at": NOW - timedelta(days=10)},
"material_type": "law", "created_at": NOW - timedelta(days=10)},
{"id": 2, "source_channel": "law_monitor", "content_origin": "extracted",
"created_at": NOW - timedelta(days=730)}, # 2년
"material_type": "law", "created_at": NOW - timedelta(days=730)},
]
session = _MockSession(rows)
results = [_result(1, base), _result(2, base)]
out = await apply_freshness_decay(results, session, now=NOW)
assert out[0].score == base and out[1].score == base
assert out[0].freshness_debug["freshness_policy"] is None
@pytest.mark.asyncio
async def test_apply_incident_recent_vs_old_recent_higher():
# C-1 후속: 재해사례(incident) recent 가 위 (news_90d 흡수, source_channel='crawl')
base = 0.50
rows = [
{"id": 1, "source_channel": "crawl", "content_origin": "extracted",
"material_type": "incident", "created_at": NOW - timedelta(days=5)},
{"id": 2, "source_channel": "crawl", "content_origin": "extracted",
"material_type": "incident", "created_at": NOW - timedelta(days=400)},
]
session = _MockSession(rows)
results = [_result(1, base), _result(2, base)]
out = await apply_freshness_decay(results, session, now=NOW)
assert out[0].id == 1
assert out[0].freshness_debug["freshness_policy"] == "law_365d"
# 2년 → law_365d 반감기 1년 → decay ~0.25 → multiplier ~ 0.775
assert out[1].score < out[0].score
assert out[0].score > out[1].score
assert out[0].freshness_debug["freshness_policy"] == "news_90d"
@pytest.mark.asyncio