A batteries included HTTP/1.1 client in OCaml

remove requests summary

-1106
-377
tools/analyze_repos.ml
··· 1 - open Eio.Std 2 - 3 - let src = Logs.Src.create "analyze_repos" ~doc:"Analyze HTTP client repos" 4 - module Log = (val Logs.src_log src : Logs.LOG) 5 - 6 - (* Helper to normalize language names for directory structure *) 7 - let normalize_language lang = 8 - String.lowercase_ascii lang 9 - |> String.map (function '/' -> '-' | c -> c) 10 - 11 - (* Helper to extract repo name from "owner/repo" format *) 12 - let extract_repo_name repo_path = 13 - match String.rindex_opt repo_path '/' with 14 - | Some idx -> String.sub repo_path (idx + 1) (String.length repo_path - idx - 1) 15 - | None -> repo_path 16 - 17 - (* Parse resources.json to get list of repos *) 18 - let parse_resources_json json_path = 19 - Log.info (fun m -> m "Parsing resources.json from %s" json_path); 20 - let ic = open_in json_path in 21 - let content = really_input_string ic (in_channel_length ic) in 22 - close_in ic; 23 - 24 - match Jsont_bytesrw.decode_string Jsont.json content with 25 - | Ok json -> ( 26 - match json with 27 - | Jsont.Object (fields, _) -> 28 - List.fold_left (fun acc ((lang_name, _), lang_repos) -> 29 - let lang_dir = normalize_language lang_name in 30 - match lang_repos with 31 - | Jsont.Array (repos, _) -> 32 - List.fold_left (fun acc repo -> 33 - match repo with 34 - | Jsont.Object (repo_fields, _) -> ( 35 - let repo_path_opt = List.assoc_opt ("repo", Jsont.Meta.none) repo_fields in 36 - match repo_path_opt with 37 - | Some (Jsont.String (repo_path, _)) -> 38 - let repo_name = extract_repo_name repo_path in 39 - let normalized_name = String.lowercase_ascii repo_name in 40 - (lang_name, lang_dir, normalized_name) :: acc 41 - | _ -> acc) 42 - | _ -> acc) acc repos 43 - | _ -> acc) [] fields 44 - | _ -> 45 - Log.err (fun m -> m "Invalid JSON format in resources.json"); 46 - []) 47 - | Error err -> 48 - Log.err (fun m -> m "Failed to parse resources.json: %s" err); 49 - [] 50 - 51 - (* Check if analysis output already exists *) 52 - let analysis_exists repo_dir = 53 - let output_path = Printf.sprintf "%s.json" repo_dir in 54 - Sys.file_exists output_path 55 - 56 - (* JSON schema for recommendations *) 57 - let recommendation_schema = 58 - let meta = Jsont.Meta.none in 59 - let json_object fields = Jsont.Object (fields, meta) in 60 - let json_string s = Jsont.String (s, meta) in 61 - let json_array items = Jsont.Array (items, meta) in 62 - let json_field name value = ((name, meta), value) in 63 - 64 - json_object 65 - [ 66 - json_field "type" (json_string "object"); 67 - json_field "properties" 68 - (json_object 69 - [ 70 - json_field "recommendations" 71 - (json_object 72 - [ 73 - json_field "type" (json_string "array"); 74 - json_field "items" 75 - (json_object 76 - [ 77 - json_field "type" (json_string "object"); 78 - json_field "properties" 79 - (json_object 80 - [ 81 - json_field "source_repo" 82 - (json_object 83 - [ json_field "type" (json_string "string") ]); 84 - json_field "source_language" 85 - (json_object 86 - [ json_field "type" (json_string "string") ]); 87 - json_field "criticality" 88 - (json_object 89 - [ json_field "type" (json_string "string"); 90 - json_field "enum" (json_array [ 91 - json_string "high"; 92 - json_string "medium"; 93 - json_string "low" 94 - ]) 95 - ]); 96 - json_field "change_type" 97 - (json_object 98 - [ json_field "type" (json_string "string"); 99 - json_field "enum" (json_array [ 100 - json_string "bug"; 101 - json_string "security"; 102 - json_string "enhancement"; 103 - json_string "feature"; 104 - json_string "refactor" 105 - ]) 106 - ]); 107 - json_field "title" 108 - (json_object 109 - [ json_field "type" (json_string "string") ]); 110 - json_field "description" 111 - (json_object 112 - [ json_field "type" (json_string "string") ]); 113 - json_field "affected_files" 114 - (json_object 115 - [ json_field "type" (json_string "array"); 116 - json_field "items" (json_object 117 - [ json_field "type" (json_string "string") ]) 118 - ]); 119 - json_field "rationale" 120 - (json_object 121 - [ json_field "type" (json_string "string") ]); 122 - ]); 123 - json_field "required" 124 - (json_array 125 - [ 126 - json_string "source_repo"; 127 - json_string "source_language"; 128 - json_string "criticality"; 129 - json_string "change_type"; 130 - json_string "title"; 131 - json_string "description"; 132 - json_string "affected_files"; 133 - json_string "rationale"; 134 - ]); 135 - ]); 136 - ]); 137 - ]); 138 - json_field "required" (json_array [ json_string "recommendations" ]); 139 - ] 140 - 141 - (* Analyze a single repository by path *) 142 - let analyze_single_repo_with_env ~eio_env ~sw repo_path = 143 - Log.info (fun m -> m "Analyzing repository at: %s" repo_path); 144 - 145 - (* Check if directory exists *) 146 - if not (Sys.file_exists repo_path && Sys.is_directory repo_path) then ( 147 - Log.warn (fun m -> m "Directory not found: %s" repo_path); 148 - false 149 - ) else 150 - 151 - let output_path = Printf.sprintf "%s.json" repo_path in 152 - 153 - Log.info (fun m -> m "Output will be saved to: %s" output_path); 154 - 155 - (* Create Claude client with structured output *) 156 - let output_format = Claude.Proto.Structured_output.of_json_schema recommendation_schema in 157 - let options = 158 - Claude.Options.default 159 - |> Claude.Options.with_output_format output_format 160 - |> Claude.Options.with_model `Sonnet_4_5 161 - in 162 - 163 - let client = 164 - Claude.Client.create ~sw 165 - ~process_mgr:(Eio.Stdenv.process_mgr eio_env) 166 - ~clock:(Eio.Stdenv.clock eio_env) 167 - ~options () 168 - in 169 - 170 - (* Build the prompt *) 171 - let prompt = Printf.sprintf 172 - {|You are analyzing a HTTP client library to extract best practices and recommendations for an OCaml HTTP client library. 173 - 174 - # Your Task 175 - 176 - 1. Analyze the repository at %s 177 - 2. Compare it with the OCaml codebase provided in the current working directory (ignoring the third_party directory) 178 - 3. Extract actionable recommendations for improving the OCaml library 179 - 180 - Focus on: 181 - - Error handling patterns 182 - - Authentication mechanisms 183 - - Retry/timeout strategies 184 - - Security best practices 185 - - API design and ergonomics 186 - - Testing approaches 187 - - Enhancements or missing features 188 - 189 - # Instructions 190 - 191 - Analyze the repository code and provide recommendations in the structured format requested. For each recommendation: 192 - - Specify criticality: high (critical bugs/security), medium (important improvements), low (nice-to-have) 193 - - Specify change_type: bug, security, enhancement, feature, or refactor 194 - - Provide specific file paths in the OCaml codebase that would be affected 195 - - Explain the rationale clearly 196 - 197 - Focus on practical, implementable recommendations that would meaningfully improve the OCaml library. Do not make recommendations based on specific language features in the third party library, but focus on the functionality unlocked by that feature when mapped onto the OCaml approaches used in this library. 198 - |} 199 - repo_path 200 - in 201 - 202 - Claude.Client.query client prompt; 203 - 204 - let responses = Claude.Client.receive_all client in 205 - 206 - (* Extract structured output and save to file *) 207 - let success = ref false in 208 - List.iter (function 209 - | Claude.Response.Complete result -> ( 210 - match Claude.Response.Complete.structured_output result with 211 - | Some json -> 212 - (* Write JSON directly using bytesrw-eio *) 213 - let cwd = Eio.Stdenv.cwd eio_env in 214 - let output_eio_path = Eio.Path.(cwd / output_path) in 215 - 216 - (match Eio.Path.with_open_out output_eio_path 217 - ~create:(`Or_truncate 0o644) 218 - (fun flow -> 219 - let buf_writer = Bytesrw_eio.bytes_writer_of_flow flow in 220 - Jsont_bytesrw.encode ~format:Jsont.Indent Jsont.json json ~eod:true buf_writer) with 221 - | Ok () -> 222 - Log.info (fun m -> m "Saved recommendations to %s" output_path); 223 - success := true 224 - | Error err -> 225 - Log.err (fun m -> m "Failed to write JSON: %s" err)) 226 - | None -> 227 - Log.warn (fun m -> m "No structured output received")) 228 - | _ -> ()) 229 - responses; 230 - 231 - if !success then ( 232 - Log.info (fun m -> m "Analysis complete for %s" repo_path); 233 - true 234 - ) else ( 235 - Log.err (fun m -> m "Analysis failed for %s - no recommendations generated" repo_path); 236 - false 237 - ) 238 - 239 - (* Wrapper for single repo analysis - for command line use *) 240 - let analyze_single_repo repo_path = 241 - Eio_main.run @@ fun eio_env -> 242 - Switch.run @@ fun sw -> 243 - let success = analyze_single_repo_with_env ~eio_env ~sw repo_path in 244 - if not success then exit 1 245 - 246 - (* Parallel analysis of multiple repos from resources.json *) 247 - let analyze_all_repos ?(max_parallel=8) () = 248 - let resources_path = "tools/resources.json" in 249 - 250 - if not (Sys.file_exists resources_path) then ( 251 - Log.err (fun m -> m "Resources file not found: %s" resources_path); 252 - exit 1 253 - ); 254 - 255 - let all_repos = parse_resources_json resources_path in 256 - Log.info (fun m -> m "Found %d total repositories in resources.json" (List.length all_repos)); 257 - 258 - (* Filter to only repos that don't have analysis yet *) 259 - let repos_to_analyze = 260 - List.filter (fun (_lang_name, lang_dir, repo_name) -> 261 - let repo_dir = Printf.sprintf "third_party/%s/%s" lang_dir repo_name in 262 - let exists = analysis_exists repo_dir in 263 - let dir_exists = Sys.file_exists repo_dir && Sys.is_directory repo_dir in 264 - if exists then 265 - Log.info (fun m -> m "Skipping %s (analysis already exists)" repo_dir) 266 - else if not dir_exists then 267 - Log.info (fun m -> m "Skipping %s (directory not found)" repo_dir) 268 - else 269 - Log.info (fun m -> m "Will analyze: %s" repo_dir); 270 - (not exists) && dir_exists 271 - ) all_repos 272 - in 273 - 274 - let count = List.length repos_to_analyze in 275 - Log.info (fun m -> m "Will analyze %d repositories (max %d in parallel)" count max_parallel); 276 - 277 - if count = 0 then ( 278 - Log.info (fun m -> m "No repositories need analysis. All done!"); 279 - exit 0 280 - ); 281 - 282 - Eio_main.run @@ fun eio_env -> 283 - Switch.run @@ fun _sw -> 284 - 285 - (* Run analyses in parallel with max_fibers limiting *) 286 - let final_results = 287 - Fiber.List.map ~max_fibers:max_parallel (fun (_lang_name, lang_dir, repo_name) -> 288 - let repo_dir = Printf.sprintf "third_party/%s/%s" lang_dir repo_name in 289 - let result = 290 - try 291 - Switch.run @@ fun analysis_sw -> 292 - analyze_single_repo_with_env ~eio_env ~sw:analysis_sw repo_dir 293 - with exn -> 294 - Log.err (fun m -> m "Exception analyzing %s: %s" repo_dir (Printexc.to_string exn)); 295 - false 296 - in 297 - (repo_dir, result) 298 - ) repos_to_analyze 299 - in 300 - 301 - (* Report summary *) 302 - let successful = List.filter snd final_results in 303 - let failed = List.filter (fun (_, success) -> not success) final_results in 304 - 305 - Log.info (fun m -> m ""); 306 - Log.info (fun m -> m "=== Analysis Summary ==="); 307 - Log.info (fun m -> m "Total: %d" count); 308 - Log.info (fun m -> m "Successful: %d" (List.length successful)); 309 - Log.info (fun m -> m "Failed: %d" (List.length failed)); 310 - 311 - if List.length failed > 0 then ( 312 - Log.info (fun m -> m ""); 313 - Log.info (fun m -> m "Failed repositories:"); 314 - List.iter (fun (repo, _) -> Log.info (fun m -> m " - %s" repo)) failed; 315 - exit 1 316 - ) 317 - 318 - (* Command-line interface *) 319 - let repo_path_arg = 320 - let doc = "Path to repository directory to analyze (e.g., third_party/rust/reqwest). \ 321 - Not required when using --all." in 322 - Cmdliner.Arg.(value & pos 0 (some string) None & info [] ~docv:"REPO_PATH" ~doc) 323 - 324 - let all_flag = 325 - let doc = "Analyze all repositories from tools/resources.json that don't have \ 326 - analysis output yet. Runs in parallel." in 327 - Cmdliner.Arg.(value & flag & info ["all"; "a"] ~doc) 328 - 329 - let max_parallel_arg = 330 - let doc = "Maximum number of parallel analysis sessions (default: 8). \ 331 - Only used with --all." in 332 - Cmdliner.Arg.(value & opt int 8 & info ["max-parallel"; "j"] ~docv:"N" ~doc) 333 - 334 - let setup_log style_renderer level = 335 - Fmt_tty.setup_std_outputs ?style_renderer (); 336 - Logs.set_level level; 337 - Logs.set_reporter (Logs_fmt.reporter ()); 338 - () 339 - 340 - let setup_log_t = 341 - Cmdliner.Term.(const setup_log $ Fmt_cli.style_renderer () $ Logs_cli.level ()) 342 - 343 - let run all_mode max_parallel repo_path = 344 - if all_mode then 345 - analyze_all_repos ~max_parallel () 346 - else 347 - match repo_path with 348 - | Some path -> analyze_single_repo path 349 - | None -> 350 - prerr_endline "Error: REPO_PATH required unless --all is specified"; 351 - exit 1 352 - 353 - let () = 354 - let combined_term = Cmdliner.Term.(const (fun () all max_parallel repo_path -> 355 - run all max_parallel repo_path) 356 - $ setup_log_t $ all_flag $ max_parallel_arg $ repo_path_arg) in 357 - let combined_info = Cmdliner.Cmd.info "analyze_repos" ~version:"2.0" 358 - ~doc:"Analyze HTTP client repositories and generate recommendations." 359 - ~man:[ 360 - `S Cmdliner.Manpage.s_description; 361 - `P "Analyzes HTTP client libraries from third_party/ and generates \ 362 - structured recommendations for improving the OCaml requests library."; 363 - `P "Two modes are supported:"; 364 - `P "1. Single repository mode (default): Analyze one specific repository \ 365 - by providing its path as an argument."; 366 - `P "2. Batch mode (--all): Analyze all repositories listed in \ 367 - tools/resources.json that don't have analysis output yet, \ 368 - running multiple analyses in parallel."; 369 - `S Cmdliner.Manpage.s_examples; 370 - `P "Analyze a single repository:"; 371 - `Pre " $(b,analyze_repos) third_party/php/buzz"; 372 - `P "Analyze all repositories with default parallelism (8):"; 373 - `Pre " $(b,analyze_repos) --all"; 374 - `P "Analyze all repositories with custom parallelism:"; 375 - `Pre " $(b,analyze_repos) --all --max-parallel 4"; 376 - ] in 377 - exit (Cmdliner.Cmd.eval (Cmdliner.Cmd.v combined_info combined_term))
-108
tools/clone_repos.ml
··· 1 - type library = { 2 - language : string; 3 - name : string; 4 - repo : string; 5 - } 6 - 7 - let library_of_json language json = 8 - let open Yojson.Basic.Util in 9 - { 10 - language; 11 - name = json |> member "name" |> to_string; 12 - repo = json |> member "repo" |> to_string; 13 - } 14 - 15 - let libraries_from_resources json = 16 - let open Yojson.Basic.Util in 17 - to_assoc json 18 - |> List.concat_map (fun (language, repos) -> 19 - to_list repos 20 - |> List.map (library_of_json language)) 21 - 22 - let sanitize_name name = 23 - (* Convert name to a safe directory name *) 24 - let s = String.lowercase_ascii name in 25 - (* Replace spaces and special chars with dashes *) 26 - let s = Str.global_replace (Str.regexp "[^a-z0-9-]") "-" s in 27 - (* Remove multiple consecutive dashes *) 28 - Str.global_replace (Str.regexp "-+") "-" s 29 - 30 - (* Sanitize language name to handle special cases like "Bash/Shell" *) 31 - let sanitize_language lang = 32 - let s = String.lowercase_ascii lang in 33 - (* Replace / with - for languages like "Bash/Shell" *) 34 - Str.global_replace (Str.regexp "/") "-" s 35 - 36 - let clone_repository env lib base_dir = 37 - let lang_dir = sanitize_language lib.language in 38 - let repo_name = sanitize_name lib.name in 39 - let lang_path = Filename.concat base_dir lang_dir in 40 - let repo_path = Filename.concat lang_path repo_name in 41 - 42 - (* Check if repository already exists *) 43 - let cwd = Eio.Stdenv.cwd env in 44 - let repo_eio_path = Eio.Path.(cwd / repo_path) in 45 - 46 - let exists = 47 - try 48 - Eio.Path.is_directory repo_eio_path 49 - with Eio.Io (Eio.Fs.E Not_found _, _) -> false 50 - in 51 - 52 - if exists then ( 53 - Printf.printf "Skip: %s/%s (already exists)\n%!" lib.language lib.name 54 - ) else ( 55 - let url = Printf.sprintf "https://github.com/%s.git" lib.repo in 56 - Printf.printf "Cloning: %s/%s from %s\n%!" lib.language lib.name url; 57 - 58 - (* Create language directory with parents *) 59 - Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 Eio.Path.(cwd / lang_path); 60 - 61 - (* Clone with --depth 1 for shallow clone to save space and time *) 62 - try 63 - Eio.Process.run ~cwd 64 - (Eio.Stdenv.process_mgr env) 65 - ["git"; "clone"; "--depth"; "1"; url; repo_path]; 66 - Printf.printf " ✓ Success: %s\n%!" lib.name 67 - with ex -> 68 - Printf.printf " ✗ Failed: %s (%s)\n%!" lib.name (Printexc.to_string ex) 69 - ) 70 - 71 - let run env json_file = 72 - let cwd = Eio.Stdenv.cwd env in 73 - let json_str = 74 - try 75 - Eio.Path.load Eio.Path.(cwd / json_file) 76 - with ex -> 77 - Printf.eprintf "Error reading %s: %s\n" json_file (Printexc.to_string ex); 78 - exit 1 79 - in 80 - 81 - (* Parse JSON *) 82 - let json = Yojson.Basic.from_string json_str in 83 - let libraries = libraries_from_resources json in 84 - 85 - (* Get third_party directory *) 86 - let third_party_dir = "third_party" in 87 - 88 - (* Create third_party if it doesn't exist *) 89 - Eio.Path.mkdirs ~exists_ok:true ~perm:0o755 Eio.Path.(cwd / third_party_dir); 90 - 91 - (* Clone each repository *) 92 - Printf.printf "Cloning %d repositories into %s/\n\n%!" 93 - (List.length libraries) third_party_dir; 94 - 95 - List.iter (fun lib -> clone_repository env lib third_party_dir) libraries; 96 - 97 - Printf.printf "\nDone! Cloned repositories are in %s/\n%!" third_party_dir 98 - 99 - let () = 100 - Eio_main.run @@ fun env -> 101 - (* Read JSON from resources.json file *) 102 - let json_file = 103 - if Array.length Sys.argv > 1 then 104 - Sys.argv.(1) 105 - else 106 - "tools/resources.json" 107 - in 108 - run env json_file
-30
tools/dune
··· 1 - (executables 2 - (names clone_repos analyze_repos summarise_recommendations) 3 - (libraries 4 - eio 5 - eio.unix 6 - eio_main 7 - yojson 8 - str 9 - claude 10 - jsont 11 - bytesrw 12 - bytesrw-eio 13 - cmdliner 14 - logs 15 - logs.cli 16 - logs.fmt 17 - fmt.tty 18 - fmt.cli)) 19 - 20 - (rule 21 - (targets resources.json.download) 22 - (action 23 - (progn 24 - (run curl -L -o resources.json.download 25 - "https://raw.githubusercontent.com/easybase/awesome-http/refs/heads/main/resources.json")))) 26 - 27 - (rule 28 - (alias runtest) 29 - (action 30 - (diff resources.json resources.json.download)))
-238
tools/resources.json
··· 1 - { 2 - "JavaScript": [ 3 - { 4 - "name": "Axios", 5 - "repo": "axios/axios" 6 - }, 7 - { 8 - "name": "node-fetch", 9 - "repo": "node-fetch/node-fetch" 10 - }, 11 - { 12 - "name": "Got", 13 - "repo": "sindresorhus/got" 14 - }, 15 - { 16 - "name": "superagent", 17 - "repo": "visionmedia/superagent" 18 - }, 19 - { 20 - "name": "Needle", 21 - "repo": "tomas/needle" 22 - } 23 - ], 24 - "Python": [ 25 - { 26 - "name": "Requests", 27 - "repo": "psf/requests" 28 - }, 29 - { 30 - "name": "urllib3", 31 - "repo": "urllib3/urllib3" 32 - }, 33 - { 34 - "name": "httplib2", 35 - "repo": "httplib2/httplib2" 36 - }, 37 - { 38 - "name": "GRequests", 39 - "repo": "spyoungtech/grequests" 40 - }, 41 - { 42 - "name": "Uplink", 43 - "repo": "prkumar/uplink" 44 - } 45 - ], 46 - "Java": [ 47 - { 48 - "name": "Eclipse Jetty", 49 - "repo": "eclipse/jetty.project" 50 - }, 51 - { 52 - "name": "OkHttp", 53 - "repo": "square/okhttp" 54 - }, 55 - { 56 - "name": "Heritrix", 57 - "repo": "internetarchive/heritrix3" 58 - }, 59 - { 60 - "name": "Apache HttpClient", 61 - "repo": "apache/httpcomponents-client" 62 - }, 63 - { 64 - "name": "Google HTTP Client Library", 65 - "repo": "googleapis/google-http-java-client" 66 - }, 67 - { 68 - "name": "Http Request", 69 - "repo": "kevinsawicki/http-request" 70 - } 71 - ], 72 - "Rust": [ 73 - { 74 - "name": "reqwest", 75 - "repo": "seanmonstar/reqwest" 76 - }, 77 - { 78 - "name": "hyper", 79 - "repo": "hyperium/hyper" 80 - }, 81 - { 82 - "name": "Isahc", 83 - "repo": "sagebind/isahc" 84 - }, 85 - { 86 - "name": "Surf", 87 - "repo": "http-rs/surf" 88 - }, 89 - { 90 - "name": "curl-rust", 91 - "repo": "alexcrichton/curl-rust" 92 - } 93 - ], 94 - "Swift": [ 95 - { 96 - "name": "Alamofire", 97 - "repo": "Alamofire/Alamofire" 98 - }, 99 - { 100 - "name": "SwiftHTTP", 101 - "repo": "daltoniam/SwiftHTTP" 102 - }, 103 - { 104 - "name": "Net", 105 - "repo": "nghialv/Net" 106 - }, 107 - { 108 - "name": "Moya", 109 - "repo": "Moya/Moya" 110 - }, 111 - { 112 - "name": "Just", 113 - "repo": "dduan/Just" 114 - }, 115 - { 116 - "name": "Kingfisher", 117 - "repo": "onevcat/Kingfisher" 118 - } 119 - ], 120 - "Haskell": [ 121 - { 122 - "name": "Req", 123 - "repo": "mrkkrp/req" 124 - }, 125 - { 126 - "name": "http-client", 127 - "repo": "snoyberg/http-client" 128 - }, 129 - { 130 - "name": "servant-client", 131 - "repo": "haskell-servant/servant" 132 - }, 133 - { 134 - "name": "http-streams", 135 - "repo": "aesiniath/http-streams" 136 - } 137 - ], 138 - "Go": [ 139 - { 140 - "name": "Req", 141 - "repo": "imroc/req" 142 - }, 143 - { 144 - "name": "Resty", 145 - "repo": "go-resty/resty" 146 - }, 147 - { 148 - "name": "Sling", 149 - "repo": "dghubble/sling" 150 - }, 151 - { 152 - "name": "requests", 153 - "repo": "asmcos/requests" 154 - } 155 - ], 156 - "C++": [ 157 - { 158 - "name": "Apache Serf", 159 - "repo": "apache/serf" 160 - }, 161 - { 162 - "name": "Curl for People", 163 - "repo": "libcpr/cpr" 164 - }, 165 - { 166 - "name": "cpp-netlib", 167 - "repo": "cpp-netlib/cpp-netlib" 168 - }, 169 - { 170 - "name": "Webcc", 171 - "repo": "sprinfall/webcc" 172 - }, 173 - { 174 - "name": "Proxygen", 175 - "repo": "facebook/proxygen" 176 - }, 177 - { 178 - "name": "cpp-httplib", 179 - "repo": "yhirose/cpp-httplib" 180 - }, 181 - { 182 - "name": "NFHTTP", 183 - "repo": "spotify/NFHTTP" 184 - }, 185 - { 186 - "name": "EasyHttp", 187 - "repo": "sony/easyhttpcpp" 188 - } 189 - ], 190 - "PHP": [ 191 - { 192 - "name": "Guzzle", 193 - "repo": "guzzle/guzzle" 194 - }, 195 - { 196 - "name": "HTTPlug", 197 - "repo": "php-http/httplug" 198 - }, 199 - { 200 - "name": "HTTP Client", 201 - "repo": "amphp/http-client" 202 - }, 203 - { 204 - "name": "SendGrid HTTP Client", 205 - "repo": "sendgrid/php-http-client" 206 - }, 207 - { 208 - "name": "Buzz", 209 - "repo": "kriswallsmith/Buzz" 210 - } 211 - ], 212 - "Bash/Shell": [ 213 - { 214 - "name": "HTTPie", 215 - "repo": "httpie/httpie" 216 - }, 217 - { 218 - "name": "curl", 219 - "repo": "curl/curl" 220 - }, 221 - { 222 - "name": "aria2", 223 - "repo": "aria2/aria2" 224 - }, 225 - { 226 - "name": "HTTP Prompt", 227 - "repo": "httpie/http-prompt" 228 - }, 229 - { 230 - "name": "Resty", 231 - "repo": "micha/resty" 232 - }, 233 - { 234 - "name": "Ain", 235 - "repo": "jonaslu/ain" 236 - } 237 - ] 238 - }
-353
tools/summarise_recommendations.ml
··· 1 - open Eio.Std 2 - 3 - let src = Logs.Src.create "summarise_recommendations" ~doc:"Summarise recommendations from analyzed repos" 4 - module Log = (val Logs.src_log src : Logs.LOG) 5 - 6 - (* JSON schema for the summary output *) 7 - let summary_schema = 8 - let meta = Jsont.Meta.none in 9 - let json_object fields = Jsont.Object (fields, meta) in 10 - let json_string s = Jsont.String (s, meta) in 11 - let json_array items = Jsont.Array (items, meta) in 12 - let json_field name value = ((name, meta), value) in 13 - 14 - json_object 15 - [ 16 - json_field "type" (json_string "object"); 17 - json_field "properties" 18 - (json_object 19 - [ 20 - json_field "executive_summary" 21 - (json_object [ json_field "type" (json_string "string") ]); 22 - json_field "priority_features" 23 - (json_object 24 - [ 25 - json_field "type" (json_string "array"); 26 - json_field "items" 27 - (json_object 28 - [ 29 - json_field "type" (json_string "object"); 30 - json_field "properties" 31 - (json_object 32 - [ 33 - json_field "priority_rank" 34 - (json_object [ json_field "type" (json_string "integer") ]); 35 - json_field "category" 36 - (json_object 37 - [ json_field "type" (json_string "string"); 38 - json_field "enum" (json_array [ 39 - json_string "Security & Spec Compliance"; 40 - json_string "Feature Enhancements"; 41 - json_string "Architectural Improvements" 42 - ]) 43 - ]); 44 - json_field "title" 45 - (json_object [ json_field "type" (json_string "string") ]); 46 - json_field "description" 47 - (json_object [ json_field "type" (json_string "string") ]); 48 - json_field "rfc_references" 49 - (json_object 50 - [ json_field "type" (json_string "array"); 51 - json_field "items" (json_object [ json_field "type" (json_string "string") ]) 52 - ]); 53 - json_field "source_libraries" 54 - (json_object 55 - [ json_field "type" (json_string "array"); 56 - json_field "items" (json_object [ json_field "type" (json_string "string") ]) 57 - ]); 58 - json_field "affected_files" 59 - (json_object 60 - [ json_field "type" (json_string "array"); 61 - json_field "items" (json_object [ json_field "type" (json_string "string") ]) 62 - ]); 63 - json_field "implementation_notes" 64 - (json_object [ json_field "type" (json_string "string") ]); 65 - json_field "cross_language_consensus" 66 - (json_object [ json_field "type" (json_string "integer") ]); 67 - ]); 68 - json_field "required" 69 - (json_array [ 70 - json_string "priority_rank"; 71 - json_string "category"; 72 - json_string "title"; 73 - json_string "description"; 74 - json_string "rfc_references"; 75 - json_string "source_libraries"; 76 - json_string "affected_files"; 77 - json_string "implementation_notes"; 78 - json_string "cross_language_consensus"; 79 - ]); 80 - ]); 81 - ]); 82 - ]); 83 - json_field "required" (json_array [ 84 - json_string "executive_summary"; 85 - json_string "priority_features" 86 - ]); 87 - ] 88 - 89 - (* Build markdown output from Claude's structured response *) 90 - let build_markdown_from_json json = 91 - let buf = Buffer.create 4096 in 92 - 93 - let add_line s = Buffer.add_string buf s; Buffer.add_char buf '\n' in 94 - let add_lines lines = List.iter add_line lines in 95 - 96 - add_lines [ 97 - "# OCaml HTTP Client Library - Priority Feature Recommendations"; 98 - ""; 99 - "> Auto-generated summary of recommendations from analyzing HTTP client libraries across multiple languages."; 100 - ""; 101 - ]; 102 - 103 - match json with 104 - | Jsont.Object (fields, _) -> 105 - (* Executive summary *) 106 - (match List.assoc_opt ("executive_summary", Jsont.Meta.none) fields with 107 - | Some (Jsont.String (summary, _)) -> 108 - add_lines [ 109 - "## Executive Summary"; 110 - ""; 111 - summary; 112 - ""; 113 - ] 114 - | _ -> ()); 115 - 116 - (* Priority features *) 117 - (match List.assoc_opt ("priority_features", Jsont.Meta.none) fields with 118 - | Some (Jsont.Array (features, _)) -> 119 - let current_category = ref "" in 120 - List.iter (fun feature -> 121 - match feature with 122 - | Jsont.Object (ffields, _) -> 123 - let get_string key = 124 - match List.assoc_opt (key, Jsont.Meta.none) ffields with 125 - | Some (Jsont.String (s, _)) -> s 126 - | _ -> "" 127 - in 128 - let get_int key = 129 - match List.assoc_opt (key, Jsont.Meta.none) ffields with 130 - | Some (Jsont.Number (n, _)) -> int_of_float n 131 - | _ -> 0 132 - in 133 - let get_string_list key = 134 - match List.assoc_opt (key, Jsont.Meta.none) ffields with 135 - | Some (Jsont.Array (items, _)) -> 136 - List.filter_map (function 137 - | Jsont.String (s, _) -> Some s 138 - | _ -> None) items 139 - | _ -> [] 140 - in 141 - 142 - let category = get_string "category" in 143 - let title = get_string "title" in 144 - let description = get_string "description" in 145 - let rfc_refs = get_string_list "rfc_references" in 146 - let source_libs = get_string_list "source_libraries" in 147 - let affected = get_string_list "affected_files" in 148 - let impl_notes = get_string "implementation_notes" in 149 - let consensus = get_int "cross_language_consensus" in 150 - let rank = get_int "priority_rank" in 151 - 152 - (* Add category header if changed *) 153 - if category <> !current_category then begin 154 - current_category := category; 155 - add_lines [ 156 - ""; 157 - "---"; 158 - ""; 159 - Printf.sprintf "## %s" category; 160 - ""; 161 - ] 162 - end; 163 - 164 - (* Feature entry *) 165 - add_line (Printf.sprintf "### %d. %s" rank title); 166 - add_line ""; 167 - add_line description; 168 - add_line ""; 169 - 170 - (* RFC references *) 171 - if List.length rfc_refs > 0 then begin 172 - add_line "**RFC References:**"; 173 - List.iter (fun rfc -> 174 - add_line (Printf.sprintf "- %s" rfc) 175 - ) rfc_refs; 176 - add_line "" 177 - end; 178 - 179 - (* Source libraries *) 180 - if List.length source_libs > 0 then begin 181 - add_line (Printf.sprintf "**Cross-Language Consensus:** %d libraries" consensus); 182 - add_line (Printf.sprintf "**Source Libraries:** %s" (String.concat ", " source_libs)); 183 - add_line "" 184 - end; 185 - 186 - (* Affected files *) 187 - if List.length affected > 0 then begin 188 - add_line "**Affected Files:**"; 189 - List.iter (fun f -> add_line (Printf.sprintf "- `%s`" f)) affected; 190 - add_line "" 191 - end; 192 - 193 - (* Implementation notes *) 194 - if impl_notes <> "" then begin 195 - add_line "**Implementation Notes:**"; 196 - add_line impl_notes; 197 - add_line "" 198 - end 199 - | _ -> () 200 - ) features 201 - | _ -> ()); 202 - 203 - Buffer.contents buf 204 - | _ -> 205 - add_line "Error: Invalid JSON structure received from Claude."; 206 - Buffer.contents buf 207 - 208 - (* Main summarization function *) 209 - let summarise ~eio_env ~sw output_path = 210 - Log.info (fun m -> m "Starting recommendation summarization..."); 211 - 212 - 213 - let prompt = Printf.sprintf {|You are analyzing recommendations for an OCaml HTTP client library. Your task is to synthesize ALL recommendations into a prioritized summary. 214 - 215 - CRITICAL: You MUST systematically process EVERY SINGLE recommendation listed below. Do not skip any. Read through all recommendations completely before synthesizing. 216 - 217 - # Available RFC Specifications 218 - 219 - Read these from the spec/ directory 220 - 221 - # COMPLETE LIST OF ALL RECOMMENDATIONS 222 - 223 - The third_party/ directory contains a COMPLETE list of recommendations extracted from third-party HTTP client libraries. You MUST consider EVERY recommendation when creating your summary. The structure is third_party/<language>/<library> for the sources, with the recommendation for that library in third_party/<language>/<library>.json. Only those JSON files should be considered and not ones inside the third-party library sournes. 224 - 225 - # Your Task 226 - 227 - Systematically analyze ALL recommendations above and create a comprehensive summary of priority features to implement. You MUST: 228 - 229 - 1. **Process ALL recommendations** - Do not skip any recommendation. Each one above must be considered and either grouped with similar ones or noted individually. 230 - 231 - 2. **Prioritize by category** in this exact order: 232 - - "Security & Spec Compliance" - Security vulnerabilities, RFC/spec violations, authentication issues, TLS/certificate handling 233 - - "Feature Enhancements" - New functionality, missing features that multiple libraries implement 234 - - "Architectural Improvements" - Code quality, patterns, refactoring, developer experience 235 - 236 - 3. **Group similar recommendations** - Many libraries recommend the same features (e.g., multiple suggest "middleware/hooks", "circuit breakers", "response body size limits"). Identify ALL these patterns across all recommendations. 237 - 238 - 4. **Reference specific RFC sections** where applicable using format like "RFC 9110 Section 10.2.3 (Retry-After)". Use the RFC specifications listed above. 239 - 240 - 5. **Calculate cross-language consensus** - Count how many different languages/libraries recommend similar functionality. This should reflect the ACTUAL count from the recommendations above. 241 - 242 - 6. **Provide implementation notes** - Briefly describe what implementing this would involve. 243 - 244 - Create 20-30 priority features that comprehensively cover ALL the recommendations above. For recommendations that don't fit into groups, include them as individual items if they are high or medium criticality. 245 - 246 - For each priority feature, ensure you: 247 - - Use the exact category names specified above 248 - - Include all RFC references that apply 249 - - List which source libraries (from the recommendations above) suggested similar functionality 250 - - Specify which OCaml files would need changes 251 - - Provide brief, actionable implementation guidance 252 - |} 253 - in 254 - 255 - (* Create Claude client *) 256 - let output_format = Claude.Proto.Structured_output.of_json_schema summary_schema in 257 - let options = 258 - Claude.Options.default 259 - |> Claude.Options.with_output_format output_format 260 - |> Claude.Options.with_model (`Custom "claude-opus-4-5") 261 - in 262 - 263 - let client = 264 - Claude.Client.create ~sw 265 - ~process_mgr:(Eio.Stdenv.process_mgr eio_env) 266 - ~clock:(Eio.Stdenv.clock eio_env) 267 - ~options () 268 - in 269 - 270 - Claude.Client.query client prompt; 271 - let responses = Claude.Client.receive_all client in 272 - 273 - (* Process response *) 274 - let success = ref false in 275 - List.iter (function 276 - | Claude.Response.Complete result -> ( 277 - match Claude.Response.Complete.structured_output result with 278 - | Some json -> 279 - (* Build markdown from JSON *) 280 - let markdown = build_markdown_from_json json in 281 - 282 - (* Write markdown output *) 283 - let cwd = Eio.Stdenv.cwd eio_env in 284 - let md_path = Eio.Path.(cwd / output_path) in 285 - 286 - Eio.Path.save ~create:(`Or_truncate 0o644) md_path markdown; 287 - Log.info (fun m -> m "Saved summary to %s" output_path); 288 - 289 - (* Also save the raw JSON *) 290 - let json_output_path = (Filename.remove_extension output_path) ^ ".json" in 291 - let json_path = Eio.Path.(cwd / json_output_path) in 292 - 293 - (match Eio.Path.with_open_out json_path 294 - ~create:(`Or_truncate 0o644) 295 - (fun flow -> 296 - let buf_writer = Bytesrw_eio.bytes_writer_of_flow flow in 297 - Jsont_bytesrw.encode ~format:Jsont.Indent Jsont.json json ~eod:true buf_writer) with 298 - | Ok () -> 299 - Log.info (fun m -> m "Saved JSON to %s" json_output_path) 300 - | Error err -> 301 - Log.warn (fun m -> m "Failed to write JSON: %s" err)); 302 - 303 - success := true 304 - | None -> 305 - Log.warn (fun m -> m "No structured output received from Claude")) 306 - | _ -> ()) 307 - responses; 308 - 309 - if not !success then begin 310 - Log.err (fun m -> m "Summarization failed - no output generated"); 311 - exit 1 312 - end 313 - 314 - (* Command-line interface *) 315 - let output_path_arg = 316 - let doc = "Output path for the summary markdown file (default: RECOMMENDATIONS.md)" in 317 - Cmdliner.Arg.(value & opt string "RECOMMENDATIONS.md" & info ["o"; "output"] ~docv:"PATH" ~doc) 318 - 319 - let setup_log style_renderer level = 320 - Fmt_tty.setup_std_outputs ?style_renderer (); 321 - Logs.set_level level; 322 - Logs.set_reporter (Logs_fmt.reporter ()); 323 - () 324 - 325 - let setup_log_t = 326 - Cmdliner.Term.(const setup_log $ Fmt_cli.style_renderer () $ Logs_cli.level ()) 327 - 328 - let run output_path = 329 - Eio_main.run @@ fun eio_env -> 330 - Switch.run @@ fun sw -> 331 - summarise ~eio_env ~sw output_path 332 - 333 - let () = 334 - let combined_term = Cmdliner.Term.(const (fun () output_path -> 335 - run output_path) 336 - $ setup_log_t $ output_path_arg) in 337 - let combined_info = Cmdliner.Cmd.info "summarise_recommendations" ~version:"1.0" 338 - ~doc:"Summarise recommendations from analyzed HTTP client repositories." 339 - ~man:[ 340 - `S Cmdliner.Manpage.s_description; 341 - `P "Reads all recommendation JSON files from third_party/, analyzes them \ 342 - using Claude, and generates a prioritized summary markdown document."; 343 - `P "The summary prioritizes recommendations in this order:"; 344 - `P "1. Security & Spec Compliance - Security issues and RFC violations"; 345 - `P "2. Feature Enhancements - New functionality with cross-language consensus"; 346 - `P "3. Architectural Improvements - Code quality and patterns"; 347 - `S Cmdliner.Manpage.s_examples; 348 - `P "Generate summary with default output path:"; 349 - `Pre " $(b,summarise_recommendations)"; 350 - `P "Generate summary to custom path:"; 351 - `Pre " $(b,summarise_recommendations) -o docs/priorities.md"; 352 - ] in 353 - exit (Cmdliner.Cmd.eval (Cmdliner.Cmd.v combined_info combined_term))