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)