feat: press context analyzer prompt and renderer (#78, #79) (#109)

* feat: correlation badges for Hugo templates (#80) Add CSS-styled badges showing press correlation status next to repo entries. Three states: Press-correlated, Organic growth, and Hype risk. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * feat: press context analyzer prompt and renderer (#78, #79) Add prompts/analyze-press-context.md template for TechCrunch press context and scripts/render_press_context.py to render it with real crawl/correlation data. Includes tests and graceful fallback when no data exists. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

Juan Manuel Servera committed May 19, 2026 at 16:25 UTC b04026029ac339b9fc6347939907e78b20792a7b
3 files changed +368
prompts/analyze-press-context.md new
+21
@@ -0,0 +1,21 @@
1 +## Press Context (TechCrunch, week of {date})
2 +{article_count} articles published relevant to tech/open-source.
3 +
4 +Notable coverage:
5 +{articles_list}
6 +
7 +### Correlation Summary
8 +{correlation_count} repos have press correlation:
9 +{correlations_list}
10 +
11 +### Instructions
12 +For each trending repo, note if press coverage preceded the star surge.
13 +Label repos as:
14 +- '📰 Press-correlated' — stars gained after/during press coverage
15 +- '🌱 Organic growth' — stars gained without press coverage
16 +- '⚠️ Hype risk: {level}' — when hype_risk is medium or high
17 +
18 +Include a "Press vs Reality" subsection in your analysis highlighting:
19 +1. Press-hyped repos that are losing steam (high hype_risk)
20 +2. Organic gems without any press coverage
21 +3. Disconnects between press narrative and actual GitHub activity
scripts/render_press_context.py new
+152
@@ -0,0 +1,152 @@
1 +#!/usr/bin/env python3
2 +"""Render the press context prompt section with TechCrunch and correlation data.
3 +
4 +Reads crawl data and correlation data, then renders the press context
5 +prompt template with real values. Output can be piped into the analyzer.
6 +
7 +Usage:
8 + python scripts/render_press_context.py [--topic ai-ml] [--week 2026-W21]
9 +"""
10 +
11 +import argparse
12 +import json
13 +import sys
14 +from datetime import datetime
15 +from pathlib import Path
16 +
17 +# Allow imports when run from repo root or scripts/
18 +_REPO_ROOT = Path(__file__).resolve().parent.parent
19 +sys.path.insert(0, str(_REPO_ROOT / "scripts"))
20 +
21 +from topic_paths import raw_dir, analyzed_dir # noqa: E402
22 +
23 +
24 +def current_week() -> str:
25 + """Return the current ISO week as YYYY-WNN."""
26 + now = datetime.now()
27 + iso = now.isocalendar()
28 + return f"{iso[0]}-W{iso[1]:02d}"
29 +
30 +
31 +def load_json(path: Path) -> dict | None:
32 + """Load a JSON file, returning None if it doesn't exist."""
33 + if not path.exists():
34 + return None
35 + with open(path, "r", encoding="utf-8") as f:
36 + return json.load(f)
37 +
38 +
39 +def format_articles_list(articles: list[dict]) -> str:
40 + """Format articles into a markdown list."""
41 + if not articles:
42 + return "- (none)"
43 + lines = []
44 + for article in articles:
45 + title = article.get("title", "Untitled")
46 + url = article.get("url", "")
47 + categories = article.get("categories", [])
48 + cat_str = f" [{', '.join(categories)}]" if categories else ""
49 + if url:
50 + lines.append(f"- [{title}]({url}){cat_str}")
51 + else:
52 + lines.append(f"- {title}{cat_str}")
53 + return "\n".join(lines)
54 +
55 +
56 +def format_correlations_list(correlations: list[dict]) -> str:
57 + """Format correlations into a markdown list."""
58 + if not correlations:
59 + return "- (none)"
60 + lines = []
61 + for corr in correlations:
62 + repo = corr.get("repo", "unknown")
63 + match_type = corr.get("match_type", "unknown")
64 + confidence = corr.get("correlation_confidence", 0.0)
65 + hype_risk = corr.get("hype_risk", "none")
66 + lines.append(
67 + f"- {repo} — match: {match_type}, "
68 + f"confidence: {confidence:.1f}, hype_risk: {hype_risk}"
69 + )
70 + return "\n".join(lines)
71 +
72 +
73 +def render_press_context(
74 + techcrunch_data: dict | None, correlation_data: dict | None, week: str
75 +) -> str:
76 + """Render the press context prompt section.
77 +
78 + Args:
79 + techcrunch_data: Parsed TechCrunch crawl JSON or None.
80 + correlation_data: Parsed correlation JSON or None.
81 + week: The week string (YYYY-WNN).
82 +
83 + Returns:
84 + Rendered markdown prompt section.
85 + """
86 + if techcrunch_data is None and correlation_data is None:
87 + return (
88 + "No press data available for this week. "
89 + "Analyze repos based on GitHub signals only."
90 + )
91 +
92 + template_path = _REPO_ROOT / "prompts" / "analyze-press-context.md"
93 + template = template_path.read_text(encoding="utf-8")
94 +
95 + # Extract articles (filter to relevant ones)
96 + articles = []
97 + if techcrunch_data:
98 + all_articles = techcrunch_data.get("articles", [])
99 + articles = [
100 + a for a in all_articles if a.get("relevance_score", 0) >= 0.4
101 + ]
102 +
103 + # Extract correlations
104 + correlations = []
105 + if correlation_data:
106 + correlations = correlation_data.get("correlations", [])
107 +
108 + article_count = len(articles)
109 + correlation_count = len(correlations)
110 +
111 + # Render template
112 + rendered = template.replace("{date}", week)
113 + rendered = rendered.replace("{article_count}", str(article_count))
114 + rendered = rendered.replace("{articles_list}", format_articles_list(articles))
115 + rendered = rendered.replace("{correlation_count}", str(correlation_count))
116 + rendered = rendered.replace("{correlations_list}", format_correlations_list(correlations))
117 +
118 + return rendered
119 +
120 +
121 +def resolve_paths(topic: str | None, week: str) -> tuple[Path, Path]:
122 + """Resolve file paths for TechCrunch and correlation data."""
123 + tc_path = raw_dir(topic) / f"{week}-techcrunch.json"
124 + corr_path = analyzed_dir(topic) / f"{week}-correlations.json"
125 + return tc_path, corr_path
126 +
127 +
128 +def main() -> None:
129 + parser = argparse.ArgumentParser(
130 + description="Render press context prompt section"
131 + )
132 + parser.add_argument(
133 + "--topic", default=None, help="Topic ID (e.g., ai-ml)"
134 + )
135 + parser.add_argument(
136 + "--week", default=None, help="Week in YYYY-WNN format (default: current)"
137 + )
138 + args = parser.parse_args()
139 +
140 + week = args.week or current_week()
141 + topic = args.topic
142 +
143 + tc_path, corr_path = resolve_paths(topic, week)
144 + techcrunch_data = load_json(tc_path)
145 + correlation_data = load_json(corr_path)
146 +
147 + output = render_press_context(techcrunch_data, correlation_data, week)
148 + print(output)
149 +
150 +
151 +if __name__ == "__main__":
152 + main()
tests/test_render_press_context.py new
+195
@@ -0,0 +1,195 @@
1 +"""Tests for scripts/render_press_context.py."""
2 +
3 +import sys
4 +from pathlib import Path
5 +
6 +_REPO_ROOT = Path(__file__).resolve().parent.parent
7 +sys.path.insert(0, str(_REPO_ROOT / "scripts"))
8 +
9 +from render_press_context import (
10 + format_articles_list,
11 + format_correlations_list,
12 + render_press_context,
13 + resolve_paths,
14 +)
15 +
16 +
17 +# --- Fixtures ---
18 +
19 +
20 +def _article(
21 + title="AI Startup Raises $10M",
22 + url="https://techcrunch.com/2026/05/15/ai-startup",
23 + categories=None,
24 + relevance_score=0.8,
25 + github_links=None,
26 +):
27 + return {
28 + "title": title,
29 + "url": url,
30 + "categories": ["AI", "Startups"] if categories is None else categories,
31 + "relevance_score": relevance_score,
32 + "github_links": github_links or [],
33 + "entities": ["OpenAI"],
34 + "published_at": "2026-05-15T10:00:00Z",
35 + "summary": "A startup raised money.",
36 + }
37 +
38 +
39 +def _correlation(
40 + repo="acme/cool-project",
41 + match_type="direct_link",
42 + confidence=0.9,
43 + hype_risk="low",
44 +):
45 + return {
46 + "repo": repo,
47 + "press_correlated": True,
48 + "correlation_confidence": confidence,
49 + "matched_articles": ["https://techcrunch.com/article"],
50 + "match_type": match_type,
51 + "hype_risk": hype_risk,
52 + }
53 +
54 +
55 +def _techcrunch_data(articles=None):
56 + arts = articles if articles is not None else [_article()]
57 + return {
58 + "week": "2026-W21",
59 + "source": "techcrunch",
60 + "crawled_at": "2026-05-19T10:00:00Z",
61 + "articles": arts,
62 + "metadata": {
63 + "total_articles": len(arts),
64 + "relevant_articles": len(arts),
65 + "github_links_found": 0,
66 + },
67 + }
68 +
69 +
70 +def _correlation_data(correlations=None):
71 + corrs = correlations if correlations is not None else [_correlation()]
72 + return {
73 + "week": "2026-W21",
74 + "correlations": corrs,
75 + "uncorrelated_repos": [],
76 + "metadata": {
77 + "repos_analyzed": 10,
78 + "correlations_found": len(corrs),
79 + "articles_matched": 1,
80 + },
81 + }
82 +
83 +
84 +# --- Tests ---
85 +
86 +
87 +class TestFormatArticlesList:
88 + def test_empty(self):
89 + assert format_articles_list([]) == "- (none)"
90 +
91 + def test_single_article(self):
92 + result = format_articles_list([_article()])
93 + assert "[AI Startup Raises $10M]" in result
94 + assert "techcrunch.com" in result
95 + assert "[AI, Startups]" in result
96 +
97 + def test_article_without_url(self):
98 + art = _article(url="")
99 + result = format_articles_list([art])
100 + assert "AI Startup Raises $10M" in result
101 + assert "[](" not in result
102 +
103 + def test_article_without_categories(self):
104 + art = _article(categories=[])
105 + result = format_articles_list([art])
106 + assert "[AI, Startups]" not in result
107 +
108 + def test_multiple_articles(self):
109 + articles = [_article(title="First"), _article(title="Second")]
110 + result = format_articles_list(articles)
111 + assert "First" in result
112 + assert "Second" in result
113 + assert result.count("\n") == 1
114 +
115 +
116 +class TestFormatCorrelationsList:
117 + def test_empty(self):
118 + assert format_correlations_list([]) == "- (none)"
119 +
120 + def test_single_correlation(self):
121 + result = format_correlations_list([_correlation()])
122 + assert "acme/cool-project" in result
123 + assert "direct_link" in result
124 + assert "0.9" in result
125 + assert "low" in result
126 +
127 + def test_multiple(self):
128 + corrs = [_correlation(repo="a/b"), _correlation(repo="c/d")]
129 + result = format_correlations_list(corrs)
130 + assert "a/b" in result
131 + assert "c/d" in result
132 +
133 +
134 +class TestRenderPressContext:
135 + def test_no_data_returns_fallback(self):
136 + result = render_press_context(None, None, "2026-W21")
137 + assert "No press data available" in result
138 + assert "GitHub signals only" in result
139 +
140 + def test_with_techcrunch_only(self):
141 + result = render_press_context(_techcrunch_data(), None, "2026-W21")
142 + assert "Press Context" in result
143 + assert "2026-W21" in result
144 + assert "1 articles published" in result
145 + assert "0 repos have press correlation" in result
146 +
147 + def test_with_correlation_only(self):
148 + result = render_press_context(None, _correlation_data(), "2026-W21")
149 + assert "Press Context" in result
150 + assert "0 articles published" in result
151 + assert "1 repos have press correlation" in result
152 +
153 + def test_with_both(self):
154 + result = render_press_context(
155 + _techcrunch_data(), _correlation_data(), "2026-W21"
156 + )
157 + assert "1 articles published" in result
158 + assert "1 repos have press correlation" in result
159 + assert "AI Startup Raises $10M" in result
160 + assert "acme/cool-project" in result
161 +
162 + def test_filters_low_relevance_articles(self):
163 + low = _article(title="Irrelevant", relevance_score=0.2)
164 + high = _article(title="Relevant", relevance_score=0.8)
165 + tc = _techcrunch_data(articles=[low, high])
166 + result = render_press_context(tc, None, "2026-W21")
167 + assert "1 articles published" in result
168 + assert "Relevant" in result
169 + assert "Irrelevant" not in result
170 +
171 + def test_instructions_present(self):
172 + result = render_press_context(_techcrunch_data(), _correlation_data(), "2026-W21")
173 + assert "Press-correlated" in result
174 + assert "Organic growth" in result
175 + assert "Hype risk" in result
176 + assert "Press vs Reality" in result
177 +
178 + def test_hype_risk_labels(self):
179 + corr = _correlation(hype_risk="high")
180 + result = render_press_context(
181 + _techcrunch_data(), _correlation_data([corr]), "2026-W21"
182 + )
183 + assert "high" in result
184 +
185 +
186 +class TestResolvePaths:
187 + def test_with_topic(self):
188 + tc, corr = resolve_paths("ai-ml", "2026-W21")
189 + assert "raw/ai-ml/2026-W21-techcrunch.json" in str(tc)
190 + assert "analyzed/ai-ml/2026-W21-correlations.json" in str(corr)
191 +
192 + def test_without_topic(self):
193 + tc, corr = resolve_paths(None, "2026-W21")
194 + assert "2026-W21-techcrunch.json" in str(tc)
195 + assert "2026-W21-correlations.json" in str(corr)