DEV Community

Finley Zhou
Finley Zhou

Posted on

A Property Miss Cannot Open a Flake Freeze

A property miss cannot open a flake freeze. Neither can a rewritten fixture. The only legal silence is timing debt: a named check, a named owner, a short age, and a property lane that is still green.

That split is the strategy. The rest of this piece is the admission rule, a stdlib harness, and the constructed cases that must stay red.

Mixed reds are not one defect

Agent patches arrive with three failures glued into one red run. An invariant fails on an input the patch was not supposed to change. A golden file changes so the old invariant is no longer written down. A latency sample spikes because the runner was busy.

Those outcomes ask for different actions. A freeze is temporary silence on one check. Silence is defensible only after noise has been sampled. It is not a repair for a wrong answer, and it is not permission to edit the expected value until the diff looks quiet.

Latency checks make the glue stronger. A single slow call is a weak signal, which most performance write-ups already grant. The missing step is classification.

A stable overrun of a written budget is a contract miss. A wide spread across repeats of the same input is timing noise. One label for both hides the contract miss and keeps the noisy check frozen past the release that needed it.

Clear records matter more than a shorter test name. A freeze line that says only "flaky" is not reviewable. A line that names the class, the owner, the sample count, and the expiry can be rejected by a function instead of by memory.

Admission policy

Use the table as policy. The numeric bands in the harness are a local starting point for this workflow. They are not a published standard, and they are not measurements from a production suite.

Signal Class Freeze legal?
Functional mismatch on a declared property property no
Oracle fixture keys differ from the locked digest fixture no
Over budget, spread across samples is wide timing only if every other gate passes
Over budget, spread is tight property no
One sample, no repeat unknown no
Missing owner, or age outside 1–72 hours rejected request no

The 72-hour cap is a choice so a freeze cannot become a second backlog. Shorten it when the train is faster. Do not replace it with "until someone looks."

Workflow

Step 1 — Partition fixture keys before scoring

Split each fixture into oracle keys and volatile keys. Oracle keys are the fields a correct patch must preserve: status, schema version, and a hash of the semantic body. Volatile keys are transport noise: request id, observed timestamp, raw latency.

Lock a digest of the oracle subset only. An agent may refresh volatile fields when a client library changes its id format. It may not refresh oracle fields to paint a red property green.

If a fixture has no separable oracle keys, skip the partial digest. Hash the whole document, or refuse the patch until a reviewer names the keys. A partial digest on a fully semantic payload will hide drift.

Step 2 — Keep property checks on immutable inputs

Properties stay in the always-on lane. For a typical handler patch, three checks cover a wide slice of regressions. Status stays inside the documented set. The semantic body hash matches the locked oracle when the input was not supposed to change. A known-bad input is still rejected.

The patch may not rewrite those properties in the same unreviewed commit that changes the implementation. A real contract change is a separate review item with its own digest update. It is not a side effect of opening a freeze.

Step 3 — Sample timing only after the property lane is green

Collect at least five samples of the suspect check. Use the same input, the same commit, and a pinned runner image. Record minimum, maximum, and the budget.

Classify with a local rule. If the sample exceeds the budget and (max - min) is greater than 25% of the budget, label it timing. If it exceeds the budget and the spread is at or below that band, label it a stable contract miss.

Five samples do not estimate a distribution. They are an admission minimum so one spike cannot open a freeze. Raise the count when the check crosses a shared network.

Step 4 — Admit or reject with a stable code

A legal freeze needs every gate. The class must be timing. The property lane must be green. The oracle digest must be unchanged. An owner must be named. Age must sit between 1 and 72 hours. The sample count must be at least five.

Any miss returns a rejection code. The patch stays unmerged. Reviewers should see that code in the log, not a boolean buried in a summary line.

Step 5 — Thaw with a repeated sample

When the age elapses, run the same five-sample probe. One green pass does not clear the debt. The probe must sit under budget with a narrow spread, or the owner lands a real fix.

A freeze that expires into silence is a failed control. Delete the freeze record only after the probe passes, and keep the old rejection codes in the log.

Harness

The script is synthetic and stdlib-only. It does not call a model, and it does not hit a live service. Feed it check records you already store. Thresholds are policy knobs, not measured constants.

"""Admit a flake freeze only for timing debt.

Thresholds are a local policy, not a published standard.
"""

from __future__ import annotations

import hashlib
import json
from dataclasses import dataclass
from datetime import datetime, timedelta, timezone
from enum import Enum
from typing import Mapping


class Kind(str, Enum):
    PROPERTY = "property"
    FIXTURE = "fixture"
    TIMING = "timing"
    UNKNOWN = "unknown"


ORACLE_KEYS = ("status", "schema_version", "body_sha256")


@dataclass(frozen=True)
class Check:
    name: str
    passed: bool
    kind: Kind


@dataclass(frozen=True)
class FreezeRequest:
    test_name: str
    owner: str
    opened_at: datetime
    max_age_hours: int
    sample_runs: int
    spread_ms: float
    budget_ms: float


@dataclass(frozen=True)
class Admission:
    allowed: bool
    code: str
    detail: str


def oracle_digest(payload: Mapping) -> str:
    subset = {key: payload[key] for key in ORACLE_KEYS}
    raw = json.dumps(subset, sort_keys=True, separators=(",", ":")).encode()
    return hashlib.sha256(raw).hexdigest()


def classify_latency(latency_ms: float, budget_ms: float, spread_ms: float) -> Kind:
    if latency_ms <= budget_ms:
        return Kind.UNKNOWN
    if spread_ms > budget_ms * 0.25:
        return Kind.TIMING
    return Kind.PROPERTY


def thaw_allowed(samples_ms: list[float], budget_ms: float) -> bool:
    if len(samples_ms) < 5:
        return False
    if max(samples_ms) > budget_ms:
        return False
    spread = max(samples_ms) - min(samples_ms)
    return spread <= budget_ms * 0.25


def admit(
    results: list[Check],
    request: FreezeRequest,
    payload: Mapping,
    locked_digest: str,
    now: datetime,
) -> Admission:
    if request.sample_runs < 5:
        return Admission(False, "sample_too_small", "Need >= 5 samples.")
    if not 1 <= request.max_age_hours <= 72:
        return Admission(False, "age_out_of_band", "Age must be 1..72 hours.")
    if not request.owner.strip():
        return Admission(False, "missing_owner", "Owner is required.")
    if oracle_digest(payload) != locked_digest:
        return Admission(False, "fixture_oracle_drift", "Oracle keys moved.")
    red_props = [c.name for c in results if c.kind is Kind.PROPERTY and not c.passed]
    if red_props:
        return Admission(False, "property_red", "Red properties: " + ", ".join(red_props))
    red_fix = [c.name for c in results if c.kind is Kind.FIXTURE and not c.passed]
    if red_fix:
        return Admission(False, "fixture_red", "Red fixture checks: " + ", ".join(red_fix))
    if request.spread_ms <= request.budget_ms * 0.25:
        return Admission(False, "spread_too_tight", "Tight overrun is not timing debt.")
    target = [
        c
        for c in results
        if c.name == request.test_name and c.kind is Kind.TIMING and not c.passed
    ]
    if not target:
        return Admission(False, "not_timing", "Target is not a failing timing check.")
    expires = request.opened_at + timedelta(hours=request.max_age_hours)
    if now > expires:
        return Admission(False, "already_expired", "Request is past max age.")
    return Admission(True, "timing_debt_opened", expires.isoformat())


def main() -> None:
    payload = {
        "status": 200,
        "schema_version": 3,
        "body_sha256": "abc123",
        "request_id": "volatile",
        "latency_ms": 840,
    }
    locked = oracle_digest(payload)
    now = datetime(2026, 9, 24, 12, 0, tzinfo=timezone.utc)
    opened = now - timedelta(hours=1)
    results = [
        Check("status_in_set", True, Kind.PROPERTY),
        Check("known_bad_rejected", True, Kind.PROPERTY),
        Check("oracle_digest", True, Kind.FIXTURE),
        Check("list_latency", False, Kind.TIMING),
    ]
    request = FreezeRequest("list_latency", "suite-owner", opened, 24, 5, 300.0, 400.0)
    decision = admit(results, request, payload, locked, now)
    print(json.dumps({"code": decision.code, "allowed": decision.allowed, "detail": decision.detail}))


if __name__ == "__main__":
    main()
Enter fullscreen mode Exit fullscreen mode

Compute the oracle digest the same way CI will. The field body_sha256 here is a stored token, not a claim that the body was hashed in a live service.

python3 - <<'PY'
import hashlib, json
payload = {"status": 200, "schema_version": 3, "body_sha256": "abc123"}
raw = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
print(hashlib.sha256(raw).hexdigest())
PY
Enter fullscreen mode Exit fullscreen mode

Save the harness as freeze_admit.py and run it:

python3 freeze_admit.py
Enter fullscreen mode Exit fullscreen mode

For the synthetic main above, the printed line is:

{"code": "timing_debt_opened", "allowed": true, "detail": "2026-09-25T11:00:00+00:00"}
Enter fullscreen mode Exit fullscreen mode

opened_at in that script is 2026-09-24T11:00:00Z. The max age is 24 hours, so the detail field is the expiry on the next day. That is a clock example for the admission function. It is not a timed run against a service, and the date is only there so the comparison is reproducible.

Four constructed cases

These inputs exercise admit, classify_latency, and thaw_allowed. They are not production rates, and they should not be quoted as flake statistics.

Case A, property red. known_bad_rejected is false and its kind is property. list_latency can be red in the same list. The code is still property_red, because property failures are checked before the timing target. The freeze does not open.

Case B, oracle drift. Change body_sha256 and pass the previous digest as locked_digest. The code is fixture_oracle_drift. Changing only request_id does not trip that code, because request_id is outside ORACLE_KEYS.

That exclusion is intentional. It is also how a bad key partition hides a real edit. Put the key list in the same pull request as the fixture, and treat a quiet volatile set as a review item, not as a default.

Case C, stable overrun. Latency is 700 ms, budget is 400 ms, spread is 20 ms. classify_latency returns property, because 20 is not greater than 100. If a reporter still files a freeze with that spread, admit returns spread_too_tight. A tight overrun is a contract miss. If the patch added a retry loop, the extra time is the behavior under review, not runner noise.

Case D, wide spread. Latency is 840 ms, budget is 400 ms, spread is 300 ms, five samples, properties green, digest matched, owner set, age 24 hours. The band is 100 ms, so the spread clears it. The code is timing_debt_opened. Silence applies to list_latency only, and only until the recorded expiry.

Thaw is a separate predicate. The sample list [410, 390, 420, 400, 405] against a 400 ms budget fails, because 410 and 420 are over budget. [310, 320, 300, 330, 315] passes: the max is under budget and the spread is 30 ms, inside the 100 ms band. A single 200 ms sample does not pass. The length check rejects fewer than five points.

Draft lane versus score lane

A model can propose the oracle-key list. It must not be the writer of the merge bit.

Disclosure: This article was prepared as part of MonkeyCode's product outreach.

MonkeyCode's free model access fits as a draft lane. Ask it for a candidate split of fixture fields, then drop any key it cannot justify from the schema or from a fixture comment. The free server option fits as a workspace for that draft while a reviewer edits the partition.

This article assumes neither a model name, nor a quota, nor a machine shape, nor a duration, nor a permanent free tier. Check the current terms before you depend on either option. Those terms are not arguments to admit.

The authoritative call stays on the CI runner that holds the locked digest and the sample log. A host that drafted the patch, or that only stored the key proposal, does not get to mark the freeze legal. If the free server is what you use for drafts, keep it on the draft side of that line.

Proposal text is input. Admission codes are output. Do not collapse them onto one machine and call the result a score.

Limitations

The 25% band will mislabel some heavy-tailed checks. A call can be slow and unstable because the patch retries on a 500, not because the runner blipped. Read the diff before you trust a timing label. The band is a gate, not a model of latency.

Five samples block casual freezes. They do not estimate a flake rate, and they will miss a failure that shows up once in a hundred runs. If you need a rate, collect it outside this gate. Do not pretend the gate produced it.

Partial digests hide mistakes when an oracle field was left in the volatile set. Review ORACLE_KEYS beside the fixture. A wrong partition is a bad lock, even if admit returns a calm code.

The harness does not execute tests. It classifies records you pass in. admit re-checks spread against budget, but it still trusts sample_runs and the kind flag on other checks. If an upstream reporter marks a functional mismatch as timing and also invents a wide spread, the function will not reconstruct the truth. Put classify_latency in the reporter, and keep functional mismatches on the property enum by construction.

Pass timezone-aware datetimes. A naive local clock makes expiry comparisons disagree across runners. The example uses UTC on purpose.

Who should skip it

Skip the workflow if you cannot name an owner who is available before the freeze expires. Skip it if you have no CI runner separate from the draft environment. Skip it for checks where silence is not an allowed state, including safety interlocks and migration gates.

Skip partial digests when every field is semantic. A payload with no volatile keys should be hashed whole. Skip the latency split if you have not written a budget. Without a budget, every slow run looks like a freeze candidate, and the classifier has nothing stable to compare.

Do not adopt the gate as a way to land agent patches faster. admit is biased toward rejection. A patch that needs a property freeze is not ready. A patch that needs a fixture rewrite to go green is not ready either.

Before the codes go live

Confirm that the draft workspace cannot write the digest store or the sample log. Then require admit on the pull request, and print the rejection code in the log reviewers already open.

If those codes are invisible, the record is decoration. Keep the function small enough that a reviewer can re-run cases A through D before approving the key partition. A freeze that cannot be rejected in one sitting will not be rejected under a release clock.

Top comments (0)