feat(pre-push-safety-net): establish shared push and engine contracts

This commit is contained in:
Marius Ciepluch
2026-09-12 19:21:31 +02:00
parent 051f3b0058
commit 14a2a60e52
18 changed files with 810 additions and 25 deletions
+33 -1
View File
@@ -5,6 +5,9 @@ import os
import tomllib
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Literal
from openultrasast.contracts import Contract
DEFAULT_VECTOR_STORE = "json-local"
@@ -163,6 +166,28 @@ class ObligationsConfig:
policy_path: str = ".openultrasast/obligations.toml"
@dataclass(frozen=True)
class PushConfig(Contract):
"""Inert experimental push settings; configuring them does not install a hook.
The 2 GiB local cache cap is a storage budget, not a performance promise. The
eventual cache evicts unleased entries; deadline and cleanup are separate costs.
No endpoint, fetch, engine download or capability admission is enabled here.
"""
comparison_base: str | None = None
deadline_seconds: float = 30.0
cancellation_allowance_seconds: float = 2.0
cache_max_bytes: int = 2 * 1024 ** 3
mode: Literal["advisory", "blocking"] = "advisory"
incomplete_coverage_policy: Literal["allow", "block"] = "allow"
def __post_init__(self) -> None:
super().__post_init__()
if self.deadline_seconds <= 0 or self.cancellation_allowance_seconds <= 0 or self.cache_max_bytes <= 0:
raise ValueError("push deadlines, cancellation allowance and cache limit must be positive")
@dataclass(frozen=True)
class ResolvedConfig:
models: ModelConfig = ModelConfig()
@@ -181,6 +206,7 @@ class ResolvedConfig:
variants: VariantsConfig = VariantsConfig()
obligations: ObligationsConfig = ObligationsConfig()
model: ModelLayerConfig = ModelLayerConfig()
push: PushConfig = PushConfig()
runs_dir: str = ".openultrasast/runs"
@@ -208,13 +234,19 @@ def load_config(config_path: Path | None = None) -> ResolvedConfig:
variants=_load_variants(data.get("variants", {})),
obligations=_load_obligations(data.get("obligations", {})),
model=_load_model(data.get("model", {})),
push=PushConfig.from_payload(data.get("push", {})),
runs_dir=os.environ.get("OPENULTRASAST_RUNS_DIR", ".openultrasast/runs"),
)
def config_payload(config: ResolvedConfig) -> dict[str, object]:
"""Reproducible resolved settings, including the separate experimental push policy."""
return asdict(config)
def write_resolved_config(config: ResolvedConfig, path: Path) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text(json.dumps(asdict(config), indent=2, sort_keys=True) + "\n")
path.write_text(json.dumps(config_payload(config), indent=2, sort_keys=True) + "\n")
def _section(value: object) -> dict[str, object]:
+92
View File
@@ -0,0 +1,92 @@
"""Strict immutable manifest values shared by engine and adapters (pre-push Req 7.3).
These schemas validate structure, not source readability, Git object existence or alert
eligibility. Live resources deliberately have no manifest decoder.
"""
from __future__ import annotations
import math
from dataclasses import fields
from types import UnionType
from typing import Any, Literal, Self, get_args, get_origin, get_type_hints
_HINTS: dict[type, dict[str, Any]] = {}
def _hints(cls: type) -> dict[str, Any]:
if cls not in _HINTS:
_HINTS[cls] = get_type_hints(cls)
return _HINTS[cls]
def _value(value: object, kind: Any, *, decode: bool) -> object:
origin, args = get_origin(kind), get_args(kind)
if origin is UnionType:
for candidate in args:
try:
return _value(value, candidate, decode=decode)
except ValueError:
pass
raise ValueError(f"invalid union value: {value!r}")
if origin is Literal:
if any(type(value) is type(option) and value == option for option in args):
return value
elif origin is tuple:
if type(value) is tuple or (decode and type(value) is list):
items = tuple(value) # type: ignore[arg-type]
if len(args) == 2 and args[1] is Ellipsis:
return tuple(_value(item, args[0], decode=decode) for item in items)
if len(items) == len(args):
return tuple(_value(item, item_type, decode=decode) for item, item_type in zip(items, args, strict=True))
elif isinstance(kind, type) and issubclass(kind, Contract):
if decode:
return kind.from_payload(value)
if isinstance(value, kind):
return value
elif kind is float:
if type(value) in (int, float):
try:
if math.isfinite(value): # type: ignore[arg-type]
return value
except OverflowError:
pass
elif kind is str:
if type(value) is str and value.strip():
return value
elif type(value) is kind:
return value
raise ValueError(f"invalid {kind}: {value!r}")
def _payload(value: object) -> object:
if isinstance(value, Contract):
return value.to_payload()
if isinstance(value, tuple):
return [_payload(item) for item in value]
return value
class Contract:
"""Base for frozen dataclasses with strict construction and JSON round trips."""
def __post_init__(self) -> None:
for name, kind in _hints(type(self)).items():
try:
_value(getattr(self, name), kind, decode=False)
except ValueError as error:
raise ValueError(f"{type(self).__name__}.{name}: {error}") from error
def to_payload(self) -> dict[str, object]:
return {item.name: _payload(getattr(self, item.name)) for item in fields(self)} # type: ignore[arg-type]
@classmethod
def from_payload(cls, payload: object) -> Self:
if not isinstance(payload, dict) or not all(isinstance(key, str) for key in payload):
raise ValueError(f"{cls.__name__} requires an object")
hints = _hints(cls)
if unknown := payload.keys() - hints.keys():
raise ValueError(f"unknown {cls.__name__} fields: {sorted(unknown)}")
try:
return cls(**{name: _value(value, hints[name], decode=True) for name, value in payload.items()})
except TypeError as error:
raise ValueError(f"invalid {cls.__name__}: {error}") from error
+82
View File
@@ -0,0 +1,82 @@
"""Backend-owned graph provenance and live resource boundary (pre-push Req 4.3, 7.3).
Structural validity is necessary but insufficient for reuse. Task 6.1 will validate
actual source bytes, graph digest/census and engine compatibility before issuing leases.
"""
from __future__ import annotations
from dataclasses import dataclass
from pathlib import Path
from typing import Literal, Protocol
from openultrasast.contracts import Contract
@dataclass(frozen=True)
class GraphIdentity(Contract):
source_digest: str
declarations_digest: str
exclusions_digest: str
unit: str
language: str
engine_version: str
frontend: str
frontend_version: str
overlay_semantics: tuple[str, ...]
overlay_options: tuple[tuple[str, str], ...]
frontend_options: tuple[tuple[str, str], ...] = ()
def __post_init__(self) -> None:
super().__post_init__()
for options in (self.overlay_options, self.frontend_options):
if len({key for key, _ in options}) != len(options):
raise ValueError("graph option names must be unique")
@dataclass(frozen=True)
class GraphCensus(Contract):
files: int
methods: int
calls: int
def __post_init__(self) -> None:
super().__post_init__()
if min(self.files, self.methods, self.calls) < 0:
raise ValueError("graph census counts must be nonnegative")
@dataclass(frozen=True)
class GraphArtifact(Contract):
identity: GraphIdentity
graph_digest: str
source_bytes: int
census: GraphCensus
completeness: Literal["complete", "partial"]
diagnostics: tuple[str, ...]
def __post_init__(self) -> None:
super().__post_init__()
if self.source_bytes < 0:
raise ValueError("source byte count must be nonnegative")
if self.completeness == "complete" and (not self.source_bytes or not self.census.files or self.diagnostics):
raise ValueError("complete graph requires readable source, a file census and no incomplete diagnostics")
if self.completeness == "partial" and not self.diagnostics:
raise ValueError("partial graph requires a recorded reason")
class GraphLease(Protocol):
"""A backend-issued live lease; never reconstructed from a JSON manifest.
The backend validates the artifact before issuing the lease and isolates loading
when it may mutate graph bytes. The consumer owns releasing it exactly once; the
backend's release is idempotent and bounded by the supplied monotonic deadline.
A cached artifact survives release. Implementations belong to task 6.1.
"""
@property
def artifact(self) -> GraphArtifact: ...
@property
def path(self) -> Path: ...
def close(self, *, deadline_monotonic: float) -> None: ...
+5
View File
@@ -28,6 +28,11 @@ ROOTS = ("gate", "map_gate", "pair_gate", "cli", "__main__")
# Modules that stand alone: their own CLI subcommand or external protocol, deliberately off the scan path.
SPEC_OWNED = {
"contracts": "pre-push-safety-net Req 7.3: strict shared manifest serialization, independent of push",
"model.contracts": "pre-push-safety-net Req 4.1/7.3: generic deadline, change and exact scope contracts; integration pending",
"cpg.artifact": "pre-push-safety-net Req 4.3/7.3: backend graph identity and live lease contract; validation integration pending",
"push": "pre-push-safety-net Req 5.5/7.3: experimental push contract package; no hook installed",
"push.contracts": "pre-push-safety-net Req 5.5/7.3: immutable comparisons and independent result statuses; runner pending",
"model.specs": "model-grounded-detection Req 7.2-7.4: the security vocabularies the CPG queries are parameterised by",
"model.endpoint": "model-grounded-detection Req 8: the judge client, wired by model/judge.py in group 3",
"model.audit": "model-grounded-detection Req 3: this classifier; run by the maintainer, not by the scan",
+115
View File
@@ -0,0 +1,115 @@
"""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
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 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, ...]
@dataclass(frozen=True)
class RankedQuestion(Contract):
identity: QuestionIdentity
priority: float
evidence: tuple[str, ...]
@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")
+1
View File
@@ -0,0 +1 @@
"""Experimental pre-push contracts; no hook or admitted capability is enabled here."""
+86
View File
@@ -0,0 +1,86 @@
"""Push-owned immutable identities and independent result statuses (Req 5.5, 7.3).
Git resolution, admission policy and execution are separate later implementations.
No default result status can turn an unexecuted contract into a complete negative.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import Literal
from openultrasast.contracts import Contract
from openultrasast.model.contracts import QuestionIdentity, ScopeDecision
@dataclass(frozen=True)
class PushUpdate(Contract):
local_ref: str
local_oid: str
remote_ref: str
remote_oid: str
@dataclass(frozen=True)
class PushComparison(Contract):
head_oid: str
base_oid: str | None
base_reason: str
refs: tuple[str, ...]
def __post_init__(self) -> None:
super().__post_init__()
if not self.refs or len(set(self.refs)) != len(self.refs):
raise ValueError("comparison must retain distinct associated refs")
@dataclass(frozen=True)
class ComparisonAnalysis(Contract):
"""Scope and completion belong to this exact base/head comparison.
A question completed for one comparison never completes it for another base,
even when the target graph is shared. This record does not assert admission.
"""
comparison: PushComparison
scopes: tuple[ScopeDecision, ...]
completed_questions: tuple[QuestionIdentity, ...]
coverage_status: Literal["complete_within_scope", "incomplete", "unavailable"]
def __post_init__(self) -> None:
super().__post_init__()
selected_list = [item.identity for scope in self.scopes for item in scope.selected]
selected = set(selected_list)
completed = set(self.completed_questions)
if len(selected) != len(selected_list):
raise ValueError("a comparison cannot select the same question across multiple scopes")
if len(completed) != len(self.completed_questions) or not completed <= selected:
raise ValueError("completed questions must be unique members of this comparison's selected scope")
if self.coverage_status == "complete_within_scope":
if self.comparison.base_oid is None or not self.scopes:
raise ValueError("complete coverage requires compared revisions and an explicit scope census")
if completed != selected or any(
not scope.population_complete or scope.deferred or scope.unresolved_boundaries for scope in self.scopes
):
raise ValueError("deferred, unexecuted or unresolved work cannot be complete coverage")
@dataclass(frozen=True)
class PushResult(Contract):
analyses: tuple[ComparisonAnalysis, ...]
finding_status: Literal["actionable", "none"]
coverage_status: Literal["complete_within_scope", "incomplete", "unavailable", "not_applicable"]
push_disposition: Literal["allow", "block"]
actionable_defect_ids: tuple[str, ...]
def __post_init__(self) -> None:
super().__post_init__()
if (self.finding_status == "actionable") != bool(self.actionable_defect_ids):
raise ValueError("finding status must match admitted defect identities")
if len(set(self.actionable_defect_ids)) != len(self.actionable_defect_ids):
raise ValueError("admitted defect identities must be unique")
if self.coverage_status == "complete_within_scope" and (
not self.analyses or any(item.coverage_status != "complete_within_scope" for item in self.analyses)
):
raise ValueError("aggregate complete coverage requires completed comparison analyses")
if self.coverage_status == "not_applicable" and (self.analyses or self.actionable_defect_ids):
raise ValueError("not applicable cannot carry analyzed scope or findings")