fold/scripts/benchmarks/fold_eval/metamorphic.py
trvon 2dcbdbc75d
feat: harden Fold quality guard
Preserve source, binary, and runtime evidence boundaries while making precision and performance regressions reproducible.

- decompose analysis orchestration into bounded modules
- add differential, metamorphic, organic, and Yams change-time gates
- fix detector, P-Code, fuzz sanitizer, and report identity precision
2026-07-13 15:05:35 -04:00

92 lines
3.1 KiB
Python

"""Compare analyzer observations under semantics-preserving transformations."""
from __future__ import annotations
import json
from collections.abc import Mapping, Sequence
from typing import Any
from .stability import jaccard
DEFAULT_FIELDS = (
"rule_id",
"backend",
"confidence",
"summary_backed",
"cross_tu_corroboration",
"uri",
"line",
"function",
)
def _mapped_observation(
observation: Mapping[str, Any], relation: Mapping[str, Any]
) -> dict[str, Any]:
mapped = dict(observation)
uri_map = relation.get("uri_map", {})
function_map = relation.get("function_map", {})
line_delta = relation.get("line_delta", {})
if isinstance(uri_map, dict):
mapped["uri"] = uri_map.get(str(mapped.get("uri", "")), mapped.get("uri", ""))
if isinstance(function_map, dict):
mapped["function"] = function_map.get(
str(mapped.get("function", "")), mapped.get("function", "")
)
uri = str(mapped.get("uri", ""))
if isinstance(line_delta, dict) and uri in line_delta:
mapped["line"] = int(mapped.get("line", 0)) + int(line_delta[uri])
return mapped
def _key(observation: Mapping[str, Any], fields: Sequence[str]) -> str:
selected = {field: observation.get(field) for field in fields}
return json.dumps(selected, sort_keys=True, separators=(",", ":"))
def compare_metamorphic_findings(
baseline: Sequence[Mapping[str, Any]],
transformed: Sequence[Mapping[str, Any]],
relation: Mapping[str, Any] | None = None,
) -> dict[str, Any]:
"""Apply a declared mapping and compare finding multisets.
The relation can map URIs/functions, shift lines, or select identity fields.
Findings are compared as multisets so duplicate-alert regressions remain
visible instead of collapsing behind set equality.
"""
relation = relation or {}
fields = relation.get("fields", DEFAULT_FIELDS)
if not isinstance(fields, (list, tuple)) or not fields or not all(
isinstance(field, str) and field for field in fields
):
raise ValueError("relation.fields must be a non-empty string array")
mapped_baseline = [_mapped_observation(item, relation) for item in baseline]
left = sorted(_key(item, fields) for item in mapped_baseline)
right = sorted(_key(item, fields) for item in transformed)
left_counts: dict[str, int] = {}
right_counts: dict[str, int] = {}
for key in left:
left_counts[key] = left_counts.get(key, 0) + 1
for key in right:
right_counts[key] = right_counts.get(key, 0) + 1
missing: list[str] = []
unexpected: list[str] = []
for key in sorted(set(left_counts) | set(right_counts)):
delta = left_counts.get(key, 0) - right_counts.get(key, 0)
if delta > 0:
missing.extend([key] * delta)
elif delta < 0:
unexpected.extend([key] * -delta)
return {
"exact": not missing and not unexpected,
"baseline_count": len(left),
"transformed_count": len(right),
"missing": missing,
"unexpected": unexpected,
"set_jaccard": jaccard(set(left), set(right)),
"fields": list(fields),
}