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 13 author: string; 14 14 email: string; 15 15 subject: string; 16 + files: string list; 16 17 } 17 18 18 19 type channel_member = { ··· 20 21 email: string; 21 22 } 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 + 23 38 let get_git_log ~proc ~cwd ~since_head = 24 39 Log.info (fun m -> m "Getting commits since %s" since_head); 25 40 Eio.Switch.run @@ fun sw -> ··· 37 52 |> List.filter_map (fun line -> 38 53 match String.split_on_char '|' line with 39 54 | [hash; author; email; subject] -> 40 - Some { hash; author; email; subject } 55 + let files = get_commit_files ~proc ~cwd ~hash in 56 + Some { hash; author; email; subject; files } 41 57 | _ -> None) 42 58 | _ -> [] 43 59 ··· 58 74 |> List.filter_map (fun line -> 59 75 match String.split_on_char '|' line with 60 76 | [hash; author; email; subject] -> 61 - Some { hash; author; email; subject } 77 + let files = get_commit_files ~proc ~cwd ~hash in 78 + Some { hash; author; email; subject; files } 62 79 | _ -> None) 63 80 | _ -> [] 64 81 ··· 103 120 in 104 121 String.concat "" text 105 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 + 106 136 let generate ~sw ~proc ~clock ~commits ~members = 107 137 if commits = [] then None 108 138 else begin 109 139 Log.info (fun m -> m "Generating narrative changelog with Claude for %d commits" (List.length commits)); 110 140 111 - (* Format commits for the prompt *) 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 *) 112 146 let commits_text = commits 113 147 |> List.map (fun c -> 114 - Printf.sprintf "- %s by %s <%s>: %s" c.hash c.author c.email c.subject) 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) 115 153 |> String.concat "\n" 116 154 in 117 155 ··· 123 161 in 124 162 125 163 let prompt = Printf.sprintf 126 - {|You are writing a brief changelog update for a Zulip channel. Given these git commits: 164 + {|You are writing a structured changelog update for a Zulip channel about a monorepo. 165 + 166 + Git commits: 127 167 128 168 %s 129 169 130 - And these channel members who can be @mentioned (use the exact @**Name** format): 170 + Affected sub-projects: %s 171 + 172 + Channel members who can be @mentioned (use exact @**Name** format): 131 173 132 174 %s 133 175 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 176 + Write a changelog update in this format: 139 177 140 - If commits are purely internal/maintenance with no user-visible changes, just write a single sentence noting routine maintenance. 178 + **Sub-projects:** [list the affected sub-projects from above] 141 179 142 - Write ONLY the changelog text, no preamble or explanation.|} commits_text members_text 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 143 198 in 144 199 145 200 let response = ask_claude ~sw ~proc ~clock prompt in
+9 -8
poe/lib/changelog.mli
··· 16 16 author: string; 17 17 email: string; 18 18 subject: string; 19 + files: string list; 19 20 } 20 - (** A git commit with metadata. *) 21 + (** A git commit with metadata and list of changed files. *) 21 22 22 23 type channel_member = { 23 24 full_name: string; ··· 60 61 commits:commit list -> 61 62 members:channel_member list -> 62 63 string option 63 - (** [generate ~sw ~proc ~clock ~commits ~members] generates a narrative 64 + (** [generate ~sw ~proc ~clock ~commits ~members] generates a structured 64 65 changelog using Claude. Returns [None] if commits is empty, or 65 - [Some narrative] with the generated text. 66 + [Some changelog] with the generated text. 66 67 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 *) 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 *)