v3 : seconde relecture — une coquille ne touche ni chiffre, ni signe, ni relation ; retour arrière du déploiement
- Correction de coquille : un chiffre, un signe, un séparateur dans un nombre ou une relation changés (« >= » → « <= », « + » → « - », « 182.625 » → « 182 625 », « % » → « ‰ ») rendent la modification « de fond ». Testé dans le moteur et par l'API sur le vrai texte K1. - Déploiement : les conteneurs retirés de la racine qui tournaient sont notés ; si la suite échoue, l'étape « retablir » arrête le produit et les relance. Le produit est recréé à chaque déploiement (--force-recreate). - Cookies préfixés en https : __Host-ld_session, __Secure-ld_oauth (pas de cookie planté par un site voisin sous librodrome.org). - syft épinglé (v1.55.0) dans les deux pipelines : ce conteneur a le socket Docker. - Docs : limites connues assumées, groupe d'essai à deux voix, error.identity_kind. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,8 +4,9 @@
|
||||
//! tells two kinds of corrections apart:
|
||||
//!
|
||||
//! - **typographic**: accents, case, punctuation, spacing, emphasis, or at most
|
||||
//! two misspelt words fixed (edit distance ≤ 2, no number, no negation
|
||||
//! touched). The votes stay; the correction is shown with its diff.
|
||||
//! two misspelt words fixed (edit distance ≤ 2, no negation touched) — and
|
||||
//! never a figure, a sign, a separator inside a number or a relation
|
||||
//! (`≥`, `+`, `%`…). The votes stay; the correction is shown with its diff.
|
||||
//! - **substantive**: anything else. It opens a new revision and every vote
|
||||
//! cast on the previous one waits for its author to reconfirm it.
|
||||
//!
|
||||
@@ -48,9 +49,32 @@ fn skeleton(s: &str) -> String {
|
||||
fold(s).chars().filter(|c| c.is_alphanumeric()).collect()
|
||||
}
|
||||
|
||||
/// The numbers of a text, as written: "50" and "5,0", "1 5" and "15" differ.
|
||||
fn numbers(s: &str) -> Vec<&str> {
|
||||
s.split(|c: char| !c.is_ascii_digit()).filter(|n| !n.is_empty()).collect()
|
||||
/// Symbols that carry a value or a relation, wherever they stand.
|
||||
const RELATIONS: &[char] =
|
||||
&['+', '±', '×', '÷', '^', '<', '>', '=', '≤', '≥', '≠', '≈', '%', '‰', '€', '$', '£', '∞', '√'];
|
||||
/// Signs and fraction bars: meaningful next to a figure ("-5", "1/5", "5-10").
|
||||
const SIGNS: &[char] = &['-', '−', '–', '/'];
|
||||
/// Separators: meaningful between two figures ("10 000", "10,000", "182.625").
|
||||
const SEPARATORS: &[char] = &['.', ',', ' ', '\u{a0}', '\u{202f}', '\'', '’'];
|
||||
|
||||
/// What the text says in figures and symbols, as written. Two texts with the
|
||||
/// same letters but a different figure, sign, separator inside a number or
|
||||
/// relation ("≥" → "≤", "+" → "-", "50" → "5,0", "182.625" → "182 625") do not
|
||||
/// say the same thing.
|
||||
fn figures(s: &str) -> String {
|
||||
let cs: Vec<char> = s.chars().collect();
|
||||
let is_figure = |i: Option<usize>| i.and_then(|i| cs.get(i)).is_some_and(|c| c.is_numeric());
|
||||
cs.iter()
|
||||
.enumerate()
|
||||
.filter(|&(i, c)| {
|
||||
let (before, after) = (is_figure(i.checked_sub(1)), is_figure(Some(i + 1)));
|
||||
c.is_numeric()
|
||||
|| RELATIONS.contains(c)
|
||||
|| (SIGNS.contains(c) && (before || after))
|
||||
|| (SEPARATORS.contains(c) && before && after)
|
||||
})
|
||||
.map(|(_, c)| *c)
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Folded words (runs of letters/digits).
|
||||
@@ -62,8 +86,8 @@ pub fn classify(old: &str, new: &str) -> EditKind {
|
||||
if old == new {
|
||||
return EditKind::Unchanged;
|
||||
}
|
||||
// A number is never a typo: punctuation or a space inside it changes its value.
|
||||
if numbers(old) != numbers(new) {
|
||||
// A figure, a sign or a relation is never a typo: it changes what the text says.
|
||||
if figures(old) != figures(new) {
|
||||
return EditKind::Substantive;
|
||||
}
|
||||
if skeleton(old) == skeleton(new) {
|
||||
@@ -208,11 +232,27 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_number_is_never_a_typo() {
|
||||
// Same letters and digits, different values (independent review, 2026-10-21).
|
||||
assert_eq!(classify("au moins 50 voix", "au moins 5,0 voix"), EditKind::Substantive);
|
||||
assert_eq!(classify("1 5 jours", "15 jours"), EditKind::Substantive);
|
||||
fn a_figure_a_sign_or_a_relation_is_never_a_typo() {
|
||||
// Same letters, different meaning (independent reviews, 2026-10-21).
|
||||
for (old, new) in [
|
||||
("au moins 50 voix", "au moins 5,0 voix"),
|
||||
("1 5 jours", "15 jours"),
|
||||
("10 000 membres", "10,000 membres"),
|
||||
("un écart de -5", "un écart de 5"),
|
||||
("1/5 des voix", "1.5 des voix"),
|
||||
("50 % des voix", "50 ‰ des voix"),
|
||||
("avoir >= 5 certifications", "avoir <= 5 certifications"),
|
||||
("DU(n) + c² × M(n)", "DU(n) - c² × M(n)"),
|
||||
("182.625 jours", "182 625 jours"),
|
||||
("c²", "c2"),
|
||||
] {
|
||||
assert_eq!(classify(old, new), EditKind::Substantive, "{old} → {new}");
|
||||
}
|
||||
// Punctuation around figures, list markers, dashes between words stay typographic.
|
||||
assert_eq!(classify("50 voix pour", "50 voix pour !"), EditKind::Typographic);
|
||||
assert_eq!(classify("- une liste", "* une liste"), EditKind::Typographic);
|
||||
assert_eq!(classify("1. premier point", "1) premier point"), EditKind::Typographic);
|
||||
assert_eq!(classify("libre - le texte", "libre — le texte"), EditKind::Typographic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user