mirror of
https://github.com/norandom/OpenUltraSAST.git
synced 2026-11-13 01:48:17 +00:00
feat(access-control-prover): 12.3 instrument and hard runtime gates
This commit is contained in:
@@ -270,7 +270,7 @@ its final resolution, dominance, fact-acquisition and measurement semantics with
|
||||
- _Requirements: 5.1, 5.3, 5.4, 7.1, 7.6, 9.7, 11.8_
|
||||
- _Boundary: Development Runner_
|
||||
- _Depends: 10.6, 12.1_
|
||||
- [ ] 12.3 Enforce instrument and hard runtime gates
|
||||
- [x] 12.3 Enforce instrument and hard runtime gates
|
||||
- Preserve zero-byte and stale-anchor refusal and add stage-aware deadline records.
|
||||
- Fail the run if any measured change exceeds 120 seconds; never summarize the overrun as a passing result.
|
||||
- Done when zero-byte, stale-anchor, deadline and 121-second fixtures all refuse the summary.
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""Development runner: per-record provenance, stage funnel and judgment transitions.
|
||||
|
||||
Task 12.2 of the access-control-prover spec (Reqs 5.1, 5.3, 5.4, 7.1, 7.6, 9.7, 11.8).
|
||||
Every development record carries the bundle digests (analyzer revision, fact set,
|
||||
acquisition record, configuration) plus the population and per-change input digests,
|
||||
a stage funnel from entry recognition through resolution to the three-way judgment
|
||||
counts, and the transition label between compared judgments.
|
||||
Tasks 12.2 and 12.3 of the access-control-prover spec (Reqs 5.1, 5.3, 5.4, 5.6,
|
||||
6.1, 6.3, 7.1, 7.6, 9.7, 11.8). Every development record carries the bundle
|
||||
digests (analyzer revision, fact set, acquisition record, configuration) plus the
|
||||
population and per-change input digests, a stage funnel from entry recognition
|
||||
through resolution to the three-way judgment counts, and the transition label
|
||||
between compared judgments. The runner gates refuse to summarise an instrument
|
||||
failure (zero-byte read, stale anchor) or a change over the hard runtime budget.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -116,4 +118,78 @@ def run_development(
|
||||
return tuple(records)
|
||||
|
||||
|
||||
__all__ = ["DevelopmentRecord", "judgment_transition", "run_development", "stage_funnel"]
|
||||
class GateFailure(RuntimeError):
|
||||
"""A measured change exceeded the hard runtime budget (Req 6.1)."""
|
||||
|
||||
|
||||
class InstrumentFailure(RuntimeError):
|
||||
"""An instrument failure (zero-byte read, stale anchor) refuses the summary (Req 5.6)."""
|
||||
|
||||
|
||||
_INSTRUMENT_FAILURE_MARKERS = ("zero-byte", "stale_anchor")
|
||||
|
||||
|
||||
def enforce_gates(records: tuple[DevelopmentRecord, ...], *, budget_seconds: float = 120.0) -> None:
|
||||
"""Fail when any measured change exceeds the budget; never let an overrun pass (Req 6.1)."""
|
||||
for record in records:
|
||||
if record.seconds > budget_seconds:
|
||||
raise GateFailure(f"change {record.change_id} took {record.seconds:.1f}s over the {budget_seconds:.1f}s budget")
|
||||
|
||||
|
||||
def deadline_record(record: DevelopmentRecord, *, budget_seconds: float = 120.0) -> dict[str, object]:
|
||||
"""Stamp the stage-aware deadline view of one record (Req 6.3)."""
|
||||
return {
|
||||
"deadline_seconds": budget_seconds,
|
||||
"exceeded": record.seconds > budget_seconds,
|
||||
}
|
||||
|
||||
|
||||
def _instrument_failure(record: DevelopmentRecord) -> str | None:
|
||||
for value in _funnel_strings(record.stage_funnel):
|
||||
for marker in _INSTRUMENT_FAILURE_MARKERS:
|
||||
if marker in value:
|
||||
return value
|
||||
return None
|
||||
|
||||
|
||||
def _funnel_strings(funnel: dict[str, dict[str, Any]]) -> list[str]:
|
||||
strings: list[str] = []
|
||||
for stage in funnel.values():
|
||||
for value in stage.values():
|
||||
if isinstance(value, str):
|
||||
strings.append(value)
|
||||
return strings
|
||||
|
||||
|
||||
def summarize_development(records: tuple[DevelopmentRecord, ...], *, budget_seconds: float = 120.0) -> dict[str, object]:
|
||||
"""Summarise a development run; an overrun or instrument failure refuses the summary."""
|
||||
enforce_gates(records, budget_seconds=budget_seconds)
|
||||
for record in records:
|
||||
failure = _instrument_failure(record)
|
||||
if failure is not None:
|
||||
raise InstrumentFailure(f"change {record.change_id} instrument failure: {failure}")
|
||||
judgment_counts = {"unguarded": 0, "discharged": 0, "indeterminate": 0}
|
||||
for record in records:
|
||||
judgment = record.stage_funnel.get("judgment", {})
|
||||
for kind in judgment_counts:
|
||||
judgment_counts[kind] += int(judgment.get(kind, 0))
|
||||
seconds = [record.seconds for record in records]
|
||||
return {
|
||||
"total_changes": len(records),
|
||||
"judgment_counts": judgment_counts,
|
||||
"total_seconds": sum(seconds),
|
||||
"max_seconds": max(seconds, default=0.0),
|
||||
}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"DevelopmentRecord",
|
||||
"GateFailure",
|
||||
"InstrumentFailure",
|
||||
"deadline_record",
|
||||
"enforce_gates",
|
||||
"judgment_transition",
|
||||
"run_development",
|
||||
"stage_funnel",
|
||||
"summarize_development",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
"""Task 12.3 -- runner instrument and hard runtime gates (Reqs 5.6, 6.1, 6.3).
|
||||
|
||||
Gate fixtures refuse the summary: zero-byte and stale-anchor records raise
|
||||
InstrumentFailure, a change over the 120-second budget raises GateFailure, and
|
||||
the deadline record stamps budget + exceeded flag per record.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
|
||||
|
||||
from benchmarks.valid import development_runner # noqa: E402
|
||||
from benchmarks.valid.development_runner import ( # noqa: E402
|
||||
DevelopmentRecord,
|
||||
InstrumentFailure,
|
||||
deadline_record,
|
||||
enforce_gates,
|
||||
summarize_development,
|
||||
)
|
||||
|
||||
GateFailure = development_runner.GateFailure
|
||||
|
||||
|
||||
def _record(
|
||||
*,
|
||||
seconds: float = 1.0,
|
||||
change_id: str = "change-1",
|
||||
instrument_reason: str | None = None,
|
||||
) -> DevelopmentRecord:
|
||||
judgment: dict[str, object] = {"unguarded": 0, "discharged": 0, "indeterminate": 1}
|
||||
funnel: dict[str, dict[str, object]] = {"judgment": judgment}
|
||||
if instrument_reason is not None:
|
||||
funnel["instrument"] = {"reason": instrument_reason}
|
||||
return DevelopmentRecord(
|
||||
change_id=change_id,
|
||||
analyzer_revision="0" * 12,
|
||||
fact_set_digest="1" * 12,
|
||||
acquisition_record_digest="2" * 12,
|
||||
configuration_digest="3" * 12,
|
||||
population_digest="4" * 12,
|
||||
input_digest="5" * 12,
|
||||
stage_funnel=funnel,
|
||||
judgment_transition="same",
|
||||
seconds=seconds,
|
||||
)
|
||||
|
||||
|
||||
def test_enforce_gates_passes_clean_records() -> None:
|
||||
enforce_gates((_record(seconds=1.0), _record(seconds=119.9, change_id="change-2")))
|
||||
|
||||
|
||||
def test_enforce_gates_fails_a_change_over_the_budget() -> None:
|
||||
with pytest.raises(GateFailure) as caught:
|
||||
enforce_gates((_record(seconds=0.5), _record(seconds=121.0, change_id="slow-change")))
|
||||
assert "slow-change" in str(caught.value)
|
||||
|
||||
|
||||
def test_budget_is_overridable_for_fixtures() -> None:
|
||||
with pytest.raises(GateFailure):
|
||||
enforce_gates((_record(seconds=6.0),), budget_seconds=5.0)
|
||||
|
||||
|
||||
def test_deadline_record_stamps_budget_and_exceeded() -> None:
|
||||
kept = deadline_record(_record(seconds=1.0), budget_seconds=120.0)
|
||||
assert kept == {"deadline_seconds": 120.0, "exceeded": False}
|
||||
blown = deadline_record(_record(seconds=121.0), budget_seconds=120.0)
|
||||
assert blown == {"deadline_seconds": 120.0, "exceeded": True}
|
||||
|
||||
|
||||
def test_summary_refuses_an_overrun_never_reports_it_passing() -> None:
|
||||
with pytest.raises(GateFailure):
|
||||
summarize_development((_record(seconds=121.0),))
|
||||
|
||||
|
||||
def test_summary_reports_clean_records() -> None:
|
||||
summary = summarize_development((_record(seconds=1.0), _record(seconds=2.0, change_id="change-2")))
|
||||
assert summary["total_changes"] == 2
|
||||
assert summary["max_seconds"] == 2.0
|
||||
|
||||
|
||||
def test_zero_byte_record_refuses_the_summary() -> None:
|
||||
with pytest.raises(InstrumentFailure) as caught:
|
||||
summarize_development((_record(instrument_reason="zero-byte read"),))
|
||||
assert "zero-byte" in str(caught.value)
|
||||
|
||||
|
||||
def test_stale_anchor_record_refuses_the_summary() -> None:
|
||||
with pytest.raises(InstrumentFailure) as caught:
|
||||
summarize_development((_record(instrument_reason="stale_anchor"),))
|
||||
assert "stale_anchor" in str(caught.value)
|
||||
Reference in New Issue
Block a user