OCaml HTML5 parser/serialiser based on Python's JustHTML

Structure poe broadcast output with sub-projects and change categories

Enhanced changelog generation to:
- Extract affected sub-projects from commit file paths
- Categorize changes with emojis (✨ New Feature, 🐛 Bug Fix, 🔧 Enhancement, etc.)
- Include file change information in the Claude prompt for better context
- Group related commits under single bullet points
- Output structured format with Sub-projects and Changes sections

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+77 -21
+68 -13
poe/lib/changelog.ml
··· 13 author: string; 14 email: string; 15 subject: string; 16 } 17 18 type channel_member = { ··· 20 email: string; 21 } 22 23 let get_git_log ~proc ~cwd ~since_head = 24 Log.info (fun m -> m "Getting commits since %s" since_head); 25 Eio.Switch.run @@ fun sw -> ··· 37 |> List.filter_map (fun line -> 38 match String.split_on_char '|' line with 39 | [hash; author; email; subject] -> 40 - Some { hash; author; email; subject } 41 | _ -> None) 42 | _ -> [] 43 ··· 58 |> List.filter_map (fun line -> 59 match String.split_on_char '|' line with 60 | [hash; author; email; subject] -> 61 - Some { hash; author; email; subject } 62 | _ -> None) 63 | _ -> [] 64 ··· 103 in 104 String.concat "" text 105 106 let generate ~sw ~proc ~clock ~commits ~members = 107 if commits = [] then None 108 else begin 109 Log.info (fun m -> m "Generating narrative changelog with Claude for %d commits" (List.length commits)); 110 111 - (* Format commits for the prompt *) 112 let commits_text = commits 113 |> List.map (fun c -> 114 - Printf.sprintf "- %s by %s <%s>: %s" c.hash c.author c.email c.subject) 115 |> String.concat "\n" 116 in 117 ··· 123 in 124 125 let prompt = Printf.sprintf 126 - {|You are writing a brief changelog update for a Zulip channel. Given these git commits: 127 128 %s 129 130 - And these channel members who can be @mentioned (use the exact @**Name** format): 131 132 %s 133 134 - Write a brief, narrative changelog (2-4 sentences) that: 135 - 1. Focuses on user-visible features and API changes 136 - 2. Uses @**Name** mentions when a commit author matches a channel member (by name or email) 137 - 3. Is conversational and not bullet-pointed 138 - 4. Skips internal refactoring or minor fixes unless they're the only changes 139 140 - If commits are purely internal/maintenance with no user-visible changes, just write a single sentence noting routine maintenance. 141 142 - Write ONLY the changelog text, no preamble or explanation.|} commits_text members_text 143 in 144 145 let response = ask_claude ~sw ~proc ~clock prompt in
··· 13 author: string; 14 email: string; 15 subject: string; 16 + files: string list; 17 } 18 19 type channel_member = { ··· 21 email: string; 22 } 23 24 + let get_commit_files ~proc ~cwd ~hash = 25 + Eio.Switch.run @@ fun sw -> 26 + let buf = Buffer.create 256 in 27 + let child = Eio.Process.spawn proc ~sw ~cwd 28 + ~stdout:(Eio.Flow.buffer_sink buf) 29 + ["git"; "diff-tree"; "--no-commit-id"; "--name-only"; "-r"; hash] 30 + in 31 + match Eio.Process.await child with 32 + | `Exited 0 -> 33 + Buffer.contents buf 34 + |> String.split_on_char '\n' 35 + |> List.filter (fun s -> String.trim s <> "") 36 + | _ -> [] 37 + 38 let get_git_log ~proc ~cwd ~since_head = 39 Log.info (fun m -> m "Getting commits since %s" since_head); 40 Eio.Switch.run @@ fun sw -> ··· 52 |> List.filter_map (fun line -> 53 match String.split_on_char '|' line with 54 | [hash; author; email; subject] -> 55 + let files = get_commit_files ~proc ~cwd ~hash in 56 + Some { hash; author; email; subject; files } 57 | _ -> None) 58 | _ -> [] 59 ··· 74 |> List.filter_map (fun line -> 75 match String.split_on_char '|' line with 76 | [hash; author; email; subject] -> 77 + let files = get_commit_files ~proc ~cwd ~hash in 78 + Some { hash; author; email; subject; files } 79 | _ -> None) 80 | _ -> [] 81 ··· 120 in 121 String.concat "" text 122 123 + (* Extract sub-project name from a file path (first directory component) *) 124 + let subproject_of_file path = 125 + match String.split_on_char '/' path with 126 + | dir :: _ when dir <> "" && dir <> "." -> Some dir 127 + | _ -> None 128 + 129 + (* Get unique sub-projects affected by a list of commits *) 130 + let affected_subprojects commits = 131 + commits 132 + |> List.concat_map (fun c -> c.files) 133 + |> List.filter_map subproject_of_file 134 + |> List.sort_uniq String.compare 135 + 136 let generate ~sw ~proc ~clock ~commits ~members = 137 if commits = [] then None 138 else begin 139 Log.info (fun m -> m "Generating narrative changelog with Claude for %d commits" (List.length commits)); 140 141 + (* Get affected sub-projects *) 142 + let subprojects = affected_subprojects commits in 143 + let subprojects_text = String.concat ", " subprojects in 144 + 145 + (* Format commits for the prompt, including files *) 146 let commits_text = commits 147 |> List.map (fun c -> 148 + let files_text = match c.files with 149 + | [] -> "" 150 + | files -> Printf.sprintf "\n Files: %s" (String.concat ", " files) 151 + in 152 + Printf.sprintf "- %s by %s <%s>: %s%s" c.hash c.author c.email c.subject files_text) 153 |> String.concat "\n" 154 in 155 ··· 161 in 162 163 let prompt = Printf.sprintf 164 + {|You are writing a structured changelog update for a Zulip channel about a monorepo. 165 + 166 + Git commits: 167 168 %s 169 170 + Affected sub-projects: %s 171 + 172 + Channel members who can be @mentioned (use exact @**Name** format): 173 174 %s 175 176 + Write a changelog update in this format: 177 178 + **Sub-projects:** [list the affected sub-projects from above] 179 180 + **Changes:** 181 + - [emoji] **Category**: Brief description (@**Author** if they match a channel member) 182 + 183 + Use these categories and emojis: 184 + - ✨ **New Feature**: for new functionality 185 + - 🐛 **Bug Fix**: for bug fixes 186 + - 🔧 **Enhancement**: for improvements to existing features 187 + - 📚 **Documentation**: for docs changes 188 + - 🔨 **Refactoring**: for internal code changes 189 + - ⬆️ **Dependencies**: for dependency updates 190 + 191 + Guidelines: 192 + 1. Group related commits under a single bullet point when they're part of the same change 193 + 2. Focus on user-visible changes; combine pure refactoring into one line if needed 194 + 3. Keep descriptions concise (one line each) 195 + 4. Only include 3-6 bullet points maximum 196 + 197 + Write ONLY the formatted changelog, no preamble.|} commits_text subprojects_text members_text 198 in 199 200 let response = ask_claude ~sw ~proc ~clock prompt in
+9 -8
poe/lib/changelog.mli
··· 16 author: string; 17 email: string; 18 subject: string; 19 } 20 - (** A git commit with metadata. *) 21 22 type channel_member = { 23 full_name: string; ··· 60 commits:commit list -> 61 members:channel_member list -> 62 string option 63 - (** [generate ~sw ~proc ~clock ~commits ~members] generates a narrative 64 changelog using Claude. Returns [None] if commits is empty, or 65 - [Some narrative] with the generated text. 66 67 - The narrative: 68 - - Focuses on user-visible features and API changes 69 - - Uses @**Name** mentions for authors matching channel members 70 - - Is conversational prose, not bullet points 71 - - Summarizes internal changes briefly *)
··· 16 author: string; 17 email: string; 18 subject: string; 19 + files: string list; 20 } 21 + (** A git commit with metadata and list of changed files. *) 22 23 type channel_member = { 24 full_name: string; ··· 61 commits:commit list -> 62 members:channel_member list -> 63 string option 64 + (** [generate ~sw ~proc ~clock ~commits ~members] generates a structured 65 changelog using Claude. Returns [None] if commits is empty, or 66 + [Some changelog] with the generated text. 67 68 + The changelog includes: 69 + - List of affected sub-projects (based on file paths) 70 + - Categorized changes with emojis (New Feature, Bug Fix, Enhancement, etc.) 71 + - @**Name** mentions for authors matching channel members 72 + - Concise descriptions grouped by related changes *)