Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/pipeline/gates.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,11 +225,44 @@ def check(self, recipe: RecipeSpec, ctx: GateContext) -> GateResult:
return GateResult("regression", True, "at parity with registry")




class BudgetGate(BaseGate):
"""C3: declared performance budgets vs actual index values.

The index entry may declare perf budgets (size_mb today; latency
budgets ride the fixture decode in CI). A declared budget that the
actual artifact violates blocks the gate — regressions are release
failures, not notes."""

name = "budgets"

def check(self, recipe: RecipeSpec, ctx: GateContext) -> GateResult:
entry = (ctx.extra or {}).get("index_entry") or {}
perf = entry.get("perf") or {}
size_mb = perf.get("size_mb")
if size_mb is None:
return GateResult(self.name, True, "no perf budgets declared")
size = entry.get("size")
if size is None:
return GateResult(self.name, False,
"perf.size_mb declared but index has no size")
actual_mb = size / 1_000_000
if actual_mb > float(size_mb):
return GateResult(self.name, False,
f"artifact {actual_mb:.1f} MB exceeds the "
f"declared {float(size_mb):.1f} MB budget")
return GateResult(self.name, True,
f"{actual_mb:.1f} MB within the "
f"{float(size_mb):.1f} MB budget")


register_gate(QualityGate())
register_gate(ParityGate())
register_gate(LicenseGate())
register_gate(ConformanceGate())
register_gate(RegressionGate())
register_gate(BudgetGate())


def evaluate(
Expand Down
1 change: 1 addition & 0 deletions tests/test_pipeline_plan_gate.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ def test_gate_results_files_end_to_end(tmp_path: Path) -> None:
"license": True,
"conformance": False,
"regression": True,
"budgets": True,
}


Expand Down
17 changes: 17 additions & 0 deletions tests/test_pipeline_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -158,3 +158,20 @@ def test_license_policy_is_encapsulated() -> None:
})
strict = type(DEFAULT_LICENSE_POLICY)(allow_waivers=False)
assert strict.blocked(restricted), "policies vary without core edits"


def test_budget_gate_enforces_declared_size() -> None:
from pipeline.gates import BudgetGate, GateContext

gate = BudgetGate()
over = gate.check(None, GateContext(extra={"index_entry": {
"size": 100_000_000, "perf": {"size_mb": 50}}}))
assert not over.passed

ok = gate.check(None, GateContext(extra={"index_entry": {
"size": 40_000_000, "perf": {"size_mb": 50}}}))
assert ok.passed

no_budget = gate.check(None, GateContext(extra={"index_entry": {
"size": 1}}))
assert no_budget.passed # nothing declared -> nothing to enforce
Loading