| 43 | PER_JOB_EVENT_MAX = 5 # NEW_JOB per job only when ≤ 5 jobs added |
43 | PER_JOB_EVENT_MAX = 5 # NEW_JOB per job only when ≤ 5 jobs added |
| 44 | PER_PERSON_EVENT_MAX = 10 |
44 | PER_PERSON_EVENT_MAX = 10 |
| 45 | NEWS_ITEMS_MAX = 20 |
45 | NEWS_ITEMS_MAX = 20 |
| |
46 | +NEWS_ITEMS_PER_EVENT_MAX = 3 # more items than this in one observation → one aggregate communication event |
| 46 | LEADERSHIP_AGGREGATE_MIN = 3 |
47 | LEADERSHIP_AGGREGATE_MIN = 3 |
| 47 | SURGE_MIN_JOBS = 10 # fallback thresholds when no baseline is available yet |
48 | SURGE_MIN_JOBS = 10 # fallback thresholds when no baseline is available yet |
| 48 | SURGE_MIN_RATIO = 0.5 |
49 | SURGE_MIN_RATIO = 0.5 |
| 525 | prefix = {"NEWS_RELEASE": "News release", "BLOG_POST": "Blog post", "CHANGELOG_ENTRY": "Changelog entry", "INVESTOR_UPDATE": "Investor update", |
526 | prefix = {"NEWS_RELEASE": "News release", "BLOG_POST": "Blog post", "CHANGELOG_ENTRY": "Changelog entry", "INVESTOR_UPDATE": "Investor update", |
| 526 | "EARNINGS_RELEASE": "Earnings release"} |
527 | "EARNINGS_RELEASE": "Earnings release"} |
| 527 | out: list[EventDraft] = [] |
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 | title = (item.get("title") or "").strip() |
551 | title = (item.get("title") or "").strip() |
| 530 | − if not title: |
| |
| 531 | − continue |
| |
| 532 | subtype = _news_subtype(item, surface) |
552 | subtype = _news_subtype(item, surface) |
| 533 | tags = ["news", subtype.lower()] |
553 | tags = ["news", subtype.lower()] |
| 534 | if _is_ai(title): |
554 | if _is_ai(title): |