feat: v3.0.0 - intelligent search, GitHub person/project mode, ELI5, 13+ sources

v3 rewrites the search engine from the ground up:

- Intelligent pre-research: resolves X handles, GitHub repos, subreddits,
  TikTok hashtags, and YouTube channels before searching
- GitHub person-mode: PR velocity, top repos by stars, release notes
- GitHub project-mode: live star counts, README, releases, top issues
- ELI5 mode: plain language synthesis, no jargon
- 13+ sources: Reddit, X, YouTube, TikTok, Instagram, HN, Polymarket,
  GitHub, Threads, Pinterest, Perplexity, Bluesky, Web
- Free Reddit comments via public JSON (no API key needed)
- Fun judge v2: humor scoring baked into narrative
- Cookie consent before browser scanning
- 10,000 free ScrapeCreators calls
- 1,012 tests

Thank you to the community contributors whose issues and PRs shaped v3:
@uppinote20 (#143), @zerone0x (#134, #136), @thinkun (#116),
@thomasmktong (#124), @fanispoulinakisai-boop (#100), @pejmanjohn (#78),
@zl190 (#115), @hnshah (#84, #85, #86)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matt Van Horn
2026-04-08 10:52:23 -07:00
parent 61904b31e3
commit 0a9ff16dfc
397 changed files with 21427 additions and 53106 deletions
+78 -3
View File
@@ -18,6 +18,11 @@ from datetime import datetime, timedelta
from pathlib import Path
from typing import Any, Dict, List, Optional
SCRIPT_DIR = Path(__file__).parent.resolve()
sys.path.insert(0, str(SCRIPT_DIR))
from lib import schema
DB_DIR = Path.home() / ".local" / "share" / "last30days"
DB_PATH = DB_DIR / "research.db"
@@ -128,9 +133,7 @@ INSERT OR IGNORE INTO settings (key, value) VALUES ('default_schedule', '0 8 * *
"""
# Future migrations keyed by version number
MIGRATIONS: Dict[int, str] = {
# 2: "ALTER TABLE findings ADD COLUMN tags TEXT DEFAULT '[]';",
}
MIGRATIONS: Dict[int, str] = {}
def _connect(db_path: Optional[Path] = None) -> sqlite3.Connection:
@@ -577,6 +580,78 @@ def get_trending(days: int = 7) -> List[Dict[str, Any]]:
conn.close()
def finding_from_candidate(candidate: schema.Candidate) -> Dict[str, Any]:
"""Convert a ranked candidate into a persisted finding."""
primary_item = schema.candidate_primary_item(candidate)
corroborating_sources = [
source for source in schema.candidate_sources(candidate)
if source and source != candidate.source
]
summary = candidate.explanation or candidate.snippet or ""
if corroborating_sources:
prefix = f"Also seen in: {', '.join(corroborating_sources)}."
summary = f"{prefix} {summary}".strip()
body = (
primary_item.body
if primary_item and primary_item.body
else candidate.snippet or candidate.title
)
author = primary_item.author if primary_item and primary_item.author else ""
return {
"source": candidate.source or "unknown",
"source_url": candidate.url,
"source_title": candidate.title,
"author": author,
"content": body,
"summary": summary,
"engagement_score": candidate.engagement or 0,
"relevance_score": candidate.final_score or candidate.rerank_score or candidate.local_relevance,
}
def findings_from_report(
report: schema.Report,
*,
limit: Optional[int] = None,
) -> List[Dict[str, Any]]:
"""Convert report into persisted findings.
Uses ranked candidates (post-rerank) when available for quality scores and explanations.
Supplements with raw items from items_by_source for HN/PM that didn't rank highly
but are valuable for watchlist persistence.
"""
findings = []
seen_urls = set()
# Phase 1: Process ranked candidates (high-quality data with explanations and corroboration)
for candidate in report.ranked_candidates:
finding = finding_from_candidate(candidate)
findings.append(finding)
seen_urls.add(candidate.url)
# Phase 2: Add HN/PM items not already captured in ranked candidates
for source_name in ["hackernews", "polymarket"]:
if source_name not in report.items_by_source:
continue
for item in report.items_by_source[source_name]:
if item.url in seen_urls:
continue # Already captured with rich data
findings.append({
"source": source_name,
"source_url": item.url,
"source_title": item.title,
"author": item.author or "",
"content": item.body or "",
"summary": item.snippet or (item.body[:500] if item.body else ""),
"engagement_score": item.engagement_score or 0.0,
"relevance_score": item.local_relevance or 0.5,
})
seen_urls.add(item.url)
# Apply global limit after collecting all findings (fix: was per-source, now global)
return findings[:limit] if limit is not None else findings
# --- CLI interface ---