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
92 lines
3.1 KiB
Python
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),
|
|
}
|