Skip to content

Commit 86a7a79

Browse files
fix: de-duplicate rows resolving to the same fix commit (within a client)
After #PR resolution, advisory / changelog / release entries collapse onto the PR/commit they reference — cross_reference ran before that resolution so it missed them. build_security_dataset now de-dups by (client, fix_commit), keeping the richest representative (has code > PR/commit URL > tier > text): 108 duplicates removed, 2,333 -> 2,225 rows, essential slice 1,808. README + BUILD_REPORT numbers and pipeline diagram updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 21b8043 commit 86a7a79

7 files changed

Lines changed: 43 additions & 858 deletions

File tree

README.md

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,12 @@ this README explains.
1717
import pandas as pd
1818
df = pd.read_parquet("data/ethereum_vulns.parquet") # or data/ethereum_vulns.csv
1919

20-
df[df.authority_tier != "C_candidate"] # the essential slice (1,891 rows)
20+
df[df.authority_tier != "C_candidate"] # the essential slice (1,808 rows)
2121
df[df.confidence == "high"] # strongest evidence only
2222
```
2323

2424
**Browse it on GitHub:** [`data/ethereum_vulns.preview.csv`](data/ethereum_vulns.preview.csv)
25-
renders as a table (key columns). Full data: [`ethereum_vulns.csv`](data/ethereum_vulns.csv)
26-
(2.5 MB) or the `.parquet`.
25+
renders as a table (key columns). Full data with inline pre/post code: [`ethereum_vulns.csv`](data/ethereum_vulns.csv) or the `.parquet`.
2726

2827
*(A HuggingFace `datasets` mirror under [`NyxFoundation/`](https://huggingface.co/NyxFoundation) is planned.)*
2928

@@ -32,9 +31,9 @@ renders as a table (key columns). Full data: [`ethereum_vulns.csv`](data/ethereu
3231
| | rows |
3332
|---|---:|
3433
| raw snapshot (all clients) | 18,475 |
35-
| curated (security-only) | **2,333** |
36-
| └ essential slice (tier A ∪ B) | **1,891** |
37-
| by tier | A_authoritative 239 · B_corroborated 1,652 · C_candidate 442 |
34+
| curated (security-only) | **2,225** |
35+
| └ essential slice (tier A ∪ B) | **1,808** |
36+
| by tier | A_authoritative 235 · B_corroborated 1,573 · C_candidate 417 |
3837
| by confidence | high 337 · medium 1,542 · low 454 |
3938
| by severity | Critical 3 · High 63 · Medium 60 · Low 21 · Info 853 · Unrated 1,333 |
4039

@@ -49,8 +48,8 @@ flowchart TD
4948
N -->|"T2b NVD substring-match false positives −49"| D
5049
D --> G[["THE GATE<br/>keep on evidence"]]
5150
G -->|"no security signal −14,665"| X["dropped<br/>(low-signal)"]
52-
G -->|"≥ 1 signal"| K["curated<br/>2,333 rows"]
53-
K --> T["authority tiers +<br/>silent-fix LLM enrichment"]
51+
G -->|"≥ 1 signal"| K["curated<br/>2,225 rows"]
52+
K --> T["authority tiers · silent-fix LLM ·<br/>labels · dedup by fix commit"]
5453
```
5554

5655
**"Gate-dropped"** = a row that survived de-noising but for which **no independent

data/ethereum_vulns.csv

Lines changed: 0 additions & 733 deletions
Large diffs are not rendered by default.

data/ethereum_vulns.parquet

-181 KB
Binary file not shown.

data/ethereum_vulns.preview.csv

Lines changed: 0 additions & 108 deletions
Large diffs are not rendered by default.

data/manifest.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"domain": "ethereum",
3-
"n_rows": 2333,
3+
"n_rows": 2225,
44
"schema": [
55
"id",
66
"source_platform",
@@ -40,17 +40,17 @@
4040
"t2_noise_dropped": 1417,
4141
"t2b_nvd_fp_dropped": 49,
4242
"after_t1": 16998,
43-
"security_rows": 2333,
43+
"security_rows": 2225,
4444
"low_signal_dropped": 14665,
4545
"by_confidence": {
4646
"medium": 1542,
4747
"low": 454,
4848
"high": 337
4949
},
5050
"by_authority_tier": {
51-
"B_corroborated": 1652,
52-
"C_candidate": 442,
53-
"A_authoritative": 239
51+
"B_corroborated": 1573,
52+
"C_candidate": 417,
53+
"A_authoritative": 235
5454
},
5555
"by_n_signals": {
5656
"1": 487,
@@ -90,7 +90,8 @@
9090
"1.0": 167
9191
},
9292
"residual_boilerplate_fp": 0,
93-
"labelled": 2062
93+
"labelled": 2062,
94+
"deduped_fix_commit": 108
9495
},
9596
"source": "11 Ethereum execution + consensus clients (past security fixes)"
9697
}

docs/BUILD_REPORT.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,10 +41,10 @@ Critical** (geth, besu, teku). Severities preserved through the canonical path.
4141

4242
## After (curated)
4343

44-
- rows: **2,333**
44+
- rows: **2,225** (after removing 108 same-fix-commit duplicates)
4545
- residual boilerplate FP: **0**
46-
- **essential slice (A+B): 1,891** (was 173 rated-only) — clean high-precision core
47-
- by authority_tier: {'B_corroborated': 1652, 'C_candidate': 442, 'A_authoritative': 239}
46+
- **essential slice (A+B): 1,808** (was 173 rated-only) — clean high-precision core
47+
- by authority_tier: {'B_corroborated': 1573, 'C_candidate': 417, 'A_authoritative': 235}
4848
- **learned silent-fix signal (gemma4:31b):** classified **1,519** PR/commit diffs
4949
across all 11 clients (curated C_candidate + gate-dropped *plausible* rows),
5050
flagged **696** as real silent fixes. This both promotes classified fixes C→B

pipeline/build_security_dataset.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -425,6 +425,32 @@ def main() -> int:
425425
report["labelled"] = int((sec["label"].fillna("other") != "other").sum())
426426
print(f"[labels] joined {report['labelled']} area labels", file=sys.stderr)
427427

428+
# De-dup rows that resolved to the SAME fix commit within a client:
429+
# advisory / changelog / release entries collapse onto the PR/commit they
430+
# reference. cross_reference ran before #PR resolution so it missed these.
431+
# Keep the richest representative (has code > PR/commit URL > tier > text).
432+
fc = sec["fix_commit"].fillna("").astype(str)
433+
434+
def _dscore(r):
435+
s = 8.0 if str(r.get("pre_fix_code") or "[]") != "[]" else 0.0
436+
u = str(r.get("source_url") or "")
437+
s += 4 if ("/pull/" in u or "/commit/" in u) else 0
438+
s += {"A_authoritative": 3, "B_corroborated": 2, "C_candidate": 1}.get(r.get("authority_tier"), 0)
439+
s += min(len(str(r.get("description") or "")), 2000) / 1000
440+
return s
441+
442+
sec = sec.assign(_k=sec["source_platform"].astype(str) + "|" + fc, _s=sec.apply(_dscore, axis=1))
443+
hasfc = fc.str.len() > 0
444+
before = len(sec)
445+
dedup = pd.concat([sec[~hasfc],
446+
sec[hasfc].sort_values("_s", ascending=False).drop_duplicates("_k", keep="first")])
447+
sec = dedup.drop(columns=["_k", "_s"]).sort_index()
448+
report["deduped_fix_commit"] = int(before - len(sec))
449+
report["security_rows"] = int(len(sec))
450+
report["by_authority_tier"] = {k: int(v) for k, v in sec["authority_tier"].value_counts().items()}
451+
print(f"[dedup] removed {report['deduped_fix_commit']} same-fix-commit duplicates "
452+
f"-> {len(sec)} rows", file=sys.stderr)
453+
428454
print(json.dumps(report, indent=2, ensure_ascii=False))
429455
assert report["residual_boilerplate_fp"] == 0, "T1 leak: boilerplate survived into the security set"
430456

0 commit comments

Comments
 (0)