feat(news): index news packages and research persistent events

Extract reusable literal keywords once per source revision, assign complete news packages to long-lived events, and research all event members with immutable evidence and references.

Add additive research storage, vocabulary snapshots, the two-stage workspace, isolated acceptance coverage, and a paused rollout with an initial fixed 50-news production trial.
This commit is contained in:
eezd committed 2026-11-04 12:09:58 +08:00
1 parent 07252fea0f
commit 4929a32a0a
69 files changed
+8215 -2092

No files matched your search

+11 -18
View File
@@ -41,18 +41,16 @@ def test_reading_distinguishes_event_and_package_with_same_id(trading, monkeypat
assert {r['kind'] for r in result['reading']}=={'event','package'}
def test_members_are_aggregated_once_and_old_independent_package_expires(trading):
def test_reading_uses_event_rating_and_never_package_fallback(trading):
from alphapulse.news_risk.events import reading_inputs
now=datetime.now(UTC)
packages=[SimpleNamespace(id=i,updated_at=now if i<3 else now-timedelta(days=4),importance_stars=4,
result={'title':str(i)},published_revision=2,category='security',tags=[]) for i in (1,2,3)]
event=SimpleNamespace(id=1,updated_at=now,published_revision=2,result={'title':'Incident','importance_stars':2,'category':'security','timeline_points':[{'package_id':i} for i in (1,2)]})
event=SimpleNamespace(id=1,updated_at=now,published_revision=2,tags=['security'],
result={'title':'Incident','importance_stars':2,'category':'security'})
session=AsyncMock()
values=[packages,[SimpleNamespace(package_id=i,event_id=1) for i in (1,2)],[event]]
session.scalars.side_effect=[SimpleNamespace(all=lambda rows=rows:rows) for rows in values]
session.scalar.return_value=SimpleNamespace(revision=2)
session.scalars.return_value=SimpleNamespace(all=lambda:[event])
inputs=asyncio.run(reading_inputs(session,now-timedelta(hours=72)))
assert len(inputs)==1 and inputs[0]['kind']=='event' and inputs[0]['importance_stars']==4
assert len(inputs)==1 and inputs[0]['kind']=='event' and inputs[0]['importance_stars']==2
assert session.scalars.await_count==1
def test_reprocess_range_requires_order_and_timezone(trading):
@@ -72,18 +70,13 @@ def test_event_timeline_cannot_omit_a_member_or_invent_known_times(trading):
with pytest.raises(ValueError, match='已核验时间点'): validate_timeline(result,packages[:1])
def test_new_member_remains_readable_until_event_represents_it(trading):
def test_unpublished_events_do_not_emit_independent_packages(trading):
from alphapulse.news_risk.events import reading_inputs
now=datetime.now(UTC)
packages=[SimpleNamespace(id=i,updated_at=now,importance_stars=4,result={'title':str(i)},published_revision=1,
category='security',tags=[]) for i in (1,2,3)]
event=SimpleNamespace(id=1,updated_at=now,published_revision=1,result={'title':'Incident','importance_stars':4,'category':'security',
'timeline_points':[{'package_id':1},{'package_id':2}]})
session=AsyncMock()
rows=[packages,[SimpleNamespace(package_id=i,event_id=1) for i in (1,2,3)],[event]]
session.scalars.side_effect=[SimpleNamespace(all=lambda rows=rows:rows) for rows in rows]
inputs=asyncio.run(reading_inputs(session,now-timedelta(hours=72)))
assert [(p['kind'],p['id']) for p in inputs]==[('event',1),('package',3)]
session.scalars.return_value=SimpleNamespace(all=lambda:[])
assert asyncio.run(reading_inputs(session,datetime.now(UTC)-timedelta(hours=72)))==[]
sql=str(session.scalars.call_args.args[0])
assert 'published_revision = news_ai_events.revision' in sql
@pytest.mark.parametrize('withdraw_during_call', [False, True])