Files
last30days-skill/docs/plans/2026-02-24-feat-hacker-news-source-plan.md
T
Matt Van Horn 38a7ea253e feat(hackernews): add Hacker News as 5th research source
Add HN search via free Algolia API (no key needed). Two-phase approach:
search for stories, then enrich top ones with comments. Integrated into
the full pipeline (normalize, score, dedupe, render) running in parallel
with Reddit/X/YouTube. Source priority: Reddit > X > HN > YouTube > Web.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-24 18:33:31 -08:00

236 lines
11 KiB
Markdown

---
title: "feat: Add Hacker News as a 5th research source"
type: feat
status: completed
date: 2026-02-24
---
# feat: Add Hacker News as a 5th Research Source
## Overview
Add Hacker News as a source to the last30days skill, using the free Algolia HN Search API (`hn.algolia.com/api/v1`). HN provides high-signal content from a technical audience — stories with high point counts and active comment threads are strong indicators of what the developer community actually cares about. No API key required.
## Problem Statement / Motivation
The skill currently covers Reddit, X, YouTube, and web. Hacker News is missing — and for technical topics, HN often surfaces discussions that don't appear on Reddit or X. HN's upvote system and comment culture produce high-quality signal: a 500-point story with 200 comments means the developer community is genuinely engaged. Community contributor @wkbaran proposed this in PR #26 alongside YouTube and Product Hunt. YouTube shipped in v2.1; now it's time for HN.
## Proposed Solution
Add `scripts/lib/hackernews.py` following the exact same pattern as `youtube_yt.py` (the simplest existing source — no API key, just HTTP calls). Use the Algolia HN Search API for discovery, then optionally fetch top comments from high-scoring stories for enrichment (like Reddit enrichment, but using the `/items/:id` endpoint instead of Reddit's JSON API).
### Two-phase approach (matches existing Reddit pattern):
1. **Phase 1 — Search**: Query Algolia for stories matching the topic within the date range. Get titles, URLs, points, comment counts.
2. **Phase 2 — Enrichment** (optional, top N stories): Fetch the `/items/:id` endpoint for the highest-scoring stories to get top-level comments. This gives "comment_insights" like Reddit enrichment does.
## Technical Approach
### Files to Create
#### `scripts/lib/hackernews.py`
The main source module. Pattern matches `youtube_yt.py` (simplest source).
```python
# scripts/lib/hackernews.py
"""Hacker News search via Algolia API (free, no auth required)."""
ALGOLIA_SEARCH_URL = "https://hn.algolia.com/api/v1/search"
ALGOLIA_SEARCH_BY_DATE_URL = "https://hn.algolia.com/api/v1/search_by_date"
ALGOLIA_ITEM_URL = "https://hn.algolia.com/api/v1/items"
DEPTH_CONFIG = {
"quick": 15,
"default": 30,
"deep": 60,
}
ENRICH_LIMITS = {
"quick": 3,
"default": 5,
"deep": 10,
}
```
Key functions:
- `search_hackernews(topic, from_date, to_date, depth="default") -> Dict[str, Any]`
- Calls `hn.algolia.com/api/v1/search?query={topic}&tags=story&numericFilters=created_at_i>{from_ts},created_at_i<{to_ts}&hitsPerPage={count}`
- Uses `http.get()` — stdlib only, matches existing pattern
- Returns raw Algolia response
- `parse_hackernews_response(response: Dict) -> List[Dict]`
- Extracts hits, maps to raw dicts with fields: `id` (prefix "HN"), `title`, `url`, `hn_url`, `author`, `date`, `engagement` (points, num_comments), `why_relevant`, `relevance`
- `relevance` estimated from Algolia rank + engagement boost (same pattern as Bill's PR)
- `enrich_top_stories(items, depth="default") -> List[Dict]`
- Fetches `/items/{objectID}` for top N stories (by points)
- Extracts top-level comments (author, text, points)
- Adds `top_comments` and `comment_insights` fields (same structure as Reddit enrichment)
- Uses `ThreadPoolExecutor` for parallel fetching
- `_date_to_unix(date_str: str) -> int` — Helper, converts YYYY-MM-DD to Unix timestamp
#### `tests/test_hackernews.py`
Standard unittest pattern matching existing tests.
- Test `parse_hackernews_response` with sample Algolia response
- Test `_date_to_unix` conversion
- Test empty response handling
- Test enrichment parsing
- Test score integration with `score.py`
### Files to Modify
#### `scripts/lib/schema.py`
- [x] Add `HackerNewsItem` dataclass:
```python
@dataclass
class HackerNewsItem:
id: str # "HN1", "HN2", ...
title: str
url: str # Original article URL
hn_url: str # news.ycombinator.com/item?id=...
author: str # HN username
date: Optional[str]
date_confidence: str # Always "high" (Algolia provides exact timestamps)
engagement: Optional[Engagement] # points + num_comments
top_comments: List[Comment] # From enrichment
comment_insights: List[str] # From enrichment
relevance: float
why_relevant: str
subs: SubScores
score: int
```
- [x] Add `hackernews: List[HackerNewsItem] = field(default_factory=list)` to `Report`
- [x] Add `hackernews_error: Optional[str] = None` to `Report`
- [x] Update `Report.to_dict()` and `Report.from_dict()`
#### `scripts/lib/normalize.py`
- [x] Add `normalize_hackernews_items(items: List[Dict], from_date, to_date) -> List[schema.HackerNewsItem]`
- Maps raw dicts to `HackerNewsItem` dataclass instances
- Sets `date_confidence = "high"` (Algolia provides `created_at_i` exact timestamps)
- Converts `engagement` dict to `schema.Engagement(score=points, num_comments=num_comments)`
#### `scripts/lib/score.py`
- [x] Add `compute_hackernews_engagement_raw(engagement) -> float`
- Formula: `0.55 * log1p(points) + 0.45 * log1p(num_comments)`
- Points are the primary signal on HN; comments indicate depth of discussion
- [x] Add `score_hackernews_items(items) -> List[schema.HackerNewsItem]`
- Uses standard 45/25/30 weights (relevance/recency/engagement) — same as Reddit/X/YouTube
- [x] Update `sort_items()` to handle `HackerNewsItem`
- Source priority: Reddit > X > **HN** > YouTube > WebSearch
- HN slots between X and YouTube: higher signal than YouTube (curated upvotes vs raw views), but X has real-time pulse
#### `scripts/lib/dedupe.py`
- [x] Add `dedupe_hackernews(items, threshold=0.7) -> List[schema.HackerNewsItem]`
- [x] Update `get_item_text()` to handle `HackerNewsItem` (return `title`)
- [x] Consider cross-source dedup: HN stories often link to the same URLs that appear in web search results. Dedupe by URL match across `hackernews` and `websearch` items.
#### `scripts/lib/render.py`
- [x] Add HN section to `render_compact()`:
```
### Hacker News Stories
**HN1** (score:85) hn/username (2026-02-15) [350pts, 127cmt]
Story title here
https://news.ycombinator.com/item?id=12345
*Why relevant*
```
- [x] Add to `render_source_status()`:
```
✅ HN: {N} stories
```
- [x] Add to `render_full_report()` and `render_context_snippet()`
- [x] Update `_assess_data_freshness()` to include HN items
#### `scripts/last30days.py`
- [x] Import: `from lib import hackernews`
- [x] Add `_search_hackernews(topic, from_date, to_date, depth) -> (items, error)` wrapper function
- Calls `hackernews.search_hackernews()`, then `hackernews.parse_hackernews_response()`
- Returns `(items, None)` or `([], error_string)`
- [x] Add to `TIMEOUT_PROFILES`: `"hackernews_future": 60` (default), `30` (quick), `90` (deep)
- [x] Add HN to `ThreadPoolExecutor` in `run_research()`:
```python
if do_hackernews:
hn_future = executor.submit(_search_hackernews, topic, from_date, to_date, depth)
```
- Increment `max_workers` by 1 when HN is enabled
- [x] Collect results: `hn_items, hn_error = hn_future.result(timeout=hn_timeout)`
- [x] Add HN enrichment phase (after Reddit enrichment, before Phase 2):
```python
if hn_items:
hn_items = hackernews.enrich_top_stories(hn_items, depth=depth)
```
- [x] Add to processing pipeline: normalize -> filter_by_date_range -> score -> sort -> dedupe
- [x] Assign to `report.hackernews` and `report.hackernews_error`
- [x] Update status UI: add `⏳ 🟡 HN Searching Hacker News...` and `✓ 🟡 HN Found {N} stories`
#### `scripts/lib/env.py`
- [x] HN is always available (no API key, no binary dependency)
- [x] Update `get_available_sources()` to include HN in the source list
- [x] Update `validate_sources()` to accept `hn` as a valid source name
- [x] Add `hn` to the `--search` flag documentation
#### `SKILL.md`
- [x] Update description: "Sources: Reddit, X, YouTube, **Hacker News**, and web"
- [x] Add HN to stats block template:
```
├─ 🟡 HN: {N} stories │ {N} points │ {N} comments
```
- [x] Update `metadata.clawdbot.tags` to include `hackernews`
- [x] Update Security section: add `hn.algolia.com` to endpoints list
- [x] Update citation priority to include HN: Reddit > X > YouTube > HN > Web
## Acceptance Criteria
- [x] `python3 scripts/last30days.py "AI coding agents" --emit=compact` shows HN section with stories, points, and comment counts
- [x] HN stories include `hn_url` linking to the HN discussion page (not just the article URL)
- [x] Top stories are enriched with top comments (like Reddit enrichment)
- [x] HN runs in parallel with Reddit/X/YouTube (no serial bottleneck)
- [x] Scoring uses standard 45/25/30 weights with engagement formula tuned for HN metrics
- [x] Stats block shows: `├─ 🟡 HN: {N} stories │ {N} points │ {N} comments`
- [x] `--search=hn` works to run HN only; `--search=reddit,hn` works for combos
- [x] `--quick`, `--deep` flags adjust HN result count (15/30/60)
- [x] All existing tests still pass
- [x] New `tests/test_hackernews.py` with tests for parse, normalize, score, enrichment
- [x] No API key required — works out of the box
- [x] Cross-source URL dedup: HN stories linking to same URL as web results get deduped
- [x] SKILL.md updated with HN in stats block, security section, and citation priority
## Dependencies & Risks
**Low risk:**
- Algolia HN API is free, public, no auth, well-established (used since 2014)
- No new dependencies — uses existing `http.py` (stdlib urllib)
- Pattern is identical to YouTube source (simplest existing source)
**Medium risk:**
- Algolia has no officially documented rate limit, but aggressive use could get throttled
- Mitigation: Existing `http.py` exponential backoff handles 429s
- Default depth only requests 30 items (1 API call for search + N for enrichment)
- Comment enrichment adds N API calls (one per story) which could slow down quick mode
- Mitigation: Limit enrichment to top 3/5/10 stories by depth; use ThreadPoolExecutor
**Compatibility:**
- HN always available — doesn't break anything when other sources are missing
- Existing `--search` flag needs extension but is backward-compatible
## Sources & References
- PR #26 by @wkbaran: [HN implementation reference](https://github.com/mvanhorn/last30days-skill/pull/26)
- Algolia HN API docs: `https://hn.algolia.com/api`
- Existing patterns: `scripts/lib/youtube_yt.py` (simplest source), `scripts/lib/openai_reddit.py` (enrichment pattern)
- Scoring reference: `scripts/lib/score.py:compute_reddit_engagement_raw()`
- Schema reference: `scripts/lib/schema.py:RedditItem` (closest analog to HN)