Technical Briefing: Horizon

The Problem

Horizon aggregates news from diverse sources—Hacker News, Reddit, Twitter/X, RSS, Telegram, GitHub, and OpenBB financial watchlists—then scores, deduplicates, and briefs them via AI. The core tension: each source amplifies noise, and without tight coupling between fetch, score, filter, and deliver stages, a change to one ripple widely through the pipeline. The hub module (src/models.py) is depended on by 33 other modules, meaning any structural change there broadcasts across the entire codebase.

What This Does

Horizon is a daily news briefing engine. It fetches items from configured sources, deduplicates by URL, scores each with an LLM, enriches with community discussion, filters by thresholds, and outputs ranked briefings in English and Chinese. Key files and their effects:

  • src/main.py — entry point; reaches 113 functions, orchestrates the full pipeline
  • src/orchestrator.py — run reaches 149 functions; fetches all sources, determines time windows, progresses items through the pipeline, and writes summaries to disk
  • src/mcp/server.py — contains hz_fetch_items (reaches 90 functions), hz_enrich_items, hz_filter_items, and hz_generate_summary; serves as the MCP adapter layer
  • src/models.py — hub module with 21 classes and 5 functions (validate_delivery, validate_platform, etc.); depended on by 33 modules, so changes here have blast-radius 33
  • src/ai/client.py — complete calls a model for inference; 7 other classes (AnthropicClient, OpenAIClient, AzureOpenAIClient, MiniMaxClient)
  • src/scrapers/ — 8 scraper modules (rss.py, twitter.py, reddit.py, github.py, hackernews.py, openbb.py, ossinsight.py, telegram.py) each make outbound network calls
  • src/services/webhook.py — 31 functions; handles rendering, truncation, HTML stripping, and markdown conversion for delivery channels
  • src/services/email.py — send_daily_summary sends via email; also check_subscriptions

The pipeline flows: main → orchestrator.run → fetch from sources → deduplicate → score via ai/client.complete → enrich via ai/enricher.py → filter → summarize via mcp/server.hz_generate_summary → deliver via webhook or email.

How It Is Wired

Execution starts at src/main.py:34, which reaches 113 functions and is called from 1 place. The primary entry for the daily run is src/orchestrator.py:50 (run), which reaches 149 functions and is called by nothing else in the repo—it is the top-level orchestrator. From run, control flows to fetch_all_sources → _fetch_with_progress, then items move through hz_fetch_items → create_run → enrichment → filtering → summary generation.

Traced paths to effects outside the process:

  • main → load_config [filesystem via config_path.read_text]
  • run → check_subscriptions [network via mail.fetch]
  • hz_enrich_items → enrich_items → save_items → write_json [filesystem via path.write_text]
  • hz_fetch_items → fetch_items → create_run [filesystem via run_dir.mkdir]
  • hz_generate_summary → generate_summary → save_summary [filesystem via path.write_text]
  • Network calls radiate from 8 scraper files and src/services/email.py and src/services/webhook.py

The module graph shows src/orchestrator with indegree 1 and outdegree 19 (instability 0.95), making it the highest-change-risk point outside the hub. src/models has indegree 33 and outdegree 0 (instability 0)—pure sink, but any modification propagates to 33 callers.

How To Use It

Setup: This project uses uv for dependency management (per the badge in README and pyproject.toml). Install with:

uv sync

The pyproject.toml pins python^3.11 but has no uv.lock commit hash lock; the analysis flags this as a non-reproducible-build risk.

Configuration: Copy data/config.example.json to data/config.json and set required keys. Environment variables are documented in .env.example (file present at repo root). The config file is read in src/main.py via config_path.read_text.

Running it: The daily briefing is triggered by:

uv run src/main.py

or via the orchestrator:

uv run src/orchestrator.py

The GitHub Action .github/workflows/daily-summary.yml runs this on a schedule; it invokes the orchestrator which writes output and dispatches via configured channels.

Real-World Use

A product team wants a daily English/Chinese briefing filtered by subscriber interests. They configure data/config.json with their source list, set SCORING_MODEL=claude-3-5-sonnet, adjust MIN_SCORE=7, and point the webhook URL to their internal dashboard. The orchestrator runs nightly, produces a ranked list, enriches each item with community discussion summaries, and pushes the briefing via Feishu webhook (src/services/webhook.py) and email (src/services/email.py). If a story appears on both Reddit and Twitter, the deduplication in src/models.py merges them before scoring, so the subscriber sees one entry with consolidated context.

Code Health & Issues

The static analysis (19 findings, 4 high, 15 medium, 0 low) reports:

  • [HIGH] Hub module — src/models.py depended on by 33 modules; high-blast-radius stability risk. Keep it small; move volatile logic out.
  • [HIGH/cognitive_load] Deep nesting x8 — src/services/email.py, src/ai/utils.py, src/services/webhook.py; max indentation depth 11. Flatten with guard clauses.
  • [HIGH/cognitive_load] Oversized file x2 — src/services/webhook.py (694 lines) and tests/test_webhook.py; hard to hold in one head. Split into cohesive units.
  • [HIGH/clarity] Duplicated code blocks — 32 repeated 6-line blocks across 14 files (src/ai/analyzer.py, src/ai/enricher.py, src/ai/summarizer.py, src/orchestrator.py). Extract shared helpers.
  • [HIGH] Pin third-party GitHub Actions to commit SHA — .github/workflows uses astral-sh/setup-uv@v3, peaceiris/actions-gh-pages@v4; tags can move, so actions run with whatever owner last pushed. Replace @vN with 40-char commit SHAs.
  • [MEDIUM] Broad exception handling x5 — src/mcp/service.py, src/scrapers/rss.py, src/ai/enricher.py; bare except swallows errors indiscriminately. Catch specific exceptions; re-raise or log the rest.
  • [MEDIUM] File opened without context manager — src/ai/enricher.py; open(...) not wrapped in with. Use with open(...) as f: for deterministic close.
  • [MEDIUM] High branching density — src/ai/utils.py; 14 branch points over 45 lines. Decompose decision-heavy logic; consider strategy dispatch.
  • [MEDIUM] Enable Dependabot or Renovate — 1 manifest, no update bot. Without a bot, advisories sit unpatched.
  • [MEDIUM] Pin container base image by digest — Dockerfile uses python:3.11-slim (mutable tag). Use python:3.11-slim@sha256:<digest>.
  • [MEDIUM] Gate pull requests on dependency vulnerability scan — no dependency scan in CI. Add dependency-review-action on pull_request.
  • [MEDIUM] Add non-root USER to image — Dockerfile has no USER directive; process runs as root. Create unprivileged user, chown needed paths, end with USER.
  • [LOW] Set timeout-minutes on workflow jobs — .github/workflows/daily-summary.yml declares no job timeout; wedged steps run to six-hour default.

The Bottom Line

Horizon is a functional, well-scoped news-briefing engine that chains scrapers, AI scoring, deduplication, and delivery into a daily pipeline. The codebase is Python-heavy with solid MCP adapter layering and clear entry-point separation. However, the hub module (src/models.py) and orchestrator (src/orchestrator.py) carry wide blast radii, and the hygiene gaps—mutable GitHub Action tags, no dependency scanning, a root-run container, and no lockfile—are fixable with modest, targeted changes. It’s a solid starting point for a technical team that needs a customizable news radar, provided they lock dependencies, flatten the nesting in the webhook and AI utils, and add a dependabot/renovate config.