| 1 | # Design: Synthesized month summaries |
| 2 | |
| 3 | **Issue:** #398 |
| 4 | **Status:** Proposed |
| 5 | **Branch:** `design/398-month-synthesis` |
| 6 | |
| 7 | ## Goal |
| 8 | |
| 9 | Replace the current month page opening, which is effectively a week-by-week ledger, with a generated narrative summary of the completed month. The summary should compress 4-5 weekly reports into ~300 words of editorial insight while preserving links to the underlying weekly pages. |
| 10 | |
| 11 | ## Current state |
| 12 | |
| 13 | `scripts/generate_rollups.py` already: |
| 14 | |
| 15 | - loads weekly summaries from `data/analyzed/*-summary.md` |
| 16 | - groups them by `(year, month)` |
| 17 | - renders monthly Hugo pages with four append-only sections: |
| 18 | - `Month Overview` |
| 19 | - `Top Repos This Month` |
| 20 | - `Trends Observed` |
| 21 | - `Key Takeaways` |
| 22 | |
| 23 | It also has a lightweight rolling synthesis path (`--rolling`) that proves the script is already the right place to assemble multi-week context. |
| 24 | |
| 25 | ## Proposal |
| 26 | |
| 27 | Add a synthesized month artifact and inject it into monthly rollups when a month is complete. |
| 28 | |
| 29 | ### New artifact |
| 30 | |
| 31 | Generate and cache one markdown file per completed month: |
| 32 | |
| 33 | `data/analyzed/YYYY-MM-month-synthesis.md` |
| 34 | |
| 35 | This keeps the LLM output versioned alongside weekly analyses and gives yearly rollups a stable source instead of reparsing monthly Hugo pages. |
| 36 | |
| 37 | ## Architecture |
| 38 | |
| 39 | ### New data flow |
| 40 | |
| 41 | 1. `generate_rollups()` loads all weekly summaries. |
| 42 | 2. `build_monthly_pages()` groups summaries by month. |
| 43 | 3. For each month, call `ensure_month_synthesis(...)` **before** building `RollupPage.sections`. |
| 44 | 4. `ensure_month_synthesis(...)`: |
| 45 | - checks whether the month is closed |
| 46 | - builds a compressed month input pack from the month’s weekly summaries |
| 47 | - reuses an existing synthesis artifact when `weeks_covered` still matches |
| 48 | - otherwise invokes the LLM path |
| 49 | - falls back cleanly if generation fails |
| 50 | 5. `build_monthly_pages()` replaces the per-week enumeration with the synthesized narrative and appends links to individual weekly pages for audit/detail access. |
| 51 | 6. `build_yearly_pages()` prefers month synthesis artifacts for yearly narrative sections; if missing, it falls back to the current weekly-derived text. |
| 52 | |
| 53 | ### Integration point in `generate_rollups.py` |
| 54 | |
| 55 | The LLM call should not happen in `main()`. It should happen inside the monthly builder path, immediately after `items = sorted(...)` in `build_monthly_pages()`, because that is where: |
| 56 | |
| 57 | - the full set of weekly inputs for the month is available |
| 58 | - the script knows whether the month is complete |
| 59 | - the resulting synthesis can be attached to the correct `RollupPage` |
| 60 | |
| 61 | Recommended helper split: |
| 62 | |
| 63 | - `is_completed_month(month_key, all_month_keys) -> bool` |
| 64 | - `build_month_synthesis_pack(items) -> str` |
| 65 | - `ensure_month_synthesis(items, analyzed_dir, content_root, now) -> MonthSynthesis | None` |
| 66 | - `load_month_synthesis(path) -> MonthSynthesis` |
| 67 | |
| 68 | The actual model invocation should live in a new helper module (for example `scripts/month_synthesis.py`) patterned after `scripts/analyze_fallback.py`, so `generate_rollups.py` stays focused on rollup assembly. |
| 69 | |
| 70 | ## Prompt template |
| 71 | |
| 72 | Create `prompts/synthesize-month.md`. |
| 73 | |
| 74 | The prompt should ask for one coherent narrative, not bullets, and should explicitly cover: |
| 75 | |
| 76 | - what defined the month overall |
| 77 | - which trends emerged, accelerated, peaked, or faded |
| 78 | - surprises vs. confirmed patterns |
| 79 | - which weekly predictions held up or weakened |
| 80 | |
| 81 | The model should write a cached month-synthesis markdown artifact with: |
| 82 | |
| 83 | - YAML frontmatter |
| 84 | - a one-sentence `summary` |
| 85 | - a ~300 word narrative body |
| 86 | |
| 87 | ## Input format |
| 88 | |
| 89 | The LLM should not receive 4-5 raw weekly pages verbatim unless needed. Instead it should receive a compressed month pack rendered from parsed weekly fields already available in `WeeklySummary`. |
| 90 | |
| 91 | ### Compression pipeline |
| 92 | |
| 93 | For each weekly summary, extract: |
| 94 | |
| 95 | - `week` |
| 96 | - `title` |
| 97 | - `summary` |
| 98 | - `top_repo` |
| 99 | - `tags` |
| 100 | - `signal` |
| 101 | - `noise` |
| 102 | - `gaps` |
| 103 | - `conclusion` |
| 104 | - `featured_repos` (cap at top 5) |
| 105 | - `predictions` from frontmatter when present |
| 106 | |
| 107 | Then render a deterministic digest for the prompt: |
| 108 | |
| 109 | ```md |
| 110 | ### 2026-W23 |
| 111 | - Thesis: ... |
| 112 | - Top repo: owner/repo |
| 113 | - Tags: ... |
| 114 | - Signal: ... |
| 115 | - Noise: ... |
| 116 | - Gaps: ... |
| 117 | - Week-ahead prediction: ... |
| 118 | - Referenced repos: ... |
| 119 | ``` |
| 120 | |
| 121 | Budget guidance: |
| 122 | |
| 123 | - keep `summary` intact |
| 124 | - trim `signal`/`noise`/`gaps`/`conclusion` to sane character limits |
| 125 | - cap repo lists |
| 126 | - include only the previous month synthesis, not the entire yearly page, for continuity |
| 127 | |
| 128 | This reduces prompt size while preserving the editorial signal needed for synthesis. |
| 129 | |
| 130 | ## Output format |
| 131 | |
| 132 | Suggested cached artifact format: |
| 133 | |
| 134 | ```md |
| 135 | --- |
| 136 | title: "June 2026 Month Synthesis" |
| 137 | date: "2026-07-07T06:53:00Z" |
| 138 | month: "2026-06" |
| 139 | weeks_covered: ["2026-W23", "2026-W24", "2026-W25", "2026-W26"] |
| 140 | categories: ["monthly-synthesis"] |
| 141 | summary: "June turned agent skills from novelty into distribution infrastructure while GitHub spam tactics kept mutating." |
| 142 | status: "generated" |
| 143 | source_checksum: "sha256:..." |
| 144 | --- |
| 145 | |
| 146 | ## Month Synthesis |
| 147 | |
| 148 | ~300 words of narrative prose. |
| 149 | ``` |
| 150 | |
| 151 | `source_checksum` should be computed from the compressed month pack so reruns can detect when a weekly source changed and invalidate the cached synthesis. |
| 152 | |
| 153 | ## Month boundary detection |
| 154 | |
| 155 | The monthly synthesis should run only for **closed** months. |
| 156 | |
| 157 | ### Rule |
| 158 | |
| 159 | A month is closed when at least one later weekly summary exists. |
| 160 | |
| 161 | Examples: |
| 162 | |
| 163 | - W24 is the latest week in June and no July week exists yet → June is **not** closed |
| 164 | - the first July weekly summary lands → June becomes **closed** |
| 165 | - rerunning the first July week is safe because the cached June synthesis is reused unless `source_checksum` changed |
| 166 | |
| 167 | ### Implementation sketch |
| 168 | |
| 169 | ```python |
| 170 | def is_completed_month(target: tuple[int, int], all_items: list[WeeklySummary]) -> bool: |
| 171 | return any((item.year, item.month) > target for item in all_items) |
| 172 | ``` |
| 173 | |
| 174 | This matches the workflow requirement: the first successful run after a month change synthesizes the previous month. |
| 175 | |
| 176 | ## Fallback behavior |
| 177 | |
| 178 | If Copilot CLI / GitHub Models is unavailable, times out, or returns invalid output: |
| 179 | |
| 180 | 1. log a warning |
| 181 | 2. do not write a broken synthesis artifact |
| 182 | 3. generate the monthly page using the **current enumeration format** |
| 183 | 4. set frontmatter metadata such as `synthesis_status: "fallback"` on the monthly page if desired |
| 184 | 5. keep yearly rollups on the current weekly-derived fallback path |
| 185 | |
| 186 | This preserves publishability and keeps the site generation path fail-open for the monthly summary enhancement while leaving weekly details intact. |
| 187 | |
| 188 | ## Hugo integration |
| 189 | |
| 190 | ### Monthly page |
| 191 | |
| 192 | Replace the per-week enumeration format. The month page itself becomes a synthesized narrative with links to weekly pages for detail: |
| 193 | |
| 194 | 1. `Month Synthesis` — the generated ~300 word narrative |
| 195 | 2. `Weekly Reports` — a list of links to each week's individual page (not embedded content) |
| 196 | |
| 197 | Recommended monthly frontmatter additions: |
| 198 | |
| 199 | - `summary` — one-sentence thesis from the month synthesis artifact |
| 200 | - `synthesis_status` — `generated` or `fallback` |
| 201 | - `synthesis_weeks` — copied from the synthesis artifact |
| 202 | |
| 203 | Weekly detail is always accessible via the linked weekly pages, which serve as the audit trail. |
| 204 | |
| 205 | ### Yearly page |
| 206 | |
| 207 | Change yearly rollups to consume month synthesis artifacts first: |
| 208 | |
| 209 | - `Year in Review` and `What Changed` should summarize by month, not by week, when synthesis exists |
| 210 | - the existing weekly fallback remains for older months or failed syntheses |
| 211 | |
| 212 | This satisfies the requirement that month summaries feed the yearly report. |
| 213 | |
| 214 | ## Operational notes |
| 215 | |
| 216 | - Reuse the existing model/fallback pattern from `scripts/analyze_fallback.py` |
| 217 | - Keep prompt rendering fenced with `<untrusted-content>` because weekly summaries are prior LLM output |
| 218 | - Store the synthesis artifact in `data/analyzed/` so both publish and sync workflows already carry it forward |
| 219 | - Replace the per-week enumeration on monthly pages with the synthesis; weekly pages remain as the audit trail (accessible via links) |
| 220 | |
| 221 | ## Testing plan |
| 222 | |
| 223 | Add tests for: |
| 224 | |
| 225 | 1. completed-month detection |
| 226 | 2. no synthesis for open/current month |
| 227 | 3. synthesis artifact reuse when `weeks_covered` and checksum match |
| 228 | 4. fallback to existing monthly page structure on model failure |
| 229 | 5. yearly rollup preference for month synthesis artifacts |
| 230 | 6. prompt lint passing for `prompts/synthesize-month.md` |
| 231 | |
| 232 | ## Implementation sequence |
| 233 | |
| 234 | 1. add `prompts/synthesize-month.md` |
| 235 | 2. add month-synthesis helper module |
| 236 | 3. extend `generate_rollups.py` to detect closed months and reuse/cache syntheses |
| 237 | 4. update yearly rollup assembly to prefer month syntheses |
| 238 | 5. add tests |