Skip to content

Commit 4cd779d

Browse files
myia-ai-01jsboigeclaude
authored
fix(harness,#17359): un diff vide refute READY dans le gate de prevalidation (#17360)
Deux PRs ouvertes (#16975, #16976) portaient un dossier [ADJOINT PREFLIGHT] INTEGRE declarant `diff-files: 0` avec `verdict: READY`. Le gate rendait `rc=0` et autorisait leur merge : un squash de diff vide aurait ferme le grain en n'ayant rien livre (G.3). Seule une reserve morphologique sans rapport, tenue par B.0, a empeche le merge — par accident. La jambe s'ajoute au bloc `if ready_claimed:` existant, dont le commentaire pose deja le principe : un diff vide est une raison pour laquelle une PR n'est PAS mergeable, donc il refute READY et laisse BLOCKED intact. Un dossier affirmant READY sur un diff vide est auto-contradictoire, ce qui est exactement le sens de « no dossier worth trusting » — d'ou le chemin rc=1 EXISTANT, sans nouveau code de sortie ni nouveau workflow. Ce n'est pas un garde de volume. `check_trivial_diff.py` possede cette classe et laisse deliberement passer le fix de 2 lignes d'un bug critique (contre-exemple du mandat #15740) ; sa jambe `genre_meta` ne peut pas voir un `fix(lean,...)`. Le vide n'est pas une petitesse, c'est une absence. Controles mesures sur instances reelles : #16975 gate main rc=0 -> patche rc=1 « READY requires a non-empty diff » #16976 gate main rc=0 -> patche rc=1 #16956 gate main rc=0 -> patche rc=0 <- controle NEGATIF : meme campagne #16638, meme auteur, meme genre, diff 38/38 — la jambe lit le diff, pas la campagne #17223 rc=1 sur les deux (inchange) 54/54 tests passent, dont 4 neufs : controle positif sur l'instance mesuree, tolerance BLOCKED, non-regression du fix de 2 lignes, et donnee absente qui tombe en UNKNOWN (rc=2) plutot qu'en faux `empty`. See #17359 Co-authored-by: jsboige <jsboige@gmail.com> Co-authored-by: Claude-Code <noreply@anthropic.com>
1 parent 472df43 commit 4cd779d

2 files changed

Lines changed: 96 additions & 0 deletions

File tree

‎scripts/check_adjoint_prevalidation.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -680,6 +680,20 @@ def validate_dossier(dossier: Dossier, snapshot: dict[str, Any]) -> list[str]:
680680
errors.append("draft pull request cannot be READY")
681681
if integers.get("threads-unresolved") not in {None, 0}:
682682
errors.append("READY requires zero unresolved threads")
683+
# A pull request that changes zero files has nothing to squash, whatever
684+
# its genre, domain or author. This is not a judgement on smallness --
685+
# `check_trivial_diff.py` owns that, and deliberately lets a two-line
686+
# critical fix through (#15740). It is the absence of a deliverable.
687+
# Measured on #16975/#16976 (2026-09-22): both carried an INTACT dossier
688+
# declaring `diff-files: 0` and `verdict: READY`, so the gate returned 0
689+
# and authorised a merge that would have closed a grain having delivered
690+
# nothing (G.3). Only B.0, holding an unrelated morphological reserve,
691+
# happened to stop it. A dossier asserting READY over an empty diff is
692+
# self-contradictory, which is exactly what "no dossier worth trusting"
693+
# means -- hence the existing rc=1 path, not a new one. A BLOCKED dossier
694+
# over an empty diff stays intact: it attests, correctly, non-mergeability.
695+
if snapshot.get("changedFiles") == 0:
696+
errors.append("READY requires a non-empty diff: 0 files changed")
683697
return errors
684698

685699

‎scripts/tests/test_check_adjoint_prevalidation.py‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
import sys
66
from pathlib import Path
77

8+
import pytest
9+
810
HERE = Path(__file__).resolve().parent
911
CHECK_PATH = HERE.parent / "check_adjoint_prevalidation.py"
1012
spec = importlib.util.spec_from_file_location("check_adjoint_prevalidation", CHECK_PATH)
@@ -350,6 +352,86 @@ def test_unresolved_thread_cannot_be_ready():
350352
assert "READY requires zero unresolved threads" in errors
351353

352354

355+
def test_empty_diff_cannot_be_ready():
356+
"""A PR changing zero files has nothing to squash -- READY is refuted.
357+
358+
Positive control, taken from the measured instance: #16975 and #16976 each
359+
carried an INTACT dossier declaring `diff-files: 0` with `verdict: READY`,
360+
so the gate returned 0 and authorised merging a pull request that delivered
361+
nothing. The dossier is self-consistent with the live PR -- every count
362+
matches -- which is why no staleness check could catch it.
363+
"""
364+
live = _base_snapshot()
365+
live["changedFiles"] = 0
366+
live["additions"] = 0
367+
live["deletions"] = 0
368+
dossier = _body(
369+
**{"diff-files": "0", "diff-additions": "0", "diff-deletions": "0"}
370+
)
371+
live["comments"].append(_comment(dossier))
372+
verdict, errors = mod.evaluate(live)
373+
assert "READY requires a non-empty diff: 0 files changed" in errors
374+
assert verdict == "", errors
375+
376+
377+
def test_blocked_dossier_tolerates_an_empty_diff():
378+
"""An empty diff refutes READY, never BLOCKED.
379+
380+
Same asymmetry as the draft and unresolved-thread legs: an empty diff is a
381+
reason a PR is NOT mergeable, and attesting it is precisely a BLOCKED
382+
dossier's job. Refusing it there would deny the coordinator the attested
383+
motive it dispatches from.
384+
"""
385+
live = _base_snapshot()
386+
live["changedFiles"] = 0
387+
live["additions"] = 0
388+
live["deletions"] = 0
389+
dossier = _body(
390+
verdict="BLOCKED", b0="blocked", checks="BLOCKED", scope="fail",
391+
domain="fail",
392+
**{"diff-files": "0", "diff-additions": "0", "diff-deletions": "0"},
393+
)
394+
live["comments"].append(_comment(dossier))
395+
verdict, errors = mod.evaluate(live)
396+
assert verdict == mod.VERDICT_BLOCKED, errors
397+
398+
399+
def test_two_line_fix_is_still_ready():
400+
"""Negative control: the #15740 counter-example must keep passing.
401+
402+
« une correction de 2 lignes d'un bug critique serait acceptable » -- the
403+
new leg measures absence, not smallness. A one-file, two-line diff is as
404+
READY as a large one.
405+
"""
406+
live = _base_snapshot()
407+
live["changedFiles"] = 1
408+
live["additions"] = 1
409+
live["deletions"] = 1
410+
dossier = _body(
411+
**{"diff-files": "1", "diff-additions": "1", "diff-deletions": "1"}
412+
)
413+
live["comments"].append(_comment(dossier))
414+
verdict, errors = mod.evaluate(live)
415+
assert verdict == mod.VERDICT_READY, errors
416+
417+
418+
def test_absent_diff_stat_never_reads_as_an_empty_diff():
419+
"""Missing data must not decide (#14849) -- and here it already cannot.
420+
421+
The new leg tests equality with 0, so `None` does not trip it. But the
422+
state is unreachable anyway: `validate_dossier` indexes `changedFiles`
423+
directly, and `main` catches `KeyError` among the fail-closed exceptions,
424+
reporting UNKNOWN (rc=2). Absent stats therefore become "I could not
425+
measure", never "the diff is empty" -- which is the distinction that
426+
matters, because rc=2 refuses while a fabricated `empty` would accuse.
427+
"""
428+
live = _base_snapshot()
429+
live.pop("changedFiles")
430+
live["comments"].append(_comment(_body()))
431+
with pytest.raises(KeyError):
432+
mod.evaluate(live)
433+
434+
353435
def test_comment_after_dossier_invalidates_it():
354436
snapshot = _snapshot(_body())
355437
snapshot["comments"].append(_comment("new concern after preflight"))

0 commit comments

Comments
 (0)