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