tangled
alpha
login
or
join now
anil.recoil.org
/
ocaml-requests
1
fork
atom
A batteries included HTTP/1.1 client in OCaml
1
fork
atom
overview
issues
pulls
pipelines
remove requests summary
anil.recoil.org
1 month ago
c47bccc7
a0f1786c
0/1
build.yml
failed
2min 58s
-1106
5 changed files
expand all
collapse all
unified
split
tools
analyze_repos.ml
clone_repos.ml
dune
resources.json
summarise_recommendations.ml
-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))
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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)))
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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
-
}
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
-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))
···
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0