SPB Git forge
28commits 1branches 0releases
7.7 MBsize
maindefault branch
10 days agolast push
Python 66.3% TypeScript 22.7% JavaScript 8.6% HTML 1.4% CSS 0.7%

events: bursts of news items become one aggregate communication event (titles kept as evidence)

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Simon-Pierre Boucher committed 11 days ago (Sep 13, 2026) parent 75ba9ab

2 changed files +36 −3

modified src/companyatlas/services/events.py +23 −3
@@ -43,6 +43,7 @@ MAX_ENTITY_ITEMS = 50 # bounded entity lists on aggregate events
43 43 PER_JOB_EVENT_MAX = 5 # NEW_JOB per job only when ≤ 5 jobs added
44 44 PER_PERSON_EVENT_MAX = 10
45 45 NEWS_ITEMS_MAX = 20
46 +NEWS_ITEMS_PER_EVENT_MAX = 3 # more items than this in one observation → one aggregate communication event
46 47 LEADERSHIP_AGGREGATE_MIN = 3
47 48 SURGE_MIN_JOBS = 10 # fallback thresholds when no baseline is available yet
48 49 SURGE_MIN_RATIO = 0.5
@@ -525,10 +526,29 @@ def _news_rules(delta: dict[str, Any], surface: str, evidence: str) -> list[Even
525 526 prefix = {"NEWS_RELEASE": "News release", "BLOG_POST": "Blog post", "CHANGELOG_ENTRY": "Changelog entry", "INVESTOR_UPDATE": "Investor update",
526 527 "EARNINGS_RELEASE": "Earnings release"}
527 528 out: list[EventDraft] = []
528 − for item in list(news.get("added") or [])[:NEWS_ITEMS_MAX]:
529 + added = [i for i in list(news.get("added") or []) if (i.get("title") or "").strip()]
530 + if len(added) > NEWS_ITEMS_PER_EVENT_MAX:
531 + # A burst of items in one observation (catalogue re-listing, archive page, many posts at once) is one communication event,
532 + # not a flood: individual titles stay in `entities.news` for the evidence drawer.
533 + subtypes = [_news_subtype(i, surface) for i in added]
534 + subtype = max(set(subtypes), key=subtypes.count)
535 + label = {"NEWS_RELEASE": "news releases", "BLOG_POST": "blog posts", "CHANGELOG_ENTRY": "changelog entries", "INVESTOR_UPDATE": "investor updates",
536 + "EARNINGS_RELEASE": "earnings releases"}[subtype]
537 + titles = [(i.get("title") or "").strip() for i in added]
538 + tags = ["news", subtype.lower()]
539 + if any(_is_ai(t) for t in titles):
540 + tags.append("ai")
541 + if any(_LAUNCH_RE.search(t) for t in titles):
542 + tags.append("launch")
543 + out.append(EventDraft(
544 + subtype=subtype, title=f"{len(added)} new {label} published on {_label(surface)}",
545 + entity_key=f"news_batch:{normalize_entity_key(titles[0])}:{len(added)}", evidence=evidence,
546 + summary="Latest: " + " · ".join(t[:80] for t in titles[:3]) + (" …" if len(titles) > 3 else ""),
547 + entities={"news": [{"title": i.get("title"), "url": i.get("url"), "published_at": i.get("published_at")} for i in added[:NEWS_ITEMS_MAX]]},
548 + payload={"count": len(added), "category": added[0].get("category")}, tags=tags, magnitude=min(1.0, len(added) / 20)))
549 + return out
550 + for item in added[:NEWS_ITEMS_MAX]:
529 551 title = (item.get("title") or "").strip()
530 − if not title:
531 − continue
532 552 subtype = _news_subtype(item, surface)
533 553 tags = ["news", subtype.lower()]
534 554 if _is_ai(title):
modified tests/test_events_rules.py +13 −0
@@ -330,3 +330,16 @@ async def test_review_queue_and_llm_jobs_for_legal_change(monkeypatch):
330 330
331 331 def test_kind_enum_values_used_by_rules():
332 332 assert ChangeKind.NOISE == "noise" and ChangeKind.CRITICAL == "critical"
333 +
334 +
335 +def test_news_bursts_become_one_aggregate_event() -> None:
336 + from companyatlas.services.events import derive_events
337 +
338 + items = [{"title": f"Release {i}: quarterly update", "url": f"https://x.com/news/{i}", "published_at": None, "category": "press"} for i in range(9)]
339 + change = {"id": "chg_x", "sensor_id": "sen_x", "surface": "newsroom", "significance": 0.55, "kind": "meaningful", "diff": {}, "structured_delta": {"news": {"added": items}}}
340 + drafts = derive_events(change, {"id": "co_x", "display_name": "X"}, {"id": "sen_x", "surface": "newsroom", "connector_id": "generic-html-v1", "url": "https://x.com/news"}).events
341 + news = [d for d in drafts if d.subtype == "NEWS_RELEASE"]
342 + assert len(news) == 1 and news[0].title.startswith("9 new news releases published on") and len(news[0].entities["news"]) == 9
343 + small = dict(change, structured_delta={"news": {"added": items[:2]}})
344 + drafts = derive_events(small, {"id": "co_x", "display_name": "X"}, {"id": "sen_x", "surface": "newsroom", "connector_id": "generic-html-v1", "url": "https://x.com/news"}).events
345 + assert len([d for d in drafts if d.subtype == "NEWS_RELEASE"]) == 2
333 346