fix: clean up press context for reader-facing output (#135)
Three bugs fixed in press context rendering: 1. Correlation list truncated to top 10 in reader mode (was showing all 117) 2. AI instruction blocks stripped from reader-facing output 3. Divergence lists capped at 10 items with '…and N more' trailer Also fixes Copilot CLI invocation: uses file-read approach instead of --attachment (which only supports images) or $(cat) (argument too long). Verified locally: - All 498 tests pass - Reader-mode output is ~6KB (was 22KB+) - Copilot CLI successfully reads 411KB prompt file and produces high-quality analysis with proper Industry & Press section Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Juan Manuel Servera committed
May 19, 2026 at 21:10 UTC
9d5e5ebbcc860140fdea446280557c08614d8800
5 files changed
+391
-21
.github/workflows/crawl-and-publish.yml
+1
-2
@@ -300,8 +300,7 @@ jobs:
300
fi
301
302
if command -v copilot >/dev/null 2>&1 && copilot \
303
- -p "Analyze the attached weekly GitHub data and produce a markdown summary following the instructions in the attached file." \
304
- --attachment "$PROMPT_FILE" \
303
+ -p "Read the file at ${PROMPT_FILE} — it contains your full analysis instructions and weekly GitHub data. Follow those instructions exactly and output ONLY the final markdown analysis (no commentary)." \
304
-s \
305
--no-ask-user \
306
--model claude-sonnet-4 \
.squad/agents/farnsworth/history.md
+1
-1
@@ -26,4 +26,4 @@
26
- **2026-05-19T15:08:00Z:** Leela milestone decomposition complete. Issues assigned to v0.5–v0.9 milestones. Scribe logged orchestration and merged decision. Your assigned v0.5 analysis and synthesis issues are ready. See `.squad/orchestration-log/2026-05-19T15-08-leela.md` for full decomposition outcome.
27
- **2026-05-19T15:22:00+02:00:** Topic-aware prompt template implemented (Issue #63). Key architecture decisions: (1) Used `{{#IF_TOPIC}}`/`{{#IF_NO_TOPIC}}` conditional blocks rather than Jinja2 to keep the template readable as standalone markdown and avoid adding template engine dependencies. (2) Wisdom injection is two-tier — global wisdom from `.squad/identity/wisdom.md` (existing) plus per-topic wisdom from `topics/{id}/wisdom.md` (new). (3) Render script (`scripts/render_topic_prompt.py`) is zero-dependency (stdlib only, with optional PyYAML), so it works in any CI environment without pip install. (4) Backward compatibility guaranteed: when no `squadscope.topic.yml` exists, the template collapses cleanly to general-mode analysis identical to the existing `analyze-weekly.md` behavior.
28
- **2026-05-19T20:07:19+02:00:** Fixed correlator "0 repos" bug (PR #130). Root cause: `correlate.py` loaded repos via `raw_data.get("repos")` but `crawl.py` writes them under `new_repos` and `trending_repos`. Key paths: `scripts/correlate.py:320`, `scripts/crawl.py:857-858`. Lesson: when integrating scripts in a pipeline, always verify the producer's *actual output schema* against the consumer's expected input schema — don't assume key names match. The CI skill pattern ("test the wire") would have caught this if applied at integration time.
29
-- **2026-05-19T20:13:00+02:00:** Divergence analysis implemented (PR #131). Architecture: `detect_divergences()` in `correlate.py` takes the correlation results and inverts them — unmatched articles become "uncovered tech trends", unmatched repos become "unpublicized dev activity". Articles grouped by first category/entity, repos by first topic. Renderer (`render_press_context.py`) appends divergences as a labeled section after the template. Key insight: divergence detection is cheap once correlations are computed — it's just set-difference on matched URLs and repo names. The editorial value is in surfacing *what's missing*, not just what aligns.
29
+- **2026-05-19T20:50:22+02:00:** Press context dual-mode rendering implemented. Three reader-facing bugs fixed: (1) correlation list truncated to top-10 in reader mode (sorted by confidence desc, hype_risk severity); (2) `### Instructions` block stripped from reader output — it is AI prompt input only; (3) `#### Divergence Instructions` replaced with a plain narrative sentence for reader display. Architecture: `render_press_context.py` gained `reader_mode` kwarg propagated to `format_correlations_list(top_n=)` and `format_divergences(reader_mode=)`. `analyze_fallback._render_press_section_no_ai` now calls `_strip_ai_instructions()` which post-processes the pre-rendered file via regex — chosen because the fallback reads a file path, not raw JSON, so re-rendering from scratch would require threading data paths through. Key paths: `scripts/render_press_context.py`, `scripts/analyze_fallback.py`. 16 new tests added; 498 total pass.
scripts/analyze_fallback.py
+87
-3
@@ -258,8 +258,93 @@ def call_github_models(prompt: str) -> str:
258
raise RuntimeError("GitHub Models API request failed after retries") from last_exc
259
260
261
+def _strip_ai_instructions(content: str) -> str:
262
+ """Remove AI-facing instruction blocks from a rendered press context string.
263
+
264
+ Strips:
265
+ - The "### Instructions" section (from that heading to the next "###" or EOF)
266
+ - The "#### Divergence Instructions" block (heading + bullet items)
267
+ - Truncates the "### Correlation Summary" list to the first 10 entries,
268
+ appending a "…and N more" summary line when truncation occurs.
269
+ """
270
+ import re # noqa: PLC0415
271
+
272
+ # Strip ### Instructions section (to next ### heading or EOF)
273
+ content = re.sub(
274
+ r"\n### Instructions\n.*?(?=\n###|\Z)",
275
+ "",
276
+ content,
277
+ flags=re.DOTALL,
278
+ )
279
+
280
+ # Strip #### Divergence Instructions block (to next #### / ### heading or EOF)
281
+ content = re.sub(
282
+ r"\n#### Divergence Instructions\n.*?(?=\n####|\n###|\Z)",
283
+ "",
284
+ content,
285
+ flags=re.DOTALL,
286
+ )
287
+
288
+ # Truncate correlations list to top 10
289
+ corr_match = re.search(
290
+ r"(### Correlation Summary\n[^\n]*\n)((?:- [^\n]*\n?)+)",
291
+ content,
292
+ )
293
+ if corr_match:
294
+ header = corr_match.group(1)
295
+ list_block = corr_match.group(2)
296
+ list_lines = [ln for ln in list_block.splitlines() if ln.startswith("- ")]
297
+ total = len(list_lines)
298
+ if total > 10:
299
+ omitted = total - 10
300
+ truncated = "\n".join(list_lines[:10])
301
+ truncated += f"\n…and {omitted} more repos with press correlation\n"
302
+ content = (
303
+ content[: corr_match.start()]
304
+ + header
305
+ + truncated
306
+ + content[corr_match.end() :]
307
+ )
308
+
309
+ # Truncate divergence lists to top 10 items each
310
+ for section_header in (
311
+ r"#### 🔍 Tech Trends Without Dev Activity",
312
+ r"#### 🚀 Dev Activity Without Press Coverage",
313
+ ):
314
+ div_match = re.search(
315
+ rf"({re.escape(section_header)}\n[^\n]*\n\n?)((?:- [^\n]*\n?)+)",
316
+ content,
317
+ )
318
+ if div_match:
319
+ header = div_match.group(1)
320
+ list_block = div_match.group(2)
321
+ list_lines = [ln for ln in list_block.splitlines() if ln.startswith("- ")]
322
+ total = len(list_lines)
323
+ if total > 10:
324
+ omitted = total - 10
325
+ truncated = "\n".join(list_lines[:10])
326
+ truncated += f"\n- …and {omitted} more topics\n"
327
+ content = (
328
+ content[: div_match.start()]
329
+ + header
330
+ + truncated
331
+ + content[div_match.end() :]
332
+ )
333
+
334
+ # Add reader-friendly conclusion if divergences exist but instructions were stripped
335
+ if "### Divergence Analysis" in content and "Divergence Instructions" not in content:
336
+ if "These divergences highlight" not in content:
337
+ content = content.rstrip()
338
+ content += (
339
+ "\n\nThese divergences highlight gaps between what the tech industry "
340
+ "is reporting and what developers are actually building.\n"
341
+ )
342
+
343
+ return content.strip()
344
+
345
+
346
def _render_press_section_no_ai(press_context_path: Path | None) -> str:
262
- """Render press context data for the no-AI summary."""
347
+ """Render press context data for the no-AI summary (reader-facing)."""
348
if not press_context_path or not press_context_path.exists() or press_context_path.stat().st_size == 0:
349
return (
350
"No industry press data was available for this week's analysis. "
@@ -268,8 +353,7 @@ def _render_press_section_no_ai(press_context_path: Path | None) -> str:
353
"highlighting press-driven hype versus organic growth patterns."
354
)
355
content = press_context_path.read_text(encoding="utf-8").strip()
271
- # Include the press context as-is (it's already formatted markdown)
272
- return content
356
+ return _strip_ai_instructions(content)
357
358
359
def generate_no_ai_summary(raw_json_path: Path, current_datetime: str, press_context_path: Path | None = None) -> str:
scripts/render_press_context.py
+91
-15
@@ -53,12 +53,36 @@ def format_articles_list(articles: list[dict]) -> str:
53
return "\n".join(lines)
54
55
56
-def format_correlations_list(correlations: list[dict]) -> str:
57
- """Format correlations into a markdown list."""
56
+_HYPE_RISK_SEVERITY: dict[str, int] = {"high": 3, "medium": 2, "low": 1, "none": 0}
57
+
58
+
59
+def format_correlations_list(correlations: list[dict], *, top_n: int | None = None) -> str:
60
+ """Format correlations into a markdown list.
61
+
62
+ Args:
63
+ correlations: List of correlation dicts.
64
+ top_n: When set, show only the top N entries (sorted by confidence desc,
65
+ then hype_risk severity desc) and append a "…and N more" summary line.
66
+ """
67
if not correlations:
68
return "- (none)"
69
+
70
+ if top_n is not None:
71
+ sorted_corrs = sorted(
72
+ correlations,
73
+ key=lambda c: (
74
+ -c.get("correlation_confidence", 0.0),
75
+ -_HYPE_RISK_SEVERITY.get(c.get("hype_risk", "none"), 0),
76
+ ),
77
+ )
78
+ omitted = max(0, len(sorted_corrs) - top_n)
79
+ display = sorted_corrs[:top_n]
80
+ else:
81
+ display = correlations
82
+ omitted = 0
83
+
84
lines = []
61
- for corr in correlations:
85
+ for corr in display:
86
repo = corr.get("repo", "unknown")
87
match_type = corr.get("match_type", "unknown")
88
confidence = corr.get("correlation_confidence", 0.0)
@@ -67,11 +91,21 @@ def format_correlations_list(correlations: list[dict]) -> str:
91
f"- {repo} — match: {match_type}, "
92
f"confidence: {confidence:.1f}, hype_risk: {hype_risk}"
93
)
94
+
95
+ if omitted > 0:
96
+ lines.append(f"…and {omitted} more repos with press correlation")
97
+
98
return "\n".join(lines)
99
100
73
-def format_divergences(divergences: dict) -> str:
74
- """Format divergences section into markdown."""
101
+def format_divergences(divergences: dict, *, reader_mode: bool = False) -> str:
102
+ """Format divergences section into markdown.
103
+
104
+ Args:
105
+ divergences: Divergence data dict.
106
+ reader_mode: When True, replaces the AI instruction block with a
107
+ reader-friendly conclusion sentence.
108
+ """
109
if not divergences:
110
return ""
111
@@ -83,10 +117,14 @@ def format_divergences(divergences: dict) -> str:
117
118
lines = ["\n### Divergence Analysis\n"]
119
120
+ # In reader mode, cap divergence lists to keep output concise
121
+ max_items = 10 if reader_mode else None
122
+
123
if uncovered:
124
lines.append("#### 🔍 Tech Trends Without Dev Activity")
125
lines.append("Topics heavily covered by TechCrunch with no matching GitHub repos:\n")
89
- for item in uncovered:
126
+ display_uncovered = uncovered[:max_items] if max_items else uncovered
127
+ for item in display_uncovered:
128
topic = item.get("topic", "unknown")
129
articles = item.get("techcrunch_articles", [])
130
article_refs = ", ".join(
@@ -94,12 +132,15 @@ def format_divergences(divergences: dict) -> str:
132
for a in articles[:3]
133
)
134
lines.append(f"- **{topic}**: {article_refs}")
135
+ if max_items and len(uncovered) > max_items:
136
+ lines.append(f"- …and {len(uncovered) - max_items} more tech trends without dev activity")
137
lines.append("")
138
139
if unpublicized:
140
lines.append("#### 🚀 Dev Activity Without Press Coverage")
141
lines.append("GitHub repos/trends with no matching TechCrunch coverage:\n")
102
- for item in unpublicized:
142
+ display_unpub = unpublicized[:max_items] if max_items else unpublicized
143
+ for item in display_unpub:
144
topic = item.get("topic", "unknown")
145
repos = item.get("github_repos", [])
146
repo_refs = ", ".join(
@@ -107,19 +148,31 @@ def format_divergences(divergences: dict) -> str:
148
for r in repos[:3]
149
)
150
lines.append(f"- **{topic}**: {repo_refs}")
151
+ if max_items and len(unpublicized) > max_items:
152
+ lines.append(f"- …and {len(unpublicized) - max_items} more dev topics without press coverage")
153
lines.append("")
154
112
- lines.append("#### Divergence Instructions")
113
- lines.append("Use divergences to identify:")
114
- lines.append("- 🔮 Where industry is moving but devs haven't caught up")
115
- lines.append("- 💡 Where devs are innovating ahead of media attention")
116
- lines.append("- 📊 Opportunity gaps between narrative and reality")
155
+ if reader_mode:
156
+ lines.append(
157
+ "These divergences highlight gaps between what the tech industry is reporting "
158
+ "and what developers are actually building."
159
+ )
160
+ else:
161
+ lines.append("#### Divergence Instructions")
162
+ lines.append("Use divergences to identify:")
163
+ lines.append("- 🔮 Where industry is moving but devs haven't caught up")
164
+ lines.append("- 💡 Where devs are innovating ahead of media attention")
165
+ lines.append("- 📊 Opportunity gaps between narrative and reality")
166
167
return "\n".join(lines)
168
169
170
def render_press_context(
122
- techcrunch_data: dict | None, correlation_data: dict | None, week: str
171
+ techcrunch_data: dict | None,
172
+ correlation_data: dict | None,
173
+ week: str,
174
+ *,
175
+ reader_mode: bool = False,
176
) -> str:
177
"""Render the press context prompt section.
178
@@ -127,6 +180,9 @@ def render_press_context(
180
techcrunch_data: Parsed TechCrunch crawl JSON or None.
181
correlation_data: Parsed correlation JSON or None.
182
week: The week string (YYYY-WNN).
183
+ reader_mode: When True, produces reader-facing output: top-10 correlations
184
+ only, no AI instruction blocks, and a narrative divergence
185
+ conclusion instead of model directives.
186
187
Returns:
188
Rendered markdown prompt section.
@@ -161,15 +217,35 @@ def render_press_context(
217
if correlation_data:
218
divergences = correlation_data.get("divergences", {})
219
220
+ # In reader mode, sort correlations by confidence desc then hype_risk severity
221
+ if reader_mode and correlations:
222
+ correlations = sorted(
223
+ correlations,
224
+ key=lambda c: (
225
+ -c.get("correlation_confidence", 0.0),
226
+ -_HYPE_RISK_SEVERITY.get(c.get("hype_risk", "none"), 0),
227
+ ),
228
+ )
229
+
230
+ top_n = 10 if reader_mode else None
231
+
232
# Render template
233
rendered = template.replace("{date}", week)
234
rendered = rendered.replace("{article_count}", str(article_count))
235
rendered = rendered.replace("{articles_list}", format_articles_list(articles))
236
rendered = rendered.replace("{correlation_count}", str(correlation_count))
169
- rendered = rendered.replace("{correlations_list}", format_correlations_list(correlations))
237
+ rendered = rendered.replace(
238
+ "{correlations_list}", format_correlations_list(correlations, top_n=top_n)
239
+ )
240
+
241
+ # Strip the AI-only ### Instructions block in reader mode
242
+ if reader_mode:
243
+ instructions_marker = "\n### Instructions\n"
244
+ if instructions_marker in rendered:
245
+ rendered = rendered[: rendered.index(instructions_marker)]
246
247
# Append divergences section
172
- divergence_section = format_divergences(divergences)
248
+ divergence_section = format_divergences(divergences, reader_mode=reader_mode)
249
if divergence_section:
250
rendered += "\n" + divergence_section
251
tests/test_render_press_context.py
+211
@@ -9,6 +9,7 @@ sys.path.insert(0, str(_REPO_ROOT / "scripts"))
9
from render_press_context import (
10
format_articles_list,
11
format_correlations_list,
12
+ format_divergences,
13
render_press_context,
14
resolve_paths,
15
)
@@ -193,3 +194,213 @@ class TestResolvePaths:
194
tc, corr = resolve_paths(None, "2026-W21")
195
assert "2026-W21-techcrunch.json" in str(tc)
196
assert "2026-W21-correlations.json" in str(corr)
197
+
198
+
199
+class TestFormatCorrelationsListTopN:
200
+ def _make_corrs(self, n: int) -> list[dict]:
201
+ """Return n correlations with varying confidence/hype_risk."""
202
+ risks = ["none", "low", "medium", "high"]
203
+ return [
204
+ {
205
+ "repo": f"org/repo-{i}",
206
+ "match_type": "keyword",
207
+ "correlation_confidence": round(0.1 + 0.8 * i / max(n - 1, 1), 2),
208
+ "hype_risk": risks[i % 4],
209
+ }
210
+ for i in range(n)
211
+ ]
212
+
213
+ def test_no_truncation_when_under_limit(self):
214
+ corrs = self._make_corrs(5)
215
+ result = format_correlations_list(corrs, top_n=10)
216
+ assert "more repos with press correlation" not in result
217
+ assert result.count("- org/repo") == 5
218
+
219
+ def test_truncates_to_top_n(self):
220
+ corrs = self._make_corrs(20)
221
+ result = format_correlations_list(corrs, top_n=10)
222
+ assert "…and 10 more repos with press correlation" in result
223
+ assert result.count("- org/repo") == 10
224
+
225
+ def test_sorted_by_confidence_desc(self):
226
+ corrs = [
227
+ {"repo": "low/conf", "match_type": "k", "correlation_confidence": 0.2, "hype_risk": "none"},
228
+ {"repo": "high/conf", "match_type": "k", "correlation_confidence": 0.9, "hype_risk": "none"},
229
+ {"repo": "mid/conf", "match_type": "k", "correlation_confidence": 0.5, "hype_risk": "none"},
230
+ ]
231
+ result = format_correlations_list(corrs, top_n=2)
232
+ lines = [l for l in result.splitlines() if l.startswith("- ")]
233
+ assert lines[0].startswith("- high/conf")
234
+ assert lines[1].startswith("- mid/conf")
235
+ assert "…and 1 more repos with press correlation" in result
236
+
237
+ def test_no_top_n_returns_all(self):
238
+ corrs = self._make_corrs(20)
239
+ result = format_correlations_list(corrs)
240
+ assert result.count("- org/repo") == 20
241
+ assert "more repos" not in result
242
+
243
+
244
+class TestFormatDivergencesReaderMode:
245
+ def _divergences(self):
246
+ return {
247
+ "uncovered_tech_trends": [
248
+ {
249
+ "topic": "quantum-computing",
250
+ "techcrunch_articles": [{"title": "Quantum Leap", "url": "https://tc.com/q"}],
251
+ }
252
+ ],
253
+ "unpublicized_dev_activity": [
254
+ {
255
+ "topic": "wasm-tooling",
256
+ "github_repos": [{"full_name": "org/wasm-lib", "stars": 500}],
257
+ }
258
+ ],
259
+ }
260
+
261
+ def test_ai_mode_has_instructions(self):
262
+ result = format_divergences(self._divergences(), reader_mode=False)
263
+ assert "#### Divergence Instructions" in result
264
+ assert "Use divergences to identify" in result
265
+
266
+ def test_reader_mode_no_instructions(self):
267
+ result = format_divergences(self._divergences(), reader_mode=True)
268
+ assert "#### Divergence Instructions" not in result
269
+ assert "Use divergences to identify" not in result
270
+
271
+ def test_reader_mode_has_narrative(self):
272
+ result = format_divergences(self._divergences(), reader_mode=True)
273
+ assert "gaps between what the tech industry is reporting" in result
274
+ assert "developers are actually building" in result
275
+
276
+ def test_reader_mode_still_shows_data(self):
277
+ result = format_divergences(self._divergences(), reader_mode=True)
278
+ assert "quantum-computing" in result
279
+ assert "wasm-tooling" in result
280
+
281
+
282
+class TestRenderPressContextReaderMode:
283
+ def test_reader_mode_removes_instructions_block(self):
284
+ result = render_press_context(
285
+ _techcrunch_data(), _correlation_data(), "2026-W21", reader_mode=True
286
+ )
287
+ assert "### Instructions" not in result
288
+ assert "Press-correlated" not in result
289
+ assert "Press vs Reality" not in result
290
+
291
+ def test_ai_mode_keeps_instructions_block(self):
292
+ result = render_press_context(
293
+ _techcrunch_data(), _correlation_data(), "2026-W21", reader_mode=False
294
+ )
295
+ assert "### Instructions" in result
296
+ assert "Press-correlated" in result
297
+
298
+ def test_reader_mode_truncates_large_correlations(self):
299
+ many = [
300
+ {
301
+ "repo": f"org/repo-{i}",
302
+ "match_type": "keyword",
303
+ "correlation_confidence": 0.5,
304
+ "hype_risk": "low",
305
+ }
306
+ for i in range(20)
307
+ ]
308
+ result = render_press_context(
309
+ _techcrunch_data(),
310
+ _correlation_data(many),
311
+ "2026-W21",
312
+ reader_mode=True,
313
+ )
314
+ repo_lines = [ln for ln in result.splitlines() if ln.startswith("- org/repo")]
315
+ assert len(repo_lines) == 10
316
+ assert "…and 10 more repos with press correlation" in result
317
+
318
+ def test_reader_mode_no_truncation_when_under_limit(self):
319
+ few = [
320
+ {
321
+ "repo": f"org/repo-{i}",
322
+ "match_type": "keyword",
323
+ "correlation_confidence": 0.5,
324
+ "hype_risk": "low",
325
+ }
326
+ for i in range(5)
327
+ ]
328
+ result = render_press_context(
329
+ _techcrunch_data(),
330
+ _correlation_data(few),
331
+ "2026-W21",
332
+ reader_mode=True,
333
+ )
334
+ assert "more repos with press correlation" not in result
335
+
336
+
337
+class TestStripAiInstructions:
338
+ """Tests for analyze_fallback._strip_ai_instructions."""
339
+
340
+ def setup_method(self):
341
+ import sys
342
+ sys.path.insert(0, str(_REPO_ROOT))
343
+ import scripts.analyze_fallback as af
344
+ self.af = af
345
+
346
+ def _full_press_context(self) -> str:
347
+ """Simulate a fully rendered AI-mode press context."""
348
+ return (
349
+ "## Press Context (TechCrunch, week of 2026-W21)\n"
350
+ "3 articles published relevant to tech/open-source.\n\n"
351
+ "Notable coverage:\n"
352
+ "- [Article One](https://tc.com/1) [AI]\n\n"
353
+ "### Correlation Summary\n"
354
+ "15 repos have press correlation:\n"
355
+ + "\n".join(
356
+ f"- org/repo-{i} — match: keyword, confidence: 0.5, hype_risk: low"
357
+ for i in range(15)
358
+ )
359
+ + "\n\n"
360
+ "### Instructions\n"
361
+ "For each trending repo, note if press coverage preceded the star surge.\n"
362
+ "Label repos as:\n"
363
+ "- '📰 Press-correlated' — stars gained after/during press coverage\n"
364
+ "- '🌱 Organic growth' — stars gained without press coverage\n"
365
+ )
366
+
367
+ def test_removes_instructions_section(self):
368
+ content = self._full_press_context()
369
+ result = self.af._strip_ai_instructions(content)
370
+ assert "### Instructions" not in result
371
+ assert "Press-correlated" not in result
372
+
373
+ def test_removes_divergence_instructions(self):
374
+ content = (
375
+ "### Divergence Analysis\n\n"
376
+ "#### 🚀 Dev Activity Without Press Coverage\n"
377
+ "repos...\n\n"
378
+ "#### Divergence Instructions\n"
379
+ "Use divergences to identify:\n"
380
+ "- 🔮 Where industry is moving\n"
381
+ "- 💡 Where devs are innovating\n"
382
+ )
383
+ result = self.af._strip_ai_instructions(content)
384
+ assert "#### Divergence Instructions" not in result
385
+ assert "Use divergences to identify" not in result
386
+
387
+ def test_truncates_correlation_list_to_10(self):
388
+ content = self._full_press_context()
389
+ result = self.af._strip_ai_instructions(content)
390
+ repo_lines = [ln for ln in result.splitlines() if ln.startswith("- org/repo")]
391
+ assert len(repo_lines) == 10
392
+ assert "…and 5 more repos with press correlation" in result
393
+
394
+ def test_no_truncation_when_under_limit(self):
395
+ content = (
396
+ "### Correlation Summary\n"
397
+ "5 repos have press correlation:\n"
398
+ + "\n".join(
399
+ f"- org/repo-{i} — match: keyword, confidence: 0.5, hype_risk: low"
400
+ for i in range(5)
401
+ )
402
+ + "\n"
403
+ )
404
+ result = self.af._strip_ai_instructions(content)
405
+ assert "more repos with press correlation" not in result
406
+ assert result.count("- org/repo") == 5