main
md 238 lines 8.25 KB
Rendered Raw
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