Skip to content

feat(probas,#16620): Infer-5 — effets naturels NDE/NIE calculés, raccourcis CDE corrigés (P5a) #8427

feat(probas,#16620): Infer-5 — effets naturels NDE/NIE calculés, raccourcis CDE corrigés (P5a)

feat(probas,#16620): Infer-5 — effets naturels NDE/NIE calculés, raccourcis CDE corrigés (P5a) #8427

name: Translation Drift Check
# Purpose: DETECT (non-blocking, read-only) when a notebook source or one of its
# translations drifts out of sync with the translation CSV
# (translations/<famille>/<série>.csv, Epic #4957 / #1650). The CSV is the source
# of truth for the multilingual alignment maintained by extract_cells_to_csv.py.
#
# This job is intentionally NON-BLOCKING and READ-ONLY — same philosophy as
# catalog-drift.yml. It surfaces drift as a notice annotation so reviewers see
# it, but never writes to a branch and never fails the PR gate. The actual
# re-translation is PROPOSED by this check but EXECUTED manually by an agent
# (running the Argumentum datasetupdater motor on flagged rows, #4957 §3) —
# never an auto-commit LLM in CI.
#
# See #4957 (infra design) and #1650 (multilingual translation epic).
on:
pull_request:
branches: [ main ]
paths:
- 'MyIA.AI.Notebooks/**/*.ipynb'
- 'translations/**'
- 'scripts/translation/**'
workflow_dispatch:
permissions:
contents: read
pull-requests: write
concurrency:
group: translation-drift-${{ github.ref }}
cancel-in-progress: true
jobs:
translation-drift:
name: "Translation drift (read-only)"
# Route vers la jambe Linux auto-hebergee (#13378 tranche 5, decision ai-01
# 2026-09-02) : garde Python pur, sans secret. La garde same-repo ci-dessous
# saute les PRs de fork (pr_gate.py compte `skipped` comme OK) : aucun code de
# fork n'atteint le runner. Retour arriere = revert de la PR.
runs-on: [self-hosted, coursia-ephemeral, coursia-linux]
if: github.event.pull_request.head.repo.full_name == null || github.event.pull_request.head.repo.full_name == github.repository
steps:
- name: Checkout PR
uses: actions/checkout@v4
with:
fetch-depth: 0
filter: blob:none
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
# Non-blocking: --check always exits 0. Drift is surfaced as a notice
# annotation + JSON report. Pre-T1 (no translations/ committed yet) the
# script reports "0 CSV, nothing to verify" and the job is green.
- name: Detect translation drift
id: drift
run: |
if [ ! -d translations ]; then
echo "::notice title=Translation drift (informational)::No translations/ directory yet (pre-T1 phase, #4957). Nothing to verify."
echo "drift=none" >> "$GITHUB_OUTPUT"
exit 0
fi
# Capture stdout (JSON report) and stderr (diagnostics) separately.
# A drift detector that crashes must announce itself, NOT stay silent
# behind 2>/dev/null and report a false "all in sync".
STDERR_FILE="$(mktemp)"
REPORT="$(python scripts/translation/check_translation_sync.py translations --check --repo-root . 2>"$STDERR_FILE")"
SCRIPT_EXIT=$?
STDERR_CONTENT="$(cat "$STDERR_FILE")"
rm -f "$STDERR_FILE"
# Distinguish a script crash from a legitimately empty report.
if [ "$SCRIPT_EXIT" -ne 0 ] || ! echo "$REPORT" | python -c 'import sys,json; json.load(sys.stdin)' 2>/dev/null; then
echo "::warning title=Translation drift check (broken)::The drift detector itself failed (exit $SCRIPT_EXIT, non-JSON stdout). A false 'all in sync' cannot be trusted. Investigate the job log. Stderr: $STDERR_CONTENT"
echo "drift=broken" >> "$GITHUB_OUTPUT"
exit 0
fi
COUNT="$(echo "$REPORT" | python -c 'import sys,json; print(json.load(sys.stdin)["anomaly_count"])')"
# Fill rate by target language (#6949 point 1): a drift count alone is
# non-informative while 0% of cells carry a translation. The check
# script already prints this on stderr and in the JSON report; here we
# surface it into the PR annotation so a SRC_DRIFT=0 on a 0%-translated
# table can no longer read as "up to date" at the merge gate (#8678:
# a bare count perishes, a count plus its denominator self-contradicts).
FILL_SUMMARY="$(echo "$REPORT" | python -c 'import sys,json; sys.path.insert(0,"scripts/translation"); from check_perimeter import TARGET_LANGS as t; gf=json.load(sys.stdin).get("fill_rate",{}).get("global",{}); print(" ".join("%s=%s%%" % (l, gf.get(l,{}).get("pct",0.0)) for l in t))')"
ANY_FILLED="$(echo "$REPORT" | python -c 'import sys,json; sys.path.insert(0,"scripts/translation"); from check_perimeter import TARGET_LANGS as t; gf=json.load(sys.stdin).get("fill_rate",{}).get("global",{}); print(any(gf.get(l,{}).get("filled",0)>0 for l in t))')"
if [ "$COUNT" = "0" ]; then
if [ "$ANY_FILLED" = "True" ]; then
echo "::notice title=Translation sync::All translation CSVs in sync with notebooks. No drift. (fill rate: $FILL_SUMMARY)"
else
echo "::notice title=Translation sync (caveat)::Drift count is 0, BUT no translation is deposited in ANY target language ($FILL_SUMMARY). 'No drift' does NOT mean 'translated' here — the drift count is non-informative while the fill rate is 0%. Re-translation is gated on the T3 motor GO (#6949 point 2)."
fi
echo "drift=none" >> "$GITHUB_OUTPUT"
else
SUMMARY="$(echo "$REPORT" | python -c 'import sys,json; from collections import Counter; d=json.load(sys.stdin); print(", ".join(f"{v}={n}" for v,n in Counter(a["verdict"] for a in d["anomalies"]).items()))')"
echo "::notice title=Translation drift (informational)::$COUNT drift(s) detected ($SUMMARY). Fill rate: $FILL_SUMMARY. This does NOT block the PR. Re-translation is proposed here but executed manually by an agent (Argumentum motor, #4957 §3). See JSON report in the job log."
echo "$REPORT"
echo "drift=detected" >> "$GITHUB_OUTPUT"
fi
# This job is always green. Drift is surfaced as a notice annotation only.
- name: Done (non-blocking, read-only)
run: echo "Translation drift check complete (non-blocking, read-only)."