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
8 changes: 7 additions & 1 deletion .github/workflows/always-on-guards.yml
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,13 @@ jobs:
if [ "$CAP" = "True" ]; then
gh pr edit "$PR_NUMBER" --add-label variation-light-cap-reached 2>/dev/null || true
LANE=$(python3 -c "import json; print(json.load(open('/tmp/status.json')).get('lane','?'))" 2>/dev/null || echo "?")
CB=$(python3 -c "import json; c=(json.load(open('/tmp/status.json')).get('consumed_by') or {}); print(f\"#{c.get('number','?')} (merge a {c.get('mergedAt','?')})\") if c else print('une LIGHT anterieure de cette lane')" 2>/dev/null || echo "une LIGHT anterieure de cette lane")
# #14899 : budget_spent_by cite le/les grain(s) qui ont consomme
# le budget (LIGHT mergee pour l'axe TIER, grains light-genre pour
# l'axe GENRE) -- le fallback "une LIGHT anterieure" anonyme est ce
# qui a rendu la forensique manuelle de #14899 necessaire. Le
# consumed_by de repli garde la compat avec un status.json emis par
# une version du script sans le champ.
CB=$(python3 -c "import json; s=json.load(open('/tmp/status.json')); c=s.get('consumed_by') or None; print(s.get('budget_spent_by') or ((f\"#{c.get('number','?')} (merge a {c.get('mergedAt','?')})\") if c else 'une LIGHT anterieure de cette lane'))" 2>/dev/null || echo "une LIGHT anterieure de cette lane")
# Commentaire idempotent (marqueur HTML invisible anti-spam).
MARK="<!-- gvar2-light-cap -->"
if ! gh pr view "$PR_NUMBER" --json comments --jq '.comments[].body' 2>/dev/null | grep -qF "$MARK"; then
Expand Down
8 changes: 7 additions & 1 deletion .github/workflows/variation-tag-guard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -361,7 +361,13 @@ jobs:
if [ "$CAP" = "True" ]; then
gh pr edit "$PR_NUMBER" --add-label variation-light-cap-reached 2>/dev/null || true
LANE=$(python3 -c "import json; print(json.load(open('/tmp/status.json')).get('lane','?'))" 2>/dev/null || echo "?")
CB=$(python3 -c "import json; c=(json.load(open('/tmp/status.json')).get('consumed_by') or {}); print(f\"#{c.get('number','?')} (merge a {c.get('mergedAt','?')})\") if c else print('une LIGHT anterieure de cette lane')" 2>/dev/null || echo "une LIGHT anterieure de cette lane")
# #14899 : budget_spent_by cite le/les grain(s) qui ont consomme
# le budget (LIGHT mergee pour l'axe TIER, grains light-genre pour
# l'axe GENRE) -- le fallback "une LIGHT anterieure" anonyme est ce
# qui a rendu la forensique manuelle de #14899 necessaire. Le
# consumed_by de repli garde la compat avec un status.json emis par
# une version du script sans le champ.
CB=$(python3 -c "import json; s=json.load(open('/tmp/status.json')); c=s.get('consumed_by') or None; print(s.get('budget_spent_by') or ((f\"#{c.get('number','?')} (merge a {c.get('mergedAt','?')})\") if c else 'une LIGHT anterieure de cette lane'))" 2>/dev/null || echo "une LIGHT anterieure de cette lane")
# Commentaire idempotent : un marqueur HTML invisible evite le spam
# a chaque synchronize. On ne poste que si aucun commentaire ne le
# porte deja.
Expand Down
93 changes: 93 additions & 0 deletions scripts/tests/test_variation_light_cap.py
Original file line number Diff line number Diff line change
Expand Up @@ -2122,3 +2122,96 @@ def test_replay_valid_empty_array_still_computes_legitimate_false(tmp_path, caps
out = json.loads(capsys.readouterr().out)
assert out["cap_reached"] is False # calcule, pas fabrique
assert out.get("verdict", "assessed") == "assessed"


# --- #14899 : the cap message NAMES the grain that spent the budget ---------
#
# Incident (2026-09-06, lane myia-ai-01:CoursIA): the guard labeled #14898
# (LIGHT/readme) with "(une LIGHT anterieure de cette lane)" -- naming
# nothing. The manual forensics that followed concluded that a dependabot
# bump (#14848, untagged) had been attributed to the merger's lane and had
# consumed the budget. The replay of the exact dataset refuted that premise:
# the TIER axis had counted ZERO merged LIGHTs (spent=0; #14848 is invisible
# -- untagged PRs are dropped from numerator AND denominator), and the label
# had fired on the GENRE axis (#10480): #14727 (MED/guard) + the candidate's
# own readme = light_genre 2 > cap 1. The REAL defect the incident exposes is
# the anonymous fallback: a cap message that names nothing forces forensics,
# and forensics without data invent mechanisms. These tests pin BOTH facts:
# the bot-PR invisibility (issue acceptance 1-2, satisfied by existing
# behavior -- this is the positive control the issue asked for, in its honest
# form) and budget_spent_by citing the counted PR(s) (acceptance 3, delivered).


def test_check_pr_untagged_bot_pr_is_invisible_to_both_axes(tmp_path, capsys):
# The 2026-09-06 replay, pinned. A dependabot-shaped PR (no Grain tag)
# sits in the merged set beside two tagged MEDs of the lane; the open
# candidate is LIGHT/readme. The untagged PR must count NOWHERE.
lane = "myia-ai-01:CoursIA"
merged = [
{"number": 14727, "body": f"Grain: MED/guard -- lane {lane}",
"mergedAt": "2026-09-06T04:12:56Z"},
{"number": 14877, "body": f"Grain: MED/notebook-python -- lane {lane}",
"mergedAt": "2026-09-06T12:12:23Z"},
{"number": 14848, "body": "Bumps js-yaml from 3.15.0 to 3.15.2.",
"mergedAt": "2026-09-06T11:17:29Z"}, # dependabot, no tag
]
body = f"Grain: LIGHT/readme -- lane {lane}"
rc, res = _check_pr(tmp_path, merged, body, capsys=capsys)
assert rc == 0
# Denominator = 2 tagged MEDs + the candidate. The bot PR is absent.
assert res["lane_grains"] == 3
assert res["spent"] == 0 # ZERO merged LIGHT counted
assert res["tier_cap_reached"] is False # the tier axis consumed nothing
# The GENRE axis is what fires (#10480): #14727 MED/guard + candidate.
assert res["cap_exceeded_by_genre"] is True
assert res["cap_reached"] is True


def test_check_pr_budget_spent_by_names_the_genre_grain(tmp_path, capsys):
# Same day, one grain merged: the message must NAME #14727 with its
# tier and genre. Pre-fix, the genre-axis firing left consumed_by null
# and the workflow printed the anonymous "une LIGHT anterieure".
lane = "myia-ai-01:CoursIA"
merged = [
{"number": 14727, "body": f"Grain: MED/guard -- lane {lane}",
"mergedAt": "2026-09-06T04:12:56Z"},
]
body = f"Grain: LIGHT/readme -- lane {lane}"
rc, res = _check_pr(tmp_path, merged, body, capsys=capsys)
assert rc == 0
assert res["cap_reached"] is True
assert res["consumed_by"] is None # tier axis empty -- genre fired
named = res["budget_spent_by"]
assert named is not None
assert "#14727" in named
assert "MED/guard" in named
assert "2026-09-06T04:12:56Z" in named


def test_check_pr_budget_spent_by_names_tier_light_when_present(tmp_path, capsys):
# TIER-axis firing keeps the historical naming: the earliest merged
# LIGHT, same citation the workflow message always built from consumed_by.
lane = "myia-po-2023:CoursIA"
merged = [
{"number": 7, "body": f"Grain: LIGHT/tooling -- lane {lane}",
"mergedAt": "2026-09-06T05:00:00Z"},
]
body = f"Grain: LIGHT/tooling -- lane {lane}"
rc, res = _check_pr(tmp_path, merged, body, capsys=capsys)
assert rc == 0
assert res["cap_reached"] is True
assert res["consumed_by"]["number"] == 7
assert "#7" in res["budget_spent_by"]
assert "2026-09-06T05:00:00Z" in res["budget_spent_by"]


def test_check_pr_budget_spent_by_none_when_nothing_merged(tmp_path, capsys):
# Nothing merged, candidate is the lane's first grain: no consumption to
# cite on either axis -> budget_spent_by is None (and cap_reached False;
# the workflow's fallback text remains its own business).
lane = "myia-po-2023:CoursIA"
rc, res = _check_pr(tmp_path, [], f"Grain: LIGHT/readme -- lane {lane}",
capsys=capsys)
assert rc == 0
assert res["cap_reached"] is False
assert res["budget_spent_by"] is None
36 changes: 35 additions & 1 deletion scripts/variation_light_cap.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@
same lane-day. Emits `cap_reached` (the union, when the
candidate carries the LIGHT-genre motif, #10341),
`tier_cap_reached`, `cap_exceeded_by_genre`, `lane`,
`consumed_by`, and a `counts: "tier+genre"` disclosure.
`consumed_by`, `budget_spent_by` (#14899: a ready-made
citation of the grain(s) that spent the budget -- the
merged LIGHT for the TIER axis, the merged light-genre
grains for the GENRE axis -- so the guard's message
never falls back to naming nothing), and a
`counts: "tier+genre"` disclosure.

Parsing
-------
Expand Down Expand Up @@ -1391,6 +1396,34 @@ def main(argv: list[str] | None = None) -> int:
picker_cmd = None
if vein_runs_list:
picker_cmd = picker_command_for_vein(vein_runs_list[0], lane)
# #14899 : the workflow's "budget deja consomme" message must NAME the
# grain(s) that spent it. `consumed_by` only knows the TIER axis
# (earliest merged LIGHT); when the GENRE axis is what fired (MED/readme,
# MED/guard, ... -- "light-genre, quel que soit le tier declare"), it is
# null and the message fell back to "une LIGHT anterieure de cette
# lane", naming nothing. That anonymity is what forced the manual
# forensics behind #14899 (and the misread that followed).
if status["consumed_by"]:
budget_spent_by = (
f"#{status['consumed_by']['number']} "
f"(merge a {status['consumed_by']['mergedAt']})"
)
else:
genre_consumers = [
r for r in _candidate_record(merged, lane) if r["is_light_genre"]
]
if genre_consumers:
detail = ", ".join(
f"#{r['number']} ({r['tier']}/{r['genre']}, "
f"merge a {r['mergedAt']})"
for r in genre_consumers
)
budget_spent_by = (
"axe genre G-VAR-2/3 (light-genre, quel que soit le "
f"tier declare) : {detail}"
)
else:
budget_spent_by = None
print(json.dumps({
"pr": args.check_pr,
"lane": lane,
Expand All @@ -1407,6 +1440,7 @@ def main(argv: list[str] | None = None) -> int:
"genre_cap": genre_cap,
"lane_grains": status["lane_grains"],
"consumed_by": status["consumed_by"],
"budget_spent_by": budget_spent_by,
"counts": "tier+genre+vein",
}))
return 0
Expand Down
Loading