Skip to content

Commit f78efba

Browse files
hyperpolymathclaude
andcommitted
feat: migrate generate-summaries from Python to Julia
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 61ef778 commit f78efba

2 files changed

Lines changed: 159 additions & 129 deletions

File tree

scripts/generate-summaries.jl

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env julia
2+
# SPDX-License-Identifier: PMPL-1.0-or-later
3+
# Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) <j.d.a.jewell@open.ac.uk>
4+
#
5+
# generate-summaries.jl - Auto-generate summary fields for repos in index.json
6+
#
7+
# Reads each scan file in scans/ and builds a concise summary string from:
8+
# - weak point count and severity breakdown
9+
# - top categories of weak points
10+
# - detected language
11+
# - scan timestamp from index.json
12+
#
13+
# Usage: julia scripts/generate-summaries.jl [--dry-run]
14+
15+
using JSON3
16+
using Dates
17+
18+
const REPO_ROOT = dirname(dirname(abspath(@__FILE__)))
19+
const INDEX_PATH = joinpath(REPO_ROOT, "index.json")
20+
const SCANS_DIR = joinpath(REPO_ROOT, "scans")
21+
22+
# Severity ordering for "top severity" reporting
23+
const SEVERITY_ORDER = Dict(
24+
"Critical" => 4,
25+
"High" => 3,
26+
"Medium" => 2,
27+
"Low" => 1
28+
)
29+
30+
"""
31+
build_summary(repo_name, scan_data, index_entry) -> String
32+
33+
Build a concise summary string from scan data and index entry.
34+
35+
Format examples:
36+
"71 weak points (8 High, 58 Medium, 5 Low); top: PanicPath (30), CommandInjection (25); lang: rust"
37+
"0 weak points; clean scan; lang: elixir"
38+
"16 weak points (3 Critical, 12 Medium, 1 Low); top: UnsafeCode (8), PanicPath (5); lang: idris2"
39+
"""
40+
function build_summary(repo_name::AbstractString, scan_data::Dict, index_entry::Dict)::String
41+
weak_points = get(scan_data, "weak_points", [])
42+
wp_count = length(weak_points)
43+
language = get(scan_data, "language", "unknown")
44+
last_scan = get(index_entry, "last_scan", "unknown")
45+
46+
# Extract just the date portion from the ISO timestamp
47+
scan_date = try
48+
string(last_scan)[1:10]
49+
catch
50+
"unknown"
51+
end
52+
53+
if wp_count == 0
54+
return "0 weak points; clean scan; lang: $(language); scanned: $(scan_date)"
55+
end
56+
57+
# Severity breakdown
58+
severities = Dict{String,Int}()
59+
for wp in weak_points
60+
sev = get(wp, "severity", "Unknown")
61+
severities[sev] = get(severities, sev, 0) + 1
62+
end
63+
sorted_sevs = sort(collect(keys(severities));
64+
by = s -> get(SEVERITY_ORDER, s, 0), rev = true)
65+
sev_parts = ["$(severities[s]) $(s)" for s in sorted_sevs]
66+
sev_str = join(sev_parts, ", ")
67+
68+
# Top categories (up to 3)
69+
categories = Dict{String,Int}()
70+
for wp in weak_points
71+
cat = get(wp, "category", "Unknown")
72+
categories[cat] = get(categories, cat, 0) + 1
73+
end
74+
sorted_cats = sort(collect(pairs(categories)); by = p -> p.second, rev = true)
75+
top_cats = sorted_cats[1:min(3, length(sorted_cats))]
76+
cat_parts = ["$(cat) ($(count))" for (cat, count) in top_cats]
77+
cat_str = join(cat_parts, ", ")
78+
79+
return "$(wp_count) weak points ($(sev_str)); top: $(cat_str); lang: $(language); scanned: $(scan_date)"
80+
end
81+
82+
"""
83+
main() -> Int
84+
85+
Main entry point. Returns 0 on success, 1 if errors occurred.
86+
"""
87+
function main()::Int
88+
dry_run = "--dry-run" in ARGS
89+
90+
# Load index
91+
index = JSON3.read(read(INDEX_PATH, String)) |> Dict
92+
repos_raw = get(index, "repos", Dict())
93+
repos = Dict(string(k) => Dict(string(k2) => v2 for (k2, v2) in pairs(v))
94+
for (k, v) in pairs(repos_raw))
95+
96+
updated_count = 0
97+
skipped_count = 0
98+
error_count = 0
99+
100+
for repo_name in sort(collect(keys(repos)))
101+
entry = repos[repo_name]
102+
103+
# Only update NULL summaries
104+
if get(entry, "summary", nothing) !== nothing
105+
skipped_count += 1
106+
continue
107+
end
108+
109+
scan_file = joinpath(SCANS_DIR, "$(repo_name).json")
110+
if !isfile(scan_file)
111+
println(" SKIP (no scan file): $(repo_name)")
112+
skipped_count += 1
113+
continue
114+
end
115+
116+
local scan_data::Dict
117+
try
118+
raw = JSON3.read(read(scan_file, String))
119+
scan_data = Dict(string(k) => begin
120+
v_inner = raw[k]
121+
if v_inner isa JSON3.Array
122+
[Dict(string(k2) => v2 for (k2, v2) in pairs(item)) for item in v_inner]
123+
else
124+
v_inner
125+
end
126+
end for k in keys(raw))
127+
catch e
128+
println(" ERROR reading $(scan_file): $(e)")
129+
error_count += 1
130+
continue
131+
end
132+
133+
summary = build_summary(repo_name, scan_data, entry)
134+
entry["summary"] = summary
135+
updated_count += 1
136+
137+
if dry_run
138+
println(" $(repo_name): $(summary)")
139+
end
140+
end
141+
142+
# Update repos back into index and set timestamp
143+
index["repos"] = repos
144+
index["last_updated"] = Dates.format(now(Dates.UTC), "yyyy-mm-ddTHH:MM:SS+00:00")
145+
146+
if !dry_run
147+
# Write updated index
148+
open(INDEX_PATH, "w") do f
149+
JSON3.pretty(f, index)
150+
write(f, "\n")
151+
end
152+
println("Updated index.json")
153+
end
154+
155+
println("\nResults: $(updated_count) summaries generated, $(skipped_count) skipped, $(error_count) errors")
156+
return error_count == 0 ? 0 : 1
157+
end
158+
159+
exit(main())

scripts/generate-summaries.py

Lines changed: 0 additions & 129 deletions
This file was deleted.

0 commit comments

Comments
 (0)