One Fail Signature, Two Runners: The Flake Freeze Citation Rule
A flake freeze may be cited only when a local gate holds two witnesses that share a fixture pin, a property digest, and a bound, carry opposite verdicts, and show one stable fail signature. A remote replay is allowed to
A flake freeze may be cited only when a local gate holds two witnesses that share a fixture pin, a property digest, and a bound, carry opposite verdicts, and show one stable fail signature. A remote replay is allowed to append witnesses. It is not allowed to mint the freeze.
A boolean job status cannot support that rule. The bit drops the pin, the digest, the signature, and the runner role.
Two red builds can still be different bugs. Two opposite builds can be the same bug flickering, or a fixture the patch rewrote. The ledger keeps those fields apart, and the decision function admits nothing else.
Fields the gate reads
Store one JSON object per witness. Do not store a prose explanation in the row the gate reads. If a reviewer needs prose, keep it outside the digest inputs.
| Field | Decision use |
|---|---|
| property_id | Groups rows that claim the same behavior |
| property_digest | SHA-256 of checked-in spec bytes, never of a model draft |
| fixture_pin | SHA-256 of a schema prefix plus fixture bytes |
| bound | Canonical bound text; a changed bound is not admitted here |
| seed | Replay coordinate; recorded, not a freeze key by itself |
| verdict |
pass or fail
|
| signature | Exception class or assertion id; fails must share one value |
| runner |
local or remote
|
| freeze_writer | Must be false on every remote row |
A citation, when admitted, names one fail id and one pass id. It does not name a CI job. Expiry is a timestamp the caller supplies from policy. This note does not pick a duration, and it does not refresh an old citation.
Outcomes before any freeze file is written
Read the filter from top to bottom. The first matching reject wins. Do not average the rows into a score.
| Scoped inputs | Result |
|---|---|
Any remote row has freeze_writer true |
Reject the batch |
| Malformed runner or verdict | Reject the batch |
| Fewer than two rows share id, digest, pin, and bound | Reject |
| Shared keys, but only pass or only fail | Reject |
| Shared keys, fail signatures differ | Reject |
| Shared keys, opposite verdicts, only one runner role | Reject |
| Shared keys, both roles, opposite verdicts, one fail signature | Admit and cite ids |
The admit row is narrow on purpose. Every other row is a closed patch, a spec problem, or a collector bug. None of those are freezes.
1. Pin the fixture on the minting machine
Compute the pin before a remote job starts. The collector should receive the pin as an expected value, not as a value it invents.
python - <<'PY'
import hashlib
import pathlib
raw = pathlib.Path('tests/fixtures/order.json').read_bytes()
print(hashlib.sha256(b'fixture-v1\n' + raw).hexdigest())
PY
The fixture path above is illustrative. Use the file your property actually loads.
A pin mismatch stops the pipeline. It is not evidence of a flake. If an agent patch edits fixture bytes, the pin changes, and this gate will not cite a freeze. That patch belongs in review.
2. Digest the checked-in property file
The spec that counts is the file a reviewer can open in the repository. Hash those bytes. A model draft is a side file until someone commits it.
python - <<'PY'
import hashlib
import pathlib
spec = pathlib.Path('props/order_total.md').read_bytes()
print(hashlib.sha256(spec).hexdigest())
PY
Compare a draft digest to the spec digest outside the admit function. If they differ, quarantine the draft. Do not pass the draft hash in as expected_digest.
Acceptance is a commit that changes the spec file. It is not a log line from the collector.
3. Append local and remote rows with the same schema
Both runners write the same fields. Only the local caller may treat an admit result as authority.
A remote row that sets freeze_writer to true fails the batch even when every other field matches. Do not drop the bad row and continue. One authority violation is enough to discard that batch.
{"witness_id":"w-local-1","property_id":"order_total_nonneg","property_digest":"<spec-sha256>","fixture_pin":"<pin-sha256>","bound":"total>=0","seed":17,"verdict":"fail","signature":"AssertionError","runner":"local","freeze_writer":false}
{"witness_id":"w-remote-9","property_id":"order_total_nonneg","property_digest":"<spec-sha256>","fixture_pin":"<pin-sha256>","bound":"total>=0","seed":17,"verdict":"pass","signature":"ok","runner":"remote","freeze_writer":false}
Keep the seed equal while you test this citation rule. A different seed can matter for other investigations. It neither blocks nor justifies admission in the function below.
4. Require one fail signature across opposite verdicts
Call the reference function only after the pin and digest checks have passed. Admission then requires four predicates.
- No remote witness tried to mint.
- At least one local row and one remote row share property id, digest, pin, and bound.
- That scope contains both a pass and a fail.
- Every fail in scope carries the same signature, and that signature is not
ok.
A change from AssertionError to KeyError is a different failure mode. It is not flicker. Close the patch and inspect the fixture or the spec.
The reason string should appear in the CI log. The next reader should not have to reconstruct which predicate failed.
5. Let the local caller write the citation
The function returns identifiers. It does not open a socket, write a freeze store, or extend an older citation.
The local job may write one citation that names fail_id and pass_id, plus an expiry taken from policy. A later green run is not an argument to this function. There is no refill input, because refill would be a different policy reviewed on its own.
Reference function
The module below is a self-contained proposal. It uses the standard library only. It is not a vendor client.
The listing is unexecuted in this draft. The assertions that follow are expected results of the function, not measurements from a shared runner.
from __future__ import annotations
from dataclasses import dataclass
from typing import Optional
@dataclass(frozen=True)
class Witness:
witness_id: str
property_id: str
property_digest: str
fixture_pin: str
bound: str
seed: int
verdict: str
signature: str
runner: str
freeze_writer: bool
@dataclass(frozen=True)
class FreezeDecision:
admit: bool
reason: str
fail_id: Optional[str] = None
pass_id: Optional[str] = None
def decide_freeze(witnesses, *, expected_digest, expected_pin, expected_bound, property_id):
for item in witnesses:
if item.runner == 'remote' and item.freeze_writer:
return FreezeDecision(False, f'remote witness {item.witness_id} attempted to mint')
if item.runner not in {'local', 'remote'} or item.verdict not in {'pass', 'fail'}:
return FreezeDecision(False, f'malformed witness {item.witness_id}')
scoped = [
w for w in witnesses
if w.property_id == property_id
and w.property_digest == expected_digest
and w.fixture_pin == expected_pin
and w.bound == expected_bound
]
if len(scoped) < 2:
return FreezeDecision(False, 'need two witnesses on the same pin, digest, and bound')
fails = [w for w in scoped if w.verdict == 'fail']
passes = [w for w in scoped if w.verdict == 'pass']
if not fails or not passes:
return FreezeDecision(False, 'opposite verdicts required')
if len({w.signature for w in fails}) != 1:
return FreezeDecision(False, 'fail signature is not stable')
if not any(w.runner == 'local' for w in scoped):
return FreezeDecision(False, 'need both a local witness and a remote witness')
if not any(w.runner == 'remote' for w in scoped):
return FreezeDecision(False, 'need both a local witness and a remote witness')
if fails[0].signature == 'ok':
return FreezeDecision(False, 'fail witness cannot carry an ok signature')
return FreezeDecision(
True,
'stable signature across opposite verdicts',
fails[0].witness_id,
passes[0].witness_id,
)
Save it as witness_gate.py. Then run the three checks. Python 3.9 or newer is enough. No third-party packages are required.
python - <<'PY'
from witness_gate import Witness, decide_freeze
common = dict(
property_id='order_total_nonneg',
property_digest='d' * 64,
fixture_pin='p' * 64,
bound='total>=0',
seed=17,
freeze_writer=False,
)
ok_rows = [
Witness('w-local-1', verdict='fail', signature='AssertionError', runner='local', **common),
Witness('w-remote-9', verdict='pass', signature='ok', runner='remote', **common),
]
decision = decide_freeze(
ok_rows,
expected_digest='d' * 64,
expected_pin='p' * 64,
expected_bound='total>=0',
property_id='order_total_nonneg',
)
assert decision.admit and decision.fail_id == 'w-local-1'
bad_fields = dict(common)
bad_fields['freeze_writer'] = True
mint_attempt = [
ok_rows[0],
Witness('w-remote-bad', verdict='pass', signature='ok', runner='remote', **bad_fields),
]
denied = decide_freeze(
mint_attempt,
expected_digest='d' * 64,
expected_pin='p' * 64,
expected_bound='total>=0',
property_id='order_total_nonneg',
)
assert not denied.admit
split_sig = ok_rows + [
Witness('w-local-2', verdict='fail', signature='KeyError', runner='local', **common),
]
unstable = decide_freeze(
split_sig,
expected_digest='d' * 64,
expected_pin='p' * 64,
expected_bound='total>=0',
property_id='order_total_nonneg',
)
assert not unstable.admit
print('citation rule checks passed')
PY
If an assertion fires, the admit path or one of the two reject paths has drifted. Fix the function before wiring it to a job.
How to act on each reason
Use the reason string as the next step, not as a score.
-
attempted to mintmeans the remote job configuration is wrong. Remove write access. Do not cite a freeze from that batch. -
malformed witnessmeans the collector schema drifted. Stop the job and fix the writer. -
need two witnessesmeans the pin, digest, or bound did not line up. Recompute both hashes locally. -
opposite verdicts requiredmeans you have a consistent pass or a consistent fail. A consistent fail is a defect until a passing witness on the same keys appears. -
fail signature is not stablemeans at least two failure modes share the pin. Investigate before any freeze discussion. -
need both a local witness and a remote witnessmeans evidence is single-sourced. Do not invent the missing role. -
stable signature across opposite verdictsis the only reason that permits a citation, and only the local caller may record it.
Where a free model and a free server fit
MonkeyCode's free model access can draft a candidate property, and its free server option can run the remote replay that appends runner=remote rows. Disclosure: This article was prepared as part of MonkeyCode's product outreach. Those two options cover draft text and remote collection only.
The draft stays quarantined until the spec file changes in git. The remote row must keep freeze_writer false. Neither option is an input to the admit predicate.
If the free model is down, skip drafts and keep the checked-in spec. If the free server is down, do not let the local runner impersonate both roles. Missing remote evidence means no citation.
That default is stricter than a single-runner freeze. It is also the behavior this gate is for. Quotas, hardware, and retention are outside this note, so do not encode guesses about them into the function.
When the pin script and the spec file are already in the tree, the split is small enough to trial: draft offline, collect remotely, cite locally. The decision function stays the same if you later swap the collector.
Limitations
Signature equality is coarse. Two AssertionError rows can hide different messages. If message identity matters, hash a normalized message into signature before the call. This note does not define that normalizer.
The pair rule does not require equal seeds. It will cite a pass and a fail that used different seeds when the other keys match. If the seed is your noise source, add that predicate in a separate review. Do not fold it into this function without a new table.
A valid citation is not a safety proof. The fixture can be too narrow. Inputs outside the pin can still fail the property. Broader generators and input shrinking stay separate work. This gate only decides whether a freeze citation is allowed.
Expiry remains a policy input. A citation with no expiry field should be rejected by the writer even if decide_freeze returned admit. The function shown here does not check the clock.
Who should not use this
Skip the gate when failures have no stable signature, including timeouts that record an empty error string. Skip it when the only property text lives in a prompt. Skip it when local and remote jobs share a writable freeze store and you cannot force freeze_writer false on the remote path.
In that last layout the ledger does not constrain authority. Collecting rows would only add noise.
Also skip it for a fail you reproduced twice on the same pin, with the same signature, and with no passing witness. That case needs a code or spec change. The function already refuses it, because a pass is absent. Leave the refusal in place.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.