Enforce journalistic headlines in weekly article titles (#150)
- Prompt now explicitly requires a 5-12 word punchy editorial headline with examples (not generic 'Week NN, YYYY Analysis') - Analysis gate rejects generic week/year title patterns - Added test coverage for generic title rejection Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Juan Manuel Servera committed
May 21, 2026 at 14:04 UTC
9eb5789e3b1345a3631573e951aa621b58c21d39
3 files changed
+55
-17
prompts/analyze-weekly.md
+19
-15
@@ -84,14 +84,18 @@ Be critical, selective, and opinionated.
84
- `top_repo`
85
- `quality_score`
86
- `summary`
87
-6. `date` must be `{{CURRENT_DATETIME}}`.
88
-7. `tags` must contain 3-8 topical items.
89
-8. `categories` must include `weekly`.
90
-9. `repos_featured` should equal the total number of repos considered in the weekly editorial pass.
91
-10. `stars_tracked` should equal the total stars across those repos.
92
-11. `top_repo` should be the repo that best anchors the editorial narrative, not automatically the most-starred repo.
93
-12. `quality_score` must be an honest 0-100 self-assessment; publishable work is `>= 60`.
94
-13. Include all required sections in this exact order:
87
+6. `title` must be a punchy 5-12 word journalistic headline that captures the week's dominant themes. Never use generic week/year labels such as `Week NN, YYYY Analysis` or `Week NN, YYYY`.
88
+ - Good: `Agent Skills, Exploit Churn, and the Language Nobody Asked For`
89
+ - Good: `The Week Local Models Went Mainstream`
90
+ - Good: `MCP Eats the Middleware Layer While VCs Look Elsewhere`
91
+7. `date` must be `{{CURRENT_DATETIME}}`.
92
+8. `tags` must contain 3-8 topical items.
93
+9. `categories` must include `weekly`.
94
+10. `repos_featured` should equal the total number of repos considered in the weekly editorial pass.
95
+11. `stars_tracked` should equal the total stars across those repos.
96
+12. `top_repo` should be the repo that best anchors the editorial narrative, not automatically the most-starred repo.
97
+13. `quality_score` must be an honest 0-100 self-assessment; publishable work is `>= 60`.
98
+14. Include all required sections in this exact order:
99
100
```md
101
## This Week's Trends
@@ -109,18 +113,18 @@ Be critical, selective, and opinionated.
113
### Press & Industry
114
```
115
112
-14. Keep the section scope aligned with the spec:
116
+15. Keep the section scope aligned with the spec:
117
- `## This Week's Trends`: ~200-350 words. Name 3-5 macro trends of the week. Each trend should have a name, a 1-2 sentence explanation of what it is, and why it matters to practitioners now. Do not just list repos — synthesize across them. Reference specific repos as evidence using `[owner/repo](https://github.com/owner/repo)`.
118
- `## Where Industry Meets Code`: ~150-250 words. Compare press coverage (TechCrunch or other provided press data) against what developers are actually building. Highlight 2-4 correlations (where press and developer activity align) and call out 2-3 divergences (topics in the press with no dev traction, and developer work the press is ignoring). If no press data was provided, state: "No industry press data was available for this week's analysis." and focus on what the developer activity alone reveals.
119
- `## Signal & Noise`: ~150-260 words. Integrated analysis — what is real versus hype. Do not use Signal/Noise as separate sub-headings; write it as coherent editorial prose that distinguishes durable patterns from inflated, low-substance, or marketing-driven activity. Name names. Reference repos as evidence.
120
- `## Blind Spots`: ~80-160 words. Identify 2-4 meaningful absences from both press coverage AND developer attention. Be specific and concrete — name the missing category, why it matters, and what its absence signals.
121
- `## The Week Ahead`: ~50-110 words. Forward-looking editorial close. What should readers watch for next week? What trends are in motion that haven't peaked yet? Where is the ecosystem heading based on this week's evidence?
122
- `## Key References` with `### Notable Projects` (5-10 most important repos with 1-sentence context each) and `### Press & Industry` (3-5 most relevant articles or sources, or "No press data was provided this week." if absent).
119
-15. The body must be at least 200 words.
120
-16. Do not include raw JSON, notes to self, placeholders, tool transcripts, status summaries, self-referential text, or quality-score commentary outside the required frontmatter fields.
121
-17. Every repository reference in the body must be a clickable GitHub markdown link in this exact format: `[owner/repo](https://github.com/owner/repo)`.
122
-18. Output only the finished markdown file content.
123
-19. The first characters in the file must be the opening `---` of the YAML frontmatter, and the file must end after the final article line with no agent epilogue.
123
+16. The body must be at least 200 words.
124
+17. Do not include raw JSON, notes to self, placeholders, tool transcripts, status summaries, self-referential text, or quality-score commentary outside the required frontmatter fields.
125
+18. Every repository reference in the body must be a clickable GitHub markdown link in this exact format: `[owner/repo](https://github.com/owner/repo)`.
126
+19. Output only the finished markdown file content.
127
+20. The first characters in the file must be the opening `---` of the YAML frontmatter, and the file must end after the final article line with no agent epilogue.
128
129
## Working method
130
@@ -138,7 +142,7 @@ Be critical, selective, and opinionated.
142
143
```md
144
---
141
-title: "Week NN, YYYY Analysis"
145
+title: "Punchy 5-12 word editorial headline about the week's dominant themes"
146
date: {{CURRENT_DATETIME}}
147
week: "YYYY-WNN"
148
year: YYYY
scripts/analysis_gate.py
+8
@@ -56,6 +56,10 @@ FRONTMATTER_PATTERN = re.compile(r"^---\n(.*?)\n---\n(.*)\Z", re.DOTALL)
56
HEADING_PATTERN = re.compile(r"(?m)^(#{2,3})\s+(.+?)\s*$")
57
WORD_PATTERN = re.compile(r"\b[\w'-]+\b")
58
TOP_REPO_PATTERN = re.compile(r"^[^/\s]+/[^/\s]+$")
59
+GENERIC_TITLE_PATTERNS = [
60
+ re.compile(r"^Week\s+\d+.*Analysis$", re.IGNORECASE),
61
+ re.compile(r"^Week\s+\d+,\s*\d{4}$", re.IGNORECASE),
62
+]
63
64
65
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
@@ -247,6 +251,10 @@ def validate_analysis(text: str, raw_payload: dict[str, Any], current_datetime:
251
errors.append(f"Unexpected frontmatter fields: {', '.join(extra_fields)}")
252
253
validate_string_field(frontmatter, "title", errors)
254
+ title = frontmatter.get("title")
255
+ if isinstance(title, str) and title.strip():
256
+ if any(pattern.fullmatch(title.strip()) for pattern in GENERIC_TITLE_PATTERNS):
257
+ errors.append("title must not use a generic week/year placeholder format.")
258
validate_string_field(frontmatter, "week", errors)
259
validate_string_field(frontmatter, "top_repo", errors)
260
validate_string_field(frontmatter, "summary", errors)
tests/test_analysis_gate.py
+28
-2
@@ -78,7 +78,7 @@ def make_analysis(frontmatter: str, body: str) -> str:
78
return f"---\n{frontmatter}\n---\n\n{body}\n"
79
80
81
-VALID_FRONTMATTER = '''title: "Week 21, 2026 Analysis"
81
+VALID_FRONTMATTER = '''title: "The Week Local Models Went Mainstream"
82
date: 2026-05-18T00:00:00Z
83
week: 2026-W21
84
year: 2026
@@ -107,7 +107,7 @@ class AnalysisGateTests(unittest.TestCase):
107
self.assertGreaterEqual(word_count, 200)
108
109
def test_validate_analysis_rejects_wrong_week_date_and_types(self) -> None:
110
- invalid_frontmatter = '''title: "Week 21, 2026 Analysis"
110
+ invalid_frontmatter = '''title: "The Week Local Models Went Mainstream"
111
date: 2026-05-12T00:00:00Z
112
week: 2026-W20
113
year: "2026"
@@ -164,6 +164,32 @@ summary: "A grounded week focused on practical tools."'''.strip()
164
165
self.assertIn("Analysis body contains prohibited placeholder marker: TODO placeholder marker", errors)
166
167
+ def test_validate_analysis_rejects_generic_week_analysis_title(self) -> None:
168
+ frontmatter = VALID_FRONTMATTER.replace(
169
+ 'title: "The Week Local Models Went Mainstream"',
170
+ 'title: "Week 21, 2026 Analysis"',
171
+ )
172
+ errors, _ = analysis_gate.validate_analysis(
173
+ make_analysis(frontmatter, make_body()),
174
+ RAW_PAYLOAD,
175
+ CURRENT_DATETIME,
176
+ )
177
+
178
+ self.assertIn("title must not use a generic week/year placeholder format.", errors)
179
+
180
+ def test_validate_analysis_rejects_generic_week_year_title(self) -> None:
181
+ frontmatter = VALID_FRONTMATTER.replace(
182
+ 'title: "The Week Local Models Went Mainstream"',
183
+ 'title: "Week 21, 2026"',
184
+ )
185
+ errors, _ = analysis_gate.validate_analysis(
186
+ make_analysis(frontmatter, make_body()),
187
+ RAW_PAYLOAD,
188
+ CURRENT_DATETIME,
189
+ )
190
+
191
+ self.assertIn("title must not use a generic week/year placeholder format.", errors)
192
+
193
194
if __name__ == "__main__":
195
unittest.main()