tracegrpo / scripts /annotate_corpus.py
Dhruv1000's picture
Add files using upload-large-folder tool
867597e verified
Raw History Blame Contribute Delete
5.33 kB
"""Phase 2 CLI: runs annotate_sample over the corpus, writing one JSONL
record per sample. Resumable (spec §4: "resumable... per-sample artifact
files, skip if exists") via a line-count check against an existing output
file combined with --resume; CPU-parallelizable via --workers (spec: "target
16 workers") using a process pool, since tree-sitter parsing is CPU-bound and
releases the GIL across process boundaries trivially via multiprocessing.
Does not invoke Joern (see src/cpg/annotate.py's module docstring) — this is
the tree-sitter-backed first pass. A real Joern batch pass + calibration
subset (spec §4.1's 200-sample edge-recall comparison) is tracked as
follow-up, not run yet.
"""
from __future__ import annotations
import argparse
import json
import signal
from multiprocessing import Pool
from pathlib import Path
import pandas as pd
from src.cpg.annotate import annotate_sample, annotation_to_dict
ROW_TIMEOUT_S = 15
class _RowTimeout(Exception):
pass
def _alarm_handler(signum, frame):
raise _RowTimeout()
def _annotate_row(row: dict) -> dict:
"""Never lets one pathological sample kill — or hang — the whole pool
(spec §1.6: "fail loudly... never silently drop >2% of samples without
logging a warning with counts and reasons"). Two failure modes guarded:
exceptions (caught directly) and hangs/pathological slowness (bounded by
a SIGALRM timeout). The latter was found empirically, not hypothetically:
the real corpus contains at least one PrimeVul row whose "function" is a
482KB/23,938-line blob (clearly a misclassified data table, not a real
C function) that made build_defuse_graph's O(n^2) edge-building step
(src/cpg/defuse.py) take so long it looked indistinguishable from a
hang. A size threshold would have been a narrower, more brittle fix —
the timeout catches this case AND any future pathological input we
haven't seen yet, without needing to characterize what "too large" means
in advance."""
signal.signal(signal.SIGALRM, _alarm_handler)
signal.alarm(ROW_TIMEOUT_S)
try:
ann = annotate_sample(
sample_id=row["sample_id"],
func_vuln=row["func_vuln"],
patch_lines_vuln=list(row["patch_lines_vuln"]),
cwe=row["cwe"],
)
return annotation_to_dict(ann)
except _RowTimeout:
return {
"sample_id": row["sample_id"],
"error": f"timeout after {ROW_TIMEOUT_S}s (func_len={len(row['func_vuln'])} chars)",
"l1_lines": [], "l2_lines": [], "l3_lines": [],
"trace_gt": [], "trace_unverifiable": True, "trace_reason": "annotation_timeout",
"annotation_backend": "tree_sitter",
}
except Exception as e: # noqa: BLE001 - deliberately broad, see docstring
return {
"sample_id": row["sample_id"],
"error": f"{type(e).__name__}: {e}",
"l1_lines": [], "l2_lines": [], "l3_lines": [],
"trace_gt": [], "trace_unverifiable": True, "trace_reason": "annotation_error",
"annotation_backend": "tree_sitter",
}
finally:
signal.alarm(0)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--corpus", default="data/processed/corpus.parquet")
parser.add_argument("--out", default="data/processed/annotations.jsonl")
parser.add_argument("--workers", type=int, default=16)
parser.add_argument("--limit", type=int, default=None, help="for smoke testing on a subset")
parser.add_argument("--resume", action="store_true", default=False)
args = parser.parse_args()
df = pd.read_parquet(args.corpus)
if args.limit:
df = df.head(args.limit)
out_path = Path(args.out)
already_done: set[str] = set()
if args.resume and out_path.exists():
with open(out_path) as f:
for line in f:
line = line.strip()
if line:
already_done.add(json.loads(line)["sample_id"])
print(f"resuming: {len(already_done)} samples already annotated")
rows = [r for r in df.to_dict("records") if r["sample_id"] not in already_done]
print(f"annotating {len(rows)} samples with {args.workers} workers")
out_path.parent.mkdir(parents=True, exist_ok=True)
n_errors = 0
n_trace_unverifiable = 0
mode = "a" if args.resume else "w"
with open(out_path, mode) as f, Pool(args.workers) as pool:
for i, result in enumerate(pool.imap_unordered(_annotate_row, rows, chunksize=10)):
f.write(json.dumps(result) + "\n")
if result.get("error"):
n_errors += 1
elif result["trace_unverifiable"]:
n_trace_unverifiable += 1
if (i + 1) % 1000 == 0:
f.flush()
print(f"{i + 1}/{len(rows)} annotated ({n_errors} errors so far)")
error_rate = n_errors / len(rows) if rows else 0.0
print(f"done. {len(rows)} annotated, {n_trace_unverifiable} trace_unverifiable, "
f"{n_errors} errors ({error_rate:.1%})")
if error_rate > 0.02:
print(f"WARNING: error rate {error_rate:.1%} exceeds the 2% threshold spec §1.6 "
f"flags as needing investigation, not silent acceptance.")
if __name__ == "__main__":
main()