My aggregated monorepo of OCaml code, automaintained
1(** Changelog generation for monopam.
2
3 This module handles generating weekly and daily changelog entries using
4 Claude AI to analyze git commit history and produce user-facing change
5 summaries.
6
7 Changes are stored in a .changes directory at the monorepo root:
8 - .changes/<repo_name>.json - weekly changelog entries
9 - .changes/<repo_name>-<YYYY-MM-DD>.json - daily changelog entries (one file
10 per day per repo)
11 - .changes/YYYYMMDD.json - aggregated daily changes for broadcasting
12
13 {1 Submodules}
14
15 These modules provide types and I/O for querying the generated changes
16 files. *)
17
18module Aggregated = Changes_aggregated
19(** Aggregated daily changes format (YYYYMMDD.json files). *)
20
21module Daily = Changes_daily
22(** Daily changes with per-day-per-repo structure (repo-YYYY-MM-DD.json files).
23*)
24
25module Query = Changes_query
26(** High-level query interface for changes. *)
27
28(** {1 Types} *)
29
30type commit_range = { from_hash : string; to_hash : string; count : int }
31(** Range of commits included in a changelog entry. *)
32
33type weekly_entry = {
34 week_start : string; (** ISO date YYYY-MM-DD, Monday *)
35 week_end : string; (** ISO date YYYY-MM-DD, Sunday *)
36 summary : string; (** One-line summary *)
37 changes : string list; (** Bullet points *)
38 commit_range : commit_range;
39}
40(** A single week's changelog entry. *)
41
42type daily_entry = {
43 date : string; (** ISO date YYYY-MM-DD *)
44 hour : int; (** Hour of day 0-23 for filtering *)
45 timestamp : Ptime.t; (** RFC3339 timestamp for precise ordering *)
46 summary : string; (** One-line summary *)
47 changes : string list; (** Bullet points *)
48 commit_range : commit_range;
49 contributors : string list; (** List of contributors for this entry *)
50 repo_url : string option; (** Upstream repository URL *)
51}
52(** A single day's changelog entry with hour tracking for real-time updates. *)
53
54type changes_file = { repository : string; entries : weekly_entry list }
55(** Contents of a weekly changes JSON file for a repository. *)
56
57type daily_changes_file = { repository : string; entries : daily_entry list }
58(** Contents of a daily changes JSON file for a repository. *)
59
60(** Mode for changelog generation. *)
61type mode = Weekly | Daily
62
63(** {1 JSON Codecs} *)
64
65val commit_range_jsont : commit_range Jsont.t
66(** JSON codec for commit ranges. *)
67
68val weekly_entry_jsont : weekly_entry Jsont.t
69(** JSON codec for weekly entries. *)
70
71val changes_file_jsont : changes_file Jsont.t
72(** JSON codec for weekly changes files. *)
73
74val daily_entry_jsont : daily_entry Jsont.t
75(** JSON codec for daily entries. *)
76
77val daily_changes_file_jsont : daily_changes_file Jsont.t
78(** JSON codec for daily changes files. *)
79
80(** {1 File I/O} *)
81
82val load :
83 fs:_ Eio.Path.t -> monorepo:Fpath.t -> string -> (changes_file, string) result
84(** [load ~fs ~monorepo repo_name] loads weekly changes from
85 .changes/<repo_name>.json. Returns an empty changes file if the file does
86 not exist. *)
87
88val save :
89 fs:_ Eio.Path.t -> monorepo:Fpath.t -> changes_file -> (unit, string) result
90(** [save ~fs ~monorepo cf] saves the changes file to .changes/<repo_name>.json.
91*)
92
93val daily_exists :
94 fs:_ Eio.Path.t -> monorepo:Fpath.t -> date:string -> string -> bool
95(** [daily_exists ~fs ~monorepo ~date repo_name] checks if a daily changes file
96 exists.
97 @param date Date in YYYY-MM-DD format *)
98
99val load_daily :
100 fs:_ Eio.Path.t ->
101 monorepo:Fpath.t ->
102 date:string ->
103 string ->
104 (daily_changes_file, string) result
105(** [load_daily ~fs ~monorepo ~date repo_name] loads daily changes from
106 .changes/<repo_name>-<date>.json. Returns an empty changes file if the file
107 does not exist.
108 @param date Date in YYYY-MM-DD format *)
109
110val save_daily :
111 fs:_ Eio.Path.t ->
112 monorepo:Fpath.t ->
113 date:string ->
114 daily_changes_file ->
115 (unit, string) result
116(** [save_daily ~fs ~monorepo ~date cf] saves the changes file to
117 .changes/<repo_name>-<date>.json.
118 @param date Date in YYYY-MM-DD format *)
119
120(** {1 Markdown Generation} *)
121
122val to_markdown : changes_file -> string
123(** [to_markdown cf] generates markdown from a single weekly changes file. *)
124
125val aggregate : history:int -> changes_file list -> string
126(** [aggregate ~history cfs] generates combined markdown from multiple weekly
127 changes files.
128 @param history Number of weeks to include (0 for all) *)
129
130val aggregate_daily : history:int -> daily_changes_file list -> string
131(** [aggregate_daily ~history cfs] generates combined markdown from multiple
132 daily changes files. Only includes repos with actual changes (filters out
133 empty entries).
134 @param history Number of days to include (0 for all) *)
135
136(** {1 Date Calculation} *)
137
138val format_date : int * int * int -> string
139(** [format_date (year, month, day)] formats a date as YYYY-MM-DD. *)
140
141val week_of_date : int * int * int -> string * string
142(** [week_of_date (year, month, day)] returns (week_start, week_end) as ISO date
143 strings. week_start is Monday, week_end is Sunday. *)
144
145val week_of_ptime : Ptime.t -> string * string
146(** [week_of_ptime t] returns (week_start, week_end) for the given timestamp. *)
147
148val date_of_ptime : Ptime.t -> string
149(** [date_of_ptime t] returns the date as YYYY-MM-DD for the given timestamp. *)
150
151val has_week : changes_file -> week_start:string -> bool
152(** [has_week cf ~week_start] returns true if the changes file already has an
153 entry for the week starting on the given date. *)
154
155val has_day : daily_changes_file -> date:string -> bool
156(** [has_day cf ~date] returns true if the daily changes file already has an
157 entry for the given date. *)
158
159(** {1 Claude Integration} *)
160
161type claude_response = { summary : string; changes : string list }
162(** Response from Claude analysis. *)
163
164val generate_prompt :
165 repository:string ->
166 week_start:string ->
167 week_end:string ->
168 Git.log_entry list ->
169 string
170(** [generate_prompt ~repository ~week_start ~week_end commits] creates the
171 prompt to send to Claude for weekly changelog generation. *)
172
173val generate_weekly_prompt :
174 repository:string ->
175 week_start:string ->
176 week_end:string ->
177 Git.log_entry list ->
178 string
179(** [generate_weekly_prompt ~repository ~week_start ~week_end commits] creates
180 the prompt to send to Claude for weekly changelog generation. *)
181
182val generate_daily_prompt :
183 repository:string -> date:string -> Git.log_entry list -> string
184(** [generate_daily_prompt ~repository ~date commits] creates the prompt to send
185 to Claude for daily changelog generation. *)
186
187val parse_claude_response : string -> (claude_response option, string) result
188(** [parse_claude_response text] parses Claude's response. Returns [Ok None] if
189 the response is empty (blank summary and changes) or "NO_CHANGES". Returns
190 [Ok (Some r)] if valid JSON was parsed with actual changes. Returns
191 [Error msg] if parsing failed. *)
192
193val analyze_commits :
194 sw:Eio.Switch.t ->
195 process_mgr:_ Eio.Process.mgr ->
196 clock:float Eio.Time.clock_ty Eio.Resource.t ->
197 repository:string ->
198 week_start:string ->
199 week_end:string ->
200 Git.log_entry list ->
201 (claude_response option, string) result
202(** [analyze_commits ~sw ~process_mgr ~clock ~repository ~week_start ~week_end
203 commits] sends commits to Claude for weekly analysis and returns the parsed
204 response. *)
205
206val analyze_commits_daily :
207 sw:Eio.Switch.t ->
208 process_mgr:_ Eio.Process.mgr ->
209 clock:float Eio.Time.clock_ty Eio.Resource.t ->
210 repository:string ->
211 date:string ->
212 Git.log_entry list ->
213 (claude_response option, string) result
214(** [analyze_commits_daily ~sw ~process_mgr ~clock ~repository ~date commits]
215 sends commits to Claude for daily analysis and returns the parsed response.
216*)
217
218val refine_daily_changelog :
219 sw:Eio.Switch.t ->
220 process_mgr:_ Eio.Process.mgr ->
221 clock:float Eio.Time.clock_ty Eio.Resource.t ->
222 string ->
223 (string, string) result
224(** [refine_daily_changelog ~sw ~process_mgr ~clock markdown] sends the raw
225 daily changelog markdown through Claude to produce a more narrative,
226 well-organized version. Groups related changes together and orders them by
227 significance. Ensures all repository names are formatted as markdown links
228 using the pattern
229 [[repo-name](https://tangled.org/@anil.recoil.org/repo-name.git)]. Returns
230 the refined markdown or the original on error. *)
231
232(** {1 Aggregated Files} *)
233
234val generate_aggregated :
235 fs:_ Eio.Path.t ->
236 monorepo:Fpath.t ->
237 date:string ->
238 git_head:string ->
239 now:Ptime.t ->
240 (unit, string) result
241(** [generate_aggregated ~fs ~monorepo ~date ~git_head ~now] generates an
242 aggregated JSON file from all daily JSON files.
243
244 This creates a .changes/YYYYMMDD.json file containing all repository entries
245 for the specified date, with change type classification and author
246 aggregation.
247
248 @param fs Filesystem path
249 @param monorepo Path to the monorepo root
250 @param date Date in YYYY-MM-DD format
251 @param git_head Short git hash of the monorepo HEAD at generation time
252 @param now Current time for the generated_at field *)