Skip to content

Commit 92d5afc

Browse files
committed
report: publish per-sample metrics for the HF dataset viewer
Add --json <path> (per-sample JSONL with input/output code + scores, what the HF Dataset Viewer renders) and --markdown (aggregate scoreboard as a GitHub table for the dataset card). Link the dataset page from the README.
1 parent b09be52 commit 92d5afc

2 files changed

Lines changed: 109 additions & 11 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,16 @@ optional dashboard — not part of the test gate.
5757
The obfuscated-JS corpus lives **outside this repo**, in a Hugging Face dataset
5858
([`devirt-dev/devirt-corpus`](https://huggingface.co/datasets/devirt-dev/devirt-corpus)).
5959
HF datasets are plain git repos, so no extra tooling is needed. `./samples` is
60-
gitignored.
60+
gitignored. Live scores + a browsable input→output table are published on the
61+
[dataset page](https://huggingface.co/datasets/devirt-dev/devirt-corpus).
6162

6263
```sh
6364
scripts/pull-corpus.sh # clone/update ./samples from the dataset
6465
CORPUS_REV=<sha> scripts/pull-corpus.sh # pin a revision for reproducible scores
6566
cargo run --release --bin report # per-source readability scoreboard
6667
cargo run --release --bin report --files # …plus every sample listed
68+
cargo run --release --bin report -- --json samples/metrics.jsonl --markdown
69+
# per-sample JSONL (HF viewer) + markdown card
6770
```
6871

6972
The report groups samples by source (`generated`, `real/npm`, `real/tranco`,

src/bin/report.rs

Lines changed: 105 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,16 @@
1313
//! kept% — output bytes / input bytes (less dead weight => lower)
1414
//! opaque% — mean fraction of identifiers that are hex (`_0x..`) or 1-2 chars
1515
//! (more meaningful names => lower)
16+
//!
17+
//! Publishing flags drive the Hugging Face dataset page:
18+
//! --json <path> write per-sample rows (incl. input/output code) as JSONL,
19+
//! which the HF Dataset Viewer renders as a browsable table
20+
//! --markdown print the aggregate scoreboard as a GitHub table (for the
21+
//! dataset card / README), instead of the fixed-width table
1622
use devirt_core::deobfuscate;
1723
use std::collections::BTreeMap;
18-
use std::fs;
24+
use std::fs::{self, File};
25+
use std::io::{BufWriter, Write};
1926
use std::path::{Path, PathBuf};
2027

2128
#[derive(Default)]
@@ -30,9 +37,12 @@ struct Agg {
3037
fn main() {
3138
let args: Vec<String> = std::env::args().skip(1).collect();
3239
let show_files = args.iter().any(|a| a == "--files");
40+
let markdown = args.iter().any(|a| a == "--markdown");
41+
let json_path = flag_value(&args, "--json");
3342
let root = args
3443
.iter()
3544
.find(|a| !a.starts_with("--"))
45+
.filter(|a| Some(a.as_str()) != json_path.as_deref())
3646
.map(PathBuf::from)
3747
.unwrap_or_else(|| PathBuf::from("samples"));
3848

@@ -48,6 +58,12 @@ fn main() {
4858
std::process::exit(1);
4959
}
5060

61+
// Optional per-sample JSONL for the HF Dataset Viewer.
62+
let mut json = json_path.as_ref().map(|p| {
63+
let w = BufWriter::new(File::create(p).expect("create --json file"));
64+
w
65+
});
66+
5167
let mut groups: BTreeMap<String, Agg> = BTreeMap::new();
5268
let mut total = Agg::default();
5369
if show_files {
@@ -60,8 +76,10 @@ fn main() {
6076
let opaque_in = opaque_ratio(&src);
6177
let r = deobfuscate(&src, &name);
6278
let opaque_out = opaque_ratio(&r.code);
79+
let rel_path = rel(&root, p);
80+
let source = group_of(&root, p);
6381

64-
let g = groups.entry(group_of(&root, p)).or_default();
82+
let g = groups.entry(source.clone()).or_default();
6583
g.files += 1;
6684
g.in_bytes += src.len();
6785
g.out_bytes += r.code.len();
@@ -73,10 +91,14 @@ fn main() {
7391
total.opaque_in_sum += opaque_in;
7492
total.opaque_out_sum += opaque_out;
7593

94+
if let Some(w) = json.as_mut() {
95+
write_json_row(w, &rel_path, &source, &src, &r.code, opaque_in, opaque_out);
96+
}
97+
7698
if show_files {
7799
println!(
78100
"{:<52} {:>9} {:>9} {:>5}% {:>6.1}%→{:>5.1}%",
79-
trunc(&rel(&root, p), 52),
101+
trunc(&rel_path, 52),
80102
src.len(),
81103
r.code.len(),
82104
pct(r.code.len(), src.len()),
@@ -86,17 +108,30 @@ fn main() {
86108
}
87109
}
88110

111+
if let Some(w) = json.as_mut() {
112+
w.flush().expect("flush --json file");
113+
eprintln!("wrote {} rows to {}", total.files, json_path.unwrap());
114+
}
115+
89116
if show_files {
90117
println!();
91118
}
92-
println!(
93-
"{:<24} {:>7} {:>11} {:>11} {:>6} {:>16}",
94-
"source", "files", "in", "out", "kept%", "opaque% in→out"
95-
);
96-
for (name, a) in &groups {
97-
print_row(name, a);
119+
if markdown {
120+
print_markdown(&groups, &total);
121+
} else {
122+
println!(
123+
"{:<24} {:>7} {:>11} {:>11} {:>6} {:>16}",
124+
"source", "files", "in", "out", "kept%", "opaque% in→out"
125+
);
126+
for (name, a) in &groups {
127+
print_row(name, a);
128+
}
129+
print_row("TOTAL", &total);
98130
}
99-
print_row("TOTAL", &total);
131+
}
132+
133+
fn flag_value(args: &[String], flag: &str) -> Option<String> {
134+
args.iter().position(|a| a == flag).and_then(|i| args.get(i + 1)).cloned()
100135
}
101136

102137
fn print_row(name: &str, a: &Agg) {
@@ -113,6 +148,66 @@ fn print_row(name: &str, a: &Agg) {
113148
);
114149
}
115150

151+
/// GitHub-flavored markdown table for the dataset card.
152+
fn print_markdown(groups: &BTreeMap<String, Agg>, total: &Agg) {
153+
println!("| source | files | in bytes | out bytes | kept% | opaque% in→out |");
154+
println!("|---|--:|--:|--:|--:|--:|");
155+
let row = |name: &str, a: &Agg| {
156+
let n = a.files.max(1) as f64;
157+
println!(
158+
"| `{}` | {} | {} | {} | {}% | {:.1}% → {:.1}% |",
159+
name, a.files, a.in_bytes, a.out_bytes, pct(a.out_bytes, a.in_bytes),
160+
a.opaque_in_sum / n, a.opaque_out_sum / n,
161+
);
162+
};
163+
for (name, a) in groups {
164+
row(name, a);
165+
}
166+
row("TOTAL", total);
167+
}
168+
169+
/// One JSONL row per sample. Hand-rolled JSON (no serde dep) — only strings need
170+
/// escaping, and `js_escape` covers the JSON string grammar.
171+
fn write_json_row(
172+
w: &mut impl Write,
173+
path: &str,
174+
source: &str,
175+
input: &str,
176+
output: &str,
177+
opaque_in: f64,
178+
opaque_out: f64,
179+
) {
180+
let _ = writeln!(
181+
w,
182+
"{{\"path\":\"{}\",\"source\":\"{}\",\"in_bytes\":{},\"out_bytes\":{},\"kept_pct\":{},\"opaque_in\":{:.1},\"opaque_out\":{:.1},\"input\":\"{}\",\"output\":\"{}\"}}",
183+
json_escape(path),
184+
json_escape(source),
185+
input.len(),
186+
output.len(),
187+
pct(output.len(), input.len()),
188+
opaque_in,
189+
opaque_out,
190+
json_escape(input),
191+
json_escape(output),
192+
);
193+
}
194+
195+
fn json_escape(s: &str) -> String {
196+
let mut out = String::with_capacity(s.len() + 16);
197+
for c in s.chars() {
198+
match c {
199+
'"' => out.push_str("\\\""),
200+
'\\' => out.push_str("\\\\"),
201+
'\n' => out.push_str("\\n"),
202+
'\r' => out.push_str("\\r"),
203+
'\t' => out.push_str("\\t"),
204+
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
205+
c => out.push(c),
206+
}
207+
}
208+
out
209+
}
210+
116211
fn collect(dir: &Path, out: &mut Vec<PathBuf>) {
117212
let Ok(rd) = fs::read_dir(dir) else { return };
118213
for e in rd.flatten() {

0 commit comments

Comments
 (0)