feat(pair-corpus-honesty): score non-vendored pointer pairs through a local cache

Task 8.3 (Req 10.1-10.5).

- pairs.py: catalog rows with vendored = false carry their harvest recipe
  and no excerpts; evaluate_catalog harvests them into
  OPENULTRASAST_PAIR_CACHE (default ~/.cache/openultrasast/pairs) only
  when `pairs --pointers` or OPENULTRASAST_PAIRS_NETWORK=1 allows the
  network, otherwise records pointer_pair_skipped (a failed fetch is
  pointer_pair_fetch_failed with detail); a warm cache never scores
  without the switch. select_vendored for tests and gates.
- harvest.py: materialize_pointer harvests into the cache without the
  license requirement (nothing is redistributed); redaction applies.
- catalog_gen.py: emits pointer rows with recipe fields; training
  manifests skip them.
- pair_gate and improve/evolve.evaluate_profiles score vendored pairs
  only; cli `pairs --pointers`; .gitignore covers the cache path.
- tests: loader, skip/degradation, cache hit, fetch failure, warm cache
  with network off, profile gate exclusion, harvest and generator paths;
  conftest keeps every test offline whatever the shell exports.

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:43 +02:00
co-authored by Claude Fable 5.1
parent 7d238f2400
commit 4102404442
13 changed files with 405 additions and 21 deletions
+17 -4
View File
@@ -373,7 +373,7 @@ def provenance_header(recipe: dict[str, Any], *, side: str, sha: str, upstream_s
return "/* " + "\n * ".join(lines) + "\n */\n"
def validate_recipe(recipe: dict[str, Any]) -> None:
def validate_recipe(recipe: dict[str, Any], *, require_license: bool = True) -> None:
name = str(recipe.get("name", ""))
if any(token in str(recipe).lower() for token in BLOCKED_TOKENS):
raise RecipeError(f"{name}: embargoed or blocked dataset")
@@ -390,7 +390,7 @@ def validate_recipe(recipe: dict[str, Any]) -> None:
raise RecipeError(f"{name}: line_range mode needs integer line_start and line_end")
if mode == "enclosing" and not isinstance(recipe.get("line"), int):
raise RecipeError(f"{name}: enclosing mode needs an integer line")
if not recipe.get("license"):
if require_license and not recipe.get("license"):
raise RecipeError(f"{name}: license required")
@@ -514,8 +514,8 @@ def extract_pair(recipe: dict[str, Any], parent_src: str, fix_src: str) -> tuple
return extract_function(parent_src, function, language), extract_function(fix_src, function, language)
def harvest_recipe(recipe: dict[str, Any], root: Path) -> tuple[Path, Path]:
validate_recipe(recipe)
def harvest_recipe(recipe: dict[str, Any], root: Path, *, require_license: bool = True) -> tuple[Path, Path]:
validate_recipe(recipe, require_license=require_license)
if str(recipe.get("host", "github")) != "github":
raise RecipeError(f"{recipe.get('name')}: live fetch currently implemented for host=github only")
parent_src = fetch_url(github_raw_url(str(recipe["repo"]), str(recipe["parent"]), str(recipe["path"])))
@@ -531,6 +531,19 @@ def harvest_recipe(recipe: dict[str, Any], root: Path) -> tuple[Path, Path]:
return vuln_path, fixed_path
def materialize_pointer(recipe: dict[str, Any], cache_root: Path, *, slice_name: str) -> tuple[Path, Path]:
"""Harvest a non-vendored pair into ``cache_root/<slice>/<name>/`` (Req 10.2).
The cache lives outside the repository; a missing upstream license is allowed here because nothing is redistributed,
the excerpts stay on the operator's machine. Redaction applies as for vendored excerpts.
"""
root = Path(cache_root) / slice_name
vuln_path, fixed_path = excerpt_paths(recipe, root)
if vuln_path.is_file() and fixed_path.is_file():
return vuln_path, fixed_path
return harvest_recipe(recipe, root, require_license=False)
def fetch_url(url: str, timeout: int = 60) -> str:
host = urlparse(url).hostname or ""
if host not in ALLOWED_HOSTS: