Files
Open_Ultra_SAST/src/openultrasast/model/contracts.py
T
Marius CiepluchandClaude Opus 5.5 511de4b75f pre-push(17.4): a file new in head is new, not comparison unknown
ChangeContext records the paths Git reports as added. An entailed,
undischarged operation in an added file, with complete head evidence and no
identical operation in any base answer, is classified new
(file_added_in_head) instead of base_incomplete; admission accepts that reason
as change evidence. Unpaired add/delete pushes and every other unknown
comparison keep their vetoes. An engine finding whose novelty is established
and whose only vetoes are capability vetoes is shown as an advisory engine
finding, not an alert. Requirement 9.4.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01EmM1j46qgKMh3zqD1QMwEo
2026-10-02 22:24:24 +02:00

224 lines
7.8 KiB
Python

"""Language-agnostic change evidence and exact ranker scope (pre-push Req 7.3).
These are integration contracts; constructing them neither selects nor executes work.
Revision identities are opaque to the engine. The snapshot adapter resolves Git objects.
"""
from __future__ import annotations
import hashlib
import json
import os
from dataclasses import dataclass
from typing import Literal
from openultrasast.contracts import Contract
@dataclass(frozen=True)
class ExecutionBudget(Contract):
"""One absolute monotonic deadline, with a distinct bounded cleanup allowance.
Process-local: do not carry the absolute clock value across process restarts.
Manifests should record elapsed stage costs and configured durations instead.
"""
deadline_monotonic: float
cancellation_allowance_seconds: float
def __post_init__(self) -> None:
super().__post_init__()
if self.deadline_monotonic < 0 or self.cancellation_allowance_seconds <= 0:
raise ValueError("deadline must be nonnegative and cancellation allowance positive")
@dataclass(frozen=True)
class QuestionIdentity(Contract):
unit: str
language: str
path: str
function: str | None
family: str
@property
def question_id(self) -> str:
encoded = json.dumps(self.to_payload(), sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(encoded).hexdigest()
@dataclass(frozen=True)
class ChangedSpan(Contract):
path: str
start_line: int
end_line: int
side: Literal["base", "head"]
def __post_init__(self) -> None:
super().__post_init__()
if self.start_line < 1 or self.end_line < self.start_line:
raise ValueError("changed spans require positive inclusive ordered lines")
@dataclass(frozen=True)
class PathRename(Contract):
base_path: str
head_path: str
@dataclass(frozen=True)
class AffectedRelationship(Contract):
source: QuestionIdentity
target: QuestionIdentity
kind: str
evidence: tuple[str, ...]
@dataclass(frozen=True)
class LineCorrespondence(Contract):
"""Unique identical source lines outside edit hunks; never semantic identity proof.
A renamed declaration can retain its unchanged operation's lexical anchor.
Frontend evidence must still establish function/guard relationships and novelty.
Paths use the owning ChangeContext's explicit encoding.
"""
base_path: str
head_path: str
base_start_line: int
base_end_line: int
head_start_line: int
def __post_init__(self) -> None:
super().__post_init__()
if self.base_start_line < 1 or self.base_end_line < self.base_start_line or self.head_start_line < 1:
raise ValueError("line correspondence requires positive ordered lines")
@dataclass(frozen=True)
class ChangeContext(Contract):
base_revision: str | None
head_revision: str
changed_paths: tuple[str, ...]
deleted_paths: tuple[str, ...]
spans: tuple[ChangedSpan, ...]
renames: tuple[PathRename, ...]
declaration_paths: tuple[str, ...]
relationships: tuple[AffectedRelationship, ...]
unresolved_boundaries: tuple[str, ...]
# Filesystem paths can be whitespace-only or non-UTF8. Adapter-produced contexts use
# lowercase raw-byte hex for change paths, spans, renames and line anchors.
# Relationship QuestionIdentity paths retain canonical question encoding; they
# are not encoded or decoded by this field.
path_encoding: Literal["text", "filesystem-bytes-hex"] = "text"
line_correspondences: tuple[LineCorrespondence, ...] = ()
# Paths Git reports as added (status A) in head, in this context's path encoding. A rename
# target is not added. Only a lexical fact: novelty still needs complete head evidence.
added_paths: tuple[str, ...] = ()
def decode_path(self, path: str) -> str:
"""Return the filesystem spelling for matching ordinary question paths.
Raw undecodable filename bytes survive through filesystem surrogate escapes;
serialization should retain the authoritative encoded contract path instead.
"""
return os.fsdecode(bytes.fromhex(path)) if self.path_encoding == "filesystem-bytes-hex" else path
def __post_init__(self) -> None:
super().__post_init__()
if self.path_encoding == "filesystem-bytes-hex":
paths = [
*self.changed_paths,
*self.deleted_paths,
*self.declaration_paths,
*(s.path for s in self.spans),
*(p for r in self.renames for p in (r.base_path, r.head_path)),
*(p for m in self.line_correspondences for p in (m.base_path, m.head_path)),
*self.added_paths,
]
for path in paths:
try:
raw = bytes.fromhex(path)
except ValueError as error:
raise ValueError("invalid raw filesystem path hex") from error
if not raw or b"\0" in raw or raw.hex() != path:
raise ValueError("invalid raw filesystem path hex")
@dataclass(frozen=True)
class RankedQuestion(Contract):
identity: QuestionIdentity
priority: float
evidence: tuple[str, ...]
tier: int | None = None
score: float | None = None
@dataclass(frozen=True)
class DeferredQuestion(Contract):
identity: QuestionIdentity
reason: str
evidence: tuple[str, ...]
@dataclass(frozen=True)
class ScopeDecision(Contract):
ranker_version: str
ranking_mode: str
# Explicit census assertion: an empty list by itself is never proof of no work.
population_complete: bool
selected: tuple[RankedQuestion, ...]
deferred: tuple[DeferredQuestion, ...]
unresolved_boundaries: tuple[str, ...]
def __post_init__(self) -> None:
super().__post_init__()
identities = [item.identity for item in self.selected] + [item.identity for item in self.deferred]
if len(set(identities)) != len(identities):
raise ValueError("question identities must be unique across selected and deferred scope")
@dataclass(frozen=True)
class QuestionOutcome(Contract):
"""Execution, not vulnerability disposition; raw answers survive interrupted arbitration."""
identity: QuestionIdentity
status: Literal["completed", "unanswered", "unresolved", "not_arbitrated"]
reason: str
raw_rows_json: str | None = None
def __post_init__(self) -> None:
super().__post_init__()
if self.raw_rows_json is not None:
try:
rows = json.loads(self.raw_rows_json)
except (ValueError, TypeError) as error:
raise ValueError("raw_rows_json must contain a JSON row list") from error
if not isinstance(rows, list):
raise ValueError("raw_rows_json must contain a JSON row list")
if self.status in {"completed", "not_arbitrated"} and self.raw_rows_json is None:
raise ValueError("answered outcome requires raw rows")
if self.status == "unanswered" and self.raw_rows_json is not None:
raise ValueError("unanswered outcome cannot carry an answer")
@dataclass(frozen=True)
class FamilyCoverage(Contract):
"""Counts over the supplied population; completed never means safe or admitted."""
unit: str
language: str
family: str
selected: int
completed: int
unanswered: int
deferred: int
unsupported: int
def __post_init__(self) -> None:
super().__post_init__()
counts = (self.selected, self.completed, self.unanswered, self.deferred, self.unsupported)
if any(count < 0 for count in counts):
raise ValueError("coverage counts must be nonnegative")
if self.completed + self.unanswered != self.selected or self.unsupported > self.deferred:
raise ValueError("inconsistent family coverage counts")