feat(pre-push-safety-net): key evidence reuse by compatible semantics

This commit is contained in:
Marius Ciepluch
2026-09-13 16:16:19 +02:00
parent 6fb5ad2cff
commit 0ab5d56618
6 changed files with 276 additions and 4 deletions
@@ -69,3 +69,37 @@ No graph node mutation, executable deserialization or repository writes are intr
- CLAIM: Task 6.2 bounded local artifact publication and eviction. - CLAIM: Task 6.2 bounded local artifact publication and eviction.
- EVIDENCE: nine cache contract tests and full suite above; production cache implementation, not a mocked storage path. - EVIDENCE: nine cache contract tests and full suite above; production cache implementation, not a mocked storage path.
- GAPS: Semantic keys and live replay integration follow in 6.3 and 6.4; no warm-latency claim. - GAPS: Semantic keys and live replay integration follow in 6.3 and 6.4; no warm-latency claim.
## Task 6.3 brief
Requirements 3.4, 4.3, 4.4, 6.4, 7.2 and 7.3; design: Cache and Engine Boundary
and Admission Policy. Query keys include graph, actual question parameters/context/depth,
facts/sanitizers, query versions and configuration. Comparison/final keys additionally bind
ranking, admission, optional model/prompt, eligibility and mode; comparisons include both
revisions and actual completed evidence. Ranking-only changes preserve query keys.
Comparison reuse is integrated into the existing policy entry point. Only complete, comparable
evidence can populate/look up comparisons. Unknown outcomes never qualify. Loaded CandidateDelta
contracts must match revision and semantics ownership. Current admission still runs afterwards;
the cache never confers eligibility. JSON decoding is non-executable and bounded to 32 MiB.
## Status Report
- STATUS: READY_FOR_REVIEW
- TASK: 6.3
- RED_PHASE_OUTPUT: `/tmp/group6-3-red.log`: two failing semantic-key tests with switch OFF; `/tmp/group6-3-policy-red.log`: missing policy cache integration failed. Enabled implementation passed; switch removed.
- TESTS_RUN: 71 focused policy/admission/cache tests passed; full suite 1260 passed, 9 skipped in 103.17s. Final ruff, formatting and mypy passed.
- EVIDENCE: real on-disk comparison hit returns identical supported delta; policy/revision changes miss; unanswered comparisons do not populate complete entries; exhaustive key-dimension and final-scope invalidation tests.
## Review Verdict
- VERDICT: APPROVED
- TASK: 6.3
- MECHANICAL_RESULTS: full regression suite, 71 focused tests, static checks and whitespace checks passed; no new placeholders or secrets.
- FINDINGS: cache does not store or replay push permission. Comparison data remains separate from current admission. Query reuse deliberately ignores ranking-only/mode changes, whereas comparison/final keys include them. Decoder and deadline checks were tightened during review.
- SUMMARY: Semantic identities preserve compatible evidence without transferring obsolete policy eligibility.
## Verification Result
- STATUS: VERIFIED
- CLAIM_TYPE: TASK
- CLAIM: Task 6.3 semantic keys and completed comparison evidence reuse.
- EVIDENCE: production policy integration and cache key tests, 1260 passing suite tests and final static checks.
- GAPS: Graph/query/preparation reuse must still be connected to replay under 6.4; no rollout claim.
+1 -1
View File
@@ -174,7 +174,7 @@ hook latency is admitted.
- _Boundary: Cache and Engine Boundary, push cache integration_ - _Boundary: Cache and Engine Boundary, push cache integration_
- _Depends: 6.1_ - _Depends: 6.1_
- _Requirements: 4.1, 4.3, 4.4, 7.3_ - _Requirements: 4.1, 4.3, 4.4, 7.3_
- [ ] 6.3 Key query and comparison results by all relevant semantics - [x] 6.3 Key query and comparison results by all relevant semantics
- Include graph identity, question/context/depth, facts/sanitizers/query versions and configuration; include ranking/admission/model provenance for final results and both revision identities for comparisons. - Include graph identity, question/context/depth, facts/sanitizers/query versions and configuration; include ranking/admission/model provenance for final results and both revision identities for comparisons.
- Keep incomplete query/comparison results out of complete lookups; preserve current eligibility checks after reuse. - Keep incomplete query/comparison results out of complete lookups; preserve current eligibility checks after reuse.
- Done when source, context, fact, query, mode, policy and optional model changes invalidate the appropriate layer, while an identical compatible request returns the same evidence and provenance. - Done when source, context, fact, query, mode, policy and optional model changes invalidate the appropriate layer, while an identical compatible request returns the same evidence and provenance.
+54 -1
View File
@@ -12,10 +12,11 @@ import tempfile
import time import time
from collections.abc import Iterator from collections.abc import Iterator
from contextlib import ExitStack, contextmanager from contextlib import ExitStack, contextmanager
from dataclasses import dataclass from dataclasses import asdict, dataclass
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
from openultrasast.contracts import Contract
from openultrasast.cpg.artifact import digest_value, graph_bytes, read_bytes from openultrasast.cpg.artifact import digest_value, graph_bytes, read_bytes
from openultrasast.cpg.backend import _DeadlineExpired, _remove_owned_tree from openultrasast.cpg.backend import _DeadlineExpired, _remove_owned_tree
from openultrasast.model.contracts import ExecutionBudget from openultrasast.model.contracts import ExecutionBudget
@@ -123,6 +124,24 @@ class ArtifactCache:
self.hits += 1 self.hits += 1
yield entry yield entry
def get_json(self, key: str, *, kind: str, budget: ExecutionBudget) -> Any:
with self.lookup(key, budget) as entry:
if entry is None or entry.metadata != {"kind": kind} or entry.path.stat().st_size > 32 * 1024**2:
return None
try:
return json.loads(read_bytes(entry.path, budget.deadline_monotonic))
except (OSError, ValueError, TimeoutError):
return None
def put_json(self, key: str, payload: object, *, kind: str, budget: ExecutionBudget, complete: bool) -> bool:
if not complete or time.monotonic() >= budget.deadline_monotonic:
return False
try:
data = json.dumps(payload, sort_keys=True, allow_nan=False).encode()
except (TypeError, ValueError):
return False
return self.publish(key, data, {"kind": kind}, budget, complete=complete)
def _remove(self, root: Path, budget: ExecutionBudget) -> None: def _remove(self, root: Path, budget: ExecutionBudget) -> None:
_remove_owned_tree(root, budget.deadline_monotonic) _remove_owned_tree(root, budget.deadline_monotonic)
@@ -215,3 +234,37 @@ class ArtifactCache:
_remove_owned_tree(pending, budget.deadline_monotonic + budget.cancellation_allowance_seconds) _remove_owned_tree(pending, budget.deadline_monotonic + budget.cancellation_allowance_seconds)
except (OSError, _DeadlineExpired): except (OSError, _DeadlineExpired):
self.last_reason = "cache_partial_cleanup_incomplete" self.last_reason = "cache_partial_cleanup_incomplete"
@dataclass(frozen=True)
class SemanticKeys(Contract):
facts: str
queries: str
configuration: str
ranking: str
admission: str
model: str
eligibility: str
mode: str
def query(self, *, graph: str, kind: str, request: object, context: object) -> str:
return digest_value(
{
"layer": "query-v1",
"graph": graph,
"kind": kind,
"request": request,
"context": context,
"facts": self.facts,
"queries": self.queries,
"configuration": self.configuration,
}
)
def comparison(self, *, base: str, head: str, context: object, evidence: object) -> str:
return digest_value(
{"layer": "comparison-v1", "base": base, "head": head, "context": context, "evidence": evidence, "semantics": asdict(self)}
)
def result(self, *, comparisons: object, scope: object) -> str:
return digest_value({"layer": "result-v1", "comparisons": comparisons, "scope": scope, "semantics": asdict(self)})
+98 -2
View File
@@ -6,7 +6,7 @@ import hashlib
import json import json
import time import time
from collections.abc import Sequence from collections.abc import Sequence
from dataclasses import dataclass, replace from dataclasses import asdict, dataclass, replace
from pathlib import Path, PurePosixPath from pathlib import Path, PurePosixPath
from string import Formatter from string import Formatter
from typing import Any, Literal from typing import Any, Literal
@@ -20,6 +20,7 @@ from openultrasast.model.pipeline import ModelFinding, _arbitrate_all
from openultrasast.model.regions import ScanRegion from openultrasast.model.regions import ScanRegion
from openultrasast.model.scan import ModelScanResult, ScanBudget, _prefetched, _spec_for, scan_repository from openultrasast.model.scan import ModelScanResult, ScanBudget, _prefetched, _spec_for, scan_repository
from openultrasast.model.specs import ConfigSpec, DominanceSpec, TaintSpec from openultrasast.model.specs import ConfigSpec, DominanceSpec, TaintSpec
from openultrasast.push.cache import ArtifactCache, SemanticKeys
from openultrasast.push.contracts import ComparisonAnalysis, PushComparison, PushResult from openultrasast.push.contracts import ComparisonAnalysis, PushComparison, PushResult
@@ -247,6 +248,92 @@ def compare_evidence(
head_semantics: str, head_semantics: str,
base_semantics: str, base_semantics: str,
execution_budget: ExecutionBudget | None = None, execution_budget: ExecutionBudget | None = None,
cache: ArtifactCache | None = None,
cache_semantics: SemanticKeys | None = None,
) -> tuple[CandidateDelta, ...]:
"""Reuse completed comparison evidence; alert eligibility is always applied later."""
def complete(scan: ModelScanResult | None) -> bool:
return bool(
scan
and scan.scope
and scan.scope.population_complete
and not scan.degradations
and not scan.scope.unresolved_boundaries
and all(item.reason == "tier_zero" for item in scan.scope.deferred)
and all(item.status == "completed" for item in scan.question_outcomes)
and {item.identity for item in scan.scope.selected} == {item.identity for item in scan.question_outcomes}
)
key = None
if (
cache is not None
and cache_semantics is not None
and execution_budget is not None
and time.monotonic() < execution_budget.deadline_monotonic
and context.base_revision is not None
and not context.unresolved_boundaries
and head_semantics == base_semantics
and complete(head)
and complete(base)
):
from openultrasast.cpg.artifact import read_bytes
def evidence(scan: ModelScanResult) -> dict[str, object]:
return {
name: value
for name, value in asdict(scan).items()
if name not in {"seconds", "build_seconds", "query_seconds", "query_seconds_by_kind"}
}
assert base is not None
try:
key = cache_semantics.comparison(
base=context.base_revision,
head=context.head_revision,
context=context.to_payload(),
evidence={
"head": evidence(head),
"base": evidence(base),
"semantics": head_semantics,
"policy": hashlib.sha256(read_bytes(Path(__file__), execution_budget.deadline_monotonic)).hexdigest(),
},
)
raw = cache.get_json(key, kind="comparison", budget=execution_budget)
if isinstance(raw, list):
decoded = tuple(CandidateDelta.from_payload(item) for item in raw)
if time.monotonic() < execution_budget.deadline_monotonic and all(
item.novelty != "unknown"
and item.base_revision == context.base_revision
and item.head_revision == context.head_revision
and item.semantics == head_semantics
for item in decoded
):
return decoded
except (OSError, ValueError, TimeoutError):
key = None
result = _compare_evidence(
head, base, context=context, head_semantics=head_semantics, base_semantics=base_semantics, execution_budget=execution_budget
)
if key is not None and cache is not None and execution_budget is not None:
cache.put_json(
key,
[item.to_payload() for item in result],
kind="comparison",
budget=execution_budget,
complete=all(item.novelty != "unknown" for item in result),
)
return result
def _compare_evidence(
head: ModelScanResult,
base: ModelScanResult | None,
*,
context: ChangeContext,
head_semantics: str,
base_semantics: str,
execution_budget: ExecutionBudget | None = None,
) -> tuple[CandidateDelta, ...]: ) -> tuple[CandidateDelta, ...]:
"""Absence counts only in completed comparable scope, never from scan silence.""" """Absence counts only in completed comparable scope, never from scan silence."""
head_ops, base_ops = _operations(head, execution_budget), _operations(base, execution_budget) if base else () head_ops, base_ops = _operations(head, execution_budget), _operations(base, execution_budget) if base else ()
@@ -356,6 +443,8 @@ def compare_targeted_base(
scan_budget: ScanBudget, scan_budget: ScanBudget,
head_semantics: str, head_semantics: str,
base_semantics: str, base_semantics: str,
cache: ArtifactCache | None = None,
cache_semantics: SemanticKeys | None = None,
) -> DeltaComparison: ) -> DeltaComparison:
"""Re-run counterparts of the ranker's selected head questions through the existing driver. """Re-run counterparts of the ranker's selected head questions through the existing driver.
@@ -398,7 +487,14 @@ def compare_targeted_base(
except Exception as error: # noqa: BLE001 -- comparison failure is coverage, not a new defect except Exception as error: # noqa: BLE001 -- comparison failure is coverage, not a new defect
reasons.append(f"base_scan_failed:{type(error).__name__}") reasons.append(f"base_scan_failed:{type(error).__name__}")
candidates = compare_evidence( candidates = compare_evidence(
head, base, context=context, head_semantics=head_semantics, base_semantics=base_semantics, execution_budget=execution_budget head,
base,
context=context,
head_semantics=head_semantics,
base_semantics=base_semantics,
execution_budget=execution_budget,
cache=cache,
cache_semantics=cache_semantics,
) )
reasons.extend(c.reason for c in candidates if c.novelty == "unknown") reasons.extend(c.reason for c in candidates if c.novelty == "unknown")
return DeltaComparison(candidates, base, head_semantics, tuple(dict.fromkeys(reasons))) return DeltaComparison(candidates, base, head_semantics, tuple(dict.fromkeys(reasons)))
+62
View File
@@ -0,0 +1,62 @@
"""Semantic layers invalidate independently and comparisons retain revision ownership."""
import time
from dataclasses import replace
from openultrasast.model.contracts import ExecutionBudget
from openultrasast.push.cache import ArtifactCache, SemanticKeys
def keys():
return SemanticKeys("facts", "queries", "configuration", "ranking", "admission", "model-disabled", "eligibility", "advisory")
def test_query_and_result_invalidation_layers():
original = keys()
def query(k):
return k.query(graph="graph", kind="taint", request={"file": "a.js", "depth": 3}, context="context")
def result(k):
return k.comparison(base="base", head="head", context="context", evidence="evidence")
for field in ("facts", "queries", "configuration"):
changed = replace(original, **{field: "changed"})
assert query(changed) != query(original)
assert result(changed) != result(original)
for field in ("ranking", "admission", "model", "eligibility", "mode"):
changed = replace(original, **{field: "changed"})
assert query(changed) == query(original)
assert result(changed) != result(original)
for field in ("graph", "kind", "request", "context"):
args = {"graph": "graph", "kind": "taint", "request": {"file": "a.js", "depth": 3}, "context": "context"}
args[field] = "changed"
assert original.query(**args) != query(original)
for field in ("base", "head", "context", "evidence"):
args = {"base": "base", "head": "head", "context": "context", "evidence": "evidence"}
args[field] = "changed"
assert original.comparison(**args) != result(original)
def test_json_evidence_retains_provenance_and_rejects_incomplete(tmp_path):
cache = ArtifactCache(tmp_path / "cache", max_bytes=10000)
budget = ExecutionBudget(time.monotonic() + 5, 0.2)
key = keys().query(graph="graph", kind="taint", request={}, context="context")
payload = {"rows": [], "provenance": {"facts": "facts", "query": "queries"}}
assert not cache.put_json(key, payload, kind="query", budget=budget, complete=False)
assert cache.get_json(key, kind="query", budget=budget) is None
assert cache.put_json(key, payload, kind="query", budget=budget, complete=True)
assert cache.get_json(key, kind="query", budget=budget) == payload
assert cache.get_json(key, kind="comparison", budget=budget) is None
def test_final_results_bind_scope_and_current_admission_provenance():
original = keys()
def result(k, scope="scope"):
return k.result(comparisons=["base-head"], scope=scope)
for field in ("facts", "queries", "configuration", "ranking", "admission", "model", "eligibility", "mode"):
assert result(replace(original, **{field: "changed"})) != result(original)
assert result(original, "different-scope") != result(original)
assert original.result(comparisons=["another-base-head"], scope="scope") != result(original)
+27
View File
@@ -238,3 +238,30 @@ def test_proven_tier_zero_other_families_do_not_invalidate_base_answer():
assert delta(scan(), base).novelty == "new" assert delta(scan(), base).novelty == "new"
base = replace(base, scope=replace(base.scope, deferred=(DeferredQuestion(other, "budget_exhausted", ()),))) base = replace(base, scope=replace(base.scope, deferred=(DeferredQuestion(other, "budget_exhausted", ()),)))
assert delta(scan(), base).novelty == "unknown" assert delta(scan(), base).novelty == "unknown"
def test_comparison_cache_reuses_only_completed_compatible_evidence(tmp_path, monkeypatch):
import openultrasast.push.policy as policy
from openultrasast.push.cache import ArtifactCache, SemanticKeys
cache = ArtifactCache(tmp_path / "cache", max_bytes=100000)
keys = SemanticKeys("facts", "queries", "config", "rank", "admission", "disabled", "empty", "advisory")
budget = ExecutionBudget(time.monotonic() + 10, 0.2)
args = dict(context=context(), head_semantics="same", base_semantics="same", execution_budget=budget, cache=cache, cache_semantics=keys)
expected = policy.compare_evidence(scan(), scan(line=5, sanitized=True, findings=False), **args)
assert expected[0].novelty == "worsened"
hits = cache.hits
assert policy.compare_evidence(scan(), scan(line=5, sanitized=True, findings=False), **args) == expected
assert cache.hits > hits
args["cache_semantics"] = replace(keys, admission="changed")
hits = cache.hits
policy.compare_evidence(scan(), scan(line=5, sanitized=True, findings=False), **args)
assert cache.hits == hits
args["context"] = replace(context(), base_revision="another-base")
changed = policy.compare_evidence(scan(), scan(line=5, sanitized=True, findings=False), **args)
assert changed[0].base_revision == "another-base"
incomplete = policy.compare_evidence(scan(), scan(status="unanswered"), **args)
assert incomplete[0].novelty == "unknown"
hits = cache.hits
policy.compare_evidence(scan(), scan(status="unanswered"), **args)
assert cache.hits == hits