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()