feat(pair-corpus-honesty): shared function naming, harvest library, catalog generator

Tasks 3.1, 3.2 (harvest library) and the Scorer-owned naming helper.

- semantic/functions.py: one source of function names for scorer, overlay
  and catalog generator (declarator wins over parser name; JS/TS bare-param
  arrows and undeclared methods are <anon>; C++ Class::method resolves to
  its unqualified tail; Perl sub; statements never name).
- benchmarks/pairs/harvest.py: shared library with name / line_range /
  hunk / enclosing modes, comment-blind masking (triple-quote aware),
  upstream_start headers, code-safe quoted-only redaction.
- benchmarks/pairs/catalog_gen.py: derive function labels and sinks from
  the declarator at the mapped sink location, CWE-family sink tokens,
  --prune with refusal when nothing derived, fails loud without the
  package; vfc/harvest.py and vfc/generate_catalog.py become shims.
- tests/test_pair_harvest.py replaces test_vfc_harvest.py.

Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01T7XD7x5KonMdmGRkp63EQG
This commit is contained in:
Marius Ciepluch
2026-09-05 10:45:42 +02:00
co-authored by Claude Fable 5.1
parent 0116f9bea6
commit 071dea4712
7 changed files with 1530 additions and 375 deletions
+4 -92
View File
@@ -1,104 +1,16 @@
#!/usr/bin/env python3
"""Rebuild catalog.toml and training/manifest.jsonl from recipes.toml + excerpts."""
"""Thin shim: ``python benchmarks/pairs/catalog_gen.py --slice vfc``."""
from __future__ import annotations
import json
import tomllib
import runpy
from pathlib import Path
ROOT = Path(__file__).resolve().parent
RECIPES = ROOT / "recipes.toml"
CATALOG = ROOT / "catalog.toml"
TRAINING = ROOT / "training" / "manifest.jsonl"
LANG_TO_PAIR = {"c": "c_cpp", "cpp": "c_cpp", "perl": "unknown", "javascript": "javascript", "python": "python", "java": "java"}
def excerpt_rel(recipe: dict[str, object]) -> tuple[str, str]:
name = str(recipe["name"])
ext = Path(str(recipe.get("path", "a.c"))).suffix or ".c"
return f"{name}/vuln{ext}", f"{name}/fixed{ext}"
def catalog_text(recipes: list[dict[str, object]]) -> str:
lines = [
"# Real-world VFC slice. Reviewed isolated functions.",
"# Honesty dashboard — not the local 100% CI gate.",
"# Do not add these names to LANGUAGE_MANIFESTS.",
"# Regenerated by generate_catalog.py from recipes.toml.",
"",
]
for recipe in recipes:
vuln, fixed = excerpt_rel(recipe)
lang = LANG_TO_PAIR.get(str(recipe.get("language", "c")), "c_cpp")
lines += [
"[[pair]]",
f'name = "{recipe["name"]}"',
'slice = "vfc"',
f'language = "{lang}"',
f'origin = "{recipe.get("origin", "")}"',
f'license = "{recipe.get("license", "")}"',
f'repo = "{recipe.get("repo", "")}"',
f'cve = "{recipe.get("cve", "")}"',
f'cwe = "{recipe.get("cwe", "")}"',
f'commit = "{recipe.get("commit", "")}"',
f'parent = "{recipe.get("parent", "")}"',
f'commit_url = "{recipe.get("commit_url", "")}"',
f'vuln = "{vuln}"',
f'fixed = "{fixed}"',
f'relpath = "{recipe.get("relpath", recipe.get("path", ""))}"',
"min_recall = 1.0",
'fix_policy = "silent"',
"",
"[[pair.expected]]",
f'cwe = "{recipe.get("cwe", "")}"',
f'class = "{recipe.get("class", "")}"',
f'path = "{recipe.get("relpath", recipe.get("path", ""))}"',
]
if recipe.get("rule_id"):
lines.append(f'rule_id = "{recipe["rule_id"]}"')
if recipe.get("sink"):
lines.append(f'sink = "{recipe["sink"]}"')
if recipe.get("evidence"):
evidence = str(recipe["evidence"]).replace('"', '\\"')
lines.append(f'evidence = "{evidence}"')
lines.append("")
return "\n".join(lines)
def training_rows(recipes: list[dict[str, object]]) -> list[dict[str, object]]:
rows: list[dict[str, object]] = []
for recipe in recipes:
vuln, fixed = excerpt_rel(recipe)
base = {
"pair": recipe["name"],
"cve": recipe.get("cve", ""),
"year": int(recipe.get("year", 0) or 0),
"cwe": recipe.get("cwe", ""),
"class": recipe.get("class", ""),
"project": recipe.get("project", recipe.get("origin", "")),
"repo": recipe.get("repo", ""),
"license": recipe.get("license", ""),
"function": recipe.get("function", ""),
"relpath": recipe.get("relpath", recipe.get("path", "")),
"commit_url": recipe.get("commit_url", ""),
}
rows.append({**base, "id": f"{recipe['name']}:vuln", "label": "vuln", "file": vuln, "commit": recipe.get("parent", "")})
rows.append({**base, "id": f"{recipe['name']}:fixed", "label": "fixed", "file": fixed, "commit": recipe.get("commit", "")})
return rows
_LIB = Path(__file__).resolve().parents[1] / "catalog_gen.py"
def main() -> int:
recipes = list(tomllib.loads(RECIPES.read_text()).get("recipe", []))
CATALOG.write_text(catalog_text(recipes), encoding="utf-8")
TRAINING.parent.mkdir(parents=True, exist_ok=True)
with TRAINING.open("w", encoding="utf-8") as handle:
for row in training_rows(recipes):
handle.write(json.dumps(row, sort_keys=True) + "\n")
print(f"wrote {CATALOG} ({len(recipes)} pairs)")
print(f"wrote {TRAINING} ({len(recipes) * 2} examples)")
return 0
return int(runpy.run_path(str(_LIB), run_name="catalog_gen_lib")["main"](["--slice", "vfc"]))
if __name__ == "__main__":