14
15
import scripts.analysis_gate as analysis_gate
16
from scripts.generate_yearly_narrative import build_yearly_narrative_pages
17
+from scripts.month_synthesis import ensure_month_synthesis
18
19
PROJECT_ROOT = Path(__file__).resolve().parent.parent
20
SUMMARY_SUFFIX = "-summary.md"
22
REPO_LINK_PATTERN = re.compile(r"https://github\.com/(?P<repo>[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+)")
23
NO_UPDATES_PLACEHOLDER = "_No updates yet._"
24
MONTHLY_SECTIONS = [
25
+ "Month Synthesis",
26
"Month Overview",
27
"Top Repos This Month",
28
"Trends Observed",
113
section_order: list[str]
114
replace_existing_sections: bool = False
115
preserve_unknown_sections: bool = True
116
+ replace_sections: frozenset[str] = frozenset()
117
+
118
+
119
+ACRONYMS = {"ai", "mcp", "ci", "cd", "api", "sdk", "llm", "rag", "ml"}
120
+
121
+
122
+def _titlecase_tag(tag: str) -> str:
123
+ words = tag.replace("-", " ").split()
124
+ return " ".join(w.upper() if w.lower() in ACRONYMS else w.title() for w in words)
125
+
126
+
127
+def generate_monthly_title(synthesis: Any, month: int, year: int) -> str:
128
+ """Generate an SEO-friendly editorial title (max 70 chars) from synthesis data."""
129
+ month_year = f"{MONTH_NAMES[month]} {year}"
130
+ accel = [_titlecase_tag(t) for t in synthesis.accelerating_themes[:2]]
131
+ weak = [_titlecase_tag(t) for t in synthesis.weakening_themes[:1]]
132
+ themes = [_titlecase_tag(t) for t in synthesis.themes[:2]]
133
+
134
+ if len(accel) >= 2:
135
+ title = f"{accel[0]} and {accel[1]} Surge — {month_year}"
136
+ elif accel and weak:
137
+ title = f"{accel[0]} Surges While {weak[0]} Fades — {month_year}"
138
+ elif accel:
139
+ title = f"{accel[0]} Takes Center Stage — {month_year}"
140
+ elif len(themes) >= 2:
141
+ title = f"{themes[0]} and {themes[1]} Define the Month — {month_year}"
142
+ elif themes:
143
+ title = f"{themes[0]} Leads the Month — {month_year}"
144
+ else:
145
+ title = f"Trends Shift and Settle — {month_year}"
146
+
147
+ if len(title) > 70:
148
+ if accel:
149
+ title = f"{accel[0]} Surges — {month_year}"
150
+ elif themes:
151
+ title = f"{themes[0]} Leads — {month_year}"
152
+ if len(title) > 70:
153
+ title = title[:67] + "…"
154
+
155
+ return title
156
157
158
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
355
}
356
357
316
-def build_monthly_pages(summaries: list[WeeklySummary], content_root: Path) -> list[RollupPage]:
358
+def build_monthly_pages(summaries: list[WeeklySummary], content_root: Path, analyzed_dir: Path) -> list[RollupPage]:
359
grouped: dict[tuple[int, int], list[WeeklySummary]] = defaultdict(list)
360
for summary in summaries:
361
grouped[(summary.year, summary.month)].append(summary)
365
items = sorted(items, key=lambda item: (item.date, item.week))
366
tags_counter: Counter[str] = Counter()
367
page_entries: dict[str, list[RollupEntry]] = {section: [] for section in MONTHLY_SECTIONS}
368
+
369
+ synthesis = ensure_month_synthesis(items, analyzed_dir)
370
+
371
+ synthesis_text = synthesis.narrative
372
+ if synthesis.trend_arc:
373
+ synthesis_text += f"\n\n### Trend Arc\n\n{synthesis.trend_arc}"
374
+ page_entries["Month Synthesis"].append(
375
+ RollupEntry(marker="month-synthesis", text=synthesis_text)
376
+ )
377
+
378
for item in items:
379
tags_counter.update(item.tags)
380
for section, entry in monthly_entries(item, tags_counter).items():
381
page_entries[section].append(entry)
382
+
383
+ title = generate_monthly_title(synthesis, month, year)
384
+
385
pages.append(
386
RollupPage(
387
path=content_root / "monthly" / str(year) / f"{month:02d}.md",
388
frontmatter={
334
- "title": f"{MONTH_NAMES[month]} {year} Rollup",
389
+ "title": title,
390
"date": items[-1].date.isoformat(),
391
"month": month,
392
"year": year,
393
"categories": ["monthly"],
394
"weeks_covered": [item.week for item in items],
395
"total_repos_featured": len({repo for item in items for repo in item.featured_repos}),
396
+ "summary": synthesis.summary,
397
+ "themes": list(synthesis.themes),
398
+ "persistent_themes": list(synthesis.persistent_themes),
399
+ "accelerating_themes": list(synthesis.accelerating_themes),
400
+ "weakening_themes": list(synthesis.weakening_themes),
401
+ "key_gaps": list(synthesis.key_gaps),
402
+ "top_repos": list(synthesis.top_repos),
403
},
404
sections=page_entries,
405
section_order=MONTHLY_SECTIONS,
406
+ replace_sections=frozenset({"Month Synthesis"}),
407
)
408
)
409
return pages
435
*,
436
replace_existing_sections: bool = False,
437
preserve_unknown_sections: bool = True,
438
+ replace_sections: frozenset[str] = frozenset(),
439
) -> str:
440
intro = ""
441
existing_sections: dict[str, str] = {}
449
450
rendered_sections: list[str] = []
451
for section in section_order:
388
- content = "" if replace_existing_sections else existing_sections.get(section, "")
452
+ if replace_existing_sections or section in replace_sections:
453
+ content = ""
454
+ else:
455
+ content = existing_sections.get(section, "")
456
if content.strip() == NO_UPDATES_PLACEHOLDER:
457
content = ""
458
for entry in new_entries[section]:
482
page.sections,
483
replace_existing_sections=page.replace_existing_sections,
484
preserve_unknown_sections=page.preserve_unknown_sections,
485
+ replace_sections=page.replace_sections,
486
)
487
page.path.write_text(render_frontmatter(page.frontmatter) + body, encoding="utf-8")
488
493
return []
494
495
written: list[Path] = []
428
- monthly_pages = build_monthly_pages(summaries, content_root)
496
+ monthly_pages = build_monthly_pages(summaries, content_root, analyzed_dir)
497
for page in monthly_pages:
498
write_rollup(page)
499
written.append(page.path)