v3 : neuvième relecture — noms uniques, places justes, quantités
- Noms des liens d'équipe : comparés sans casse, accents ni ponctuation (ld_engine::names), lettres latines seulement (pas de sosie cyrillique, grec ou pleine chasse), une ligne ; index unique (migration additive 0003) contre deux ouvertures simultanées sous le même nom. - Une personne déjà connectée ne prend une place que si le lien lui ajoute un groupe ; « Ce n'est pas moi » la déconnecte sur un navigateur partagé ; un lien collé dans le même onglet est relu. - Coller ou couper un mot de quantité n'est pas une coquille (« sur tout » → « surtout », « par tout » → « partout »). - Libellés sans caractère de contrôle ; la soumission revérifie le titre. Boucle : 68 tests Rust, svelte-check, 9 vitest, e2e 13/13. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -236,8 +236,9 @@ fn words(s: &str) -> Vec<String> {
|
||||
/// « latransmission » → « la transmission ». A boundary moved is not a typo
|
||||
/// (« les avoir » → « le savoir »), nor « Si non » → « Sinon ».
|
||||
fn only_glued_or_split(a: &[String], b: &[String]) -> bool {
|
||||
// Neither the word glued or split nor its pieces may be a negation (« ja mais »).
|
||||
let joined = |w: &str, x: &str, y: &str| ![w, x, y].iter().any(|t| NEGATIONS.contains(t));
|
||||
// Neither the word glued or split nor its pieces may be a negation or a
|
||||
// quantity (« ja mais », « sur tout » → « surtout », « par tout » → « partout »).
|
||||
let joined = |w: &str, x: &str, y: &str| ![w, x, y].iter().any(|t| NEGATIONS.contains(t) || QUANTITIES.contains(t));
|
||||
let (mut i, mut j, mut moves) = (0, 0, 0);
|
||||
while i < a.len() && j < b.len() {
|
||||
if a[i] == b[j] {
|
||||
@@ -424,7 +425,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_negation_is_never_cut_nor_formed() {
|
||||
fn a_negation_or_a_quantity_is_never_cut_nor_formed() {
|
||||
// Eighth independent review, 2026-10-21: the pieces were checked, not the word.
|
||||
for (old, new) in [
|
||||
("n'avoir jamais qu'un seul compte", "n'avoir ja mais qu'un seul compte"),
|
||||
@@ -432,6 +433,9 @@ mod tests {
|
||||
("sans retour", "san s retour"),
|
||||
("rien de plus", "ri en de plus"),
|
||||
("nowhere else", "now here else"),
|
||||
// Ninth review: a quantity glued changes a scope.
|
||||
("Je vérifie sur tout le territoire", "Je vérifie surtout le territoire"),
|
||||
("par tout membre", "partout membre"),
|
||||
] {
|
||||
assert_eq!(classify(old, new), EditKind::Substantive, "{old} → {new}");
|
||||
assert_eq!(classify(new, old), EditKind::Substantive, "{new} → {old}");
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
pub mod assist;
|
||||
pub mod compose;
|
||||
pub mod edit;
|
||||
pub mod names;
|
||||
pub mod threshold;
|
||||
|
||||
pub use threshold::{FormulaParams, InertiaPreset, Tally};
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
//! Names people give themselves (team links): what is accepted, and the key two
|
||||
//! names collide on.
|
||||
//!
|
||||
//! A name is shown as the author of proposals, revisions and signals, so nobody
|
||||
//! may take a name already there, even dressed differently: « émile » for
|
||||
//! « Émile », an accent written apart, a Cyrillic « в » or a Greek « Υ » for a
|
||||
//! Latin letter, full-width « Yvv ». Hence a short alphabet — Latin letters
|
||||
//! as French, English and Spanish write them, figures, space, hyphen,
|
||||
//! apostrophe, dot — and a key that forgets case, accents and punctuation.
|
||||
|
||||
use unicode_normalization::UnicodeNormalization;
|
||||
use unicode_normalization::char::is_combining_mark;
|
||||
|
||||
const MIN_NAME: usize = 2;
|
||||
const MAX_NAME: usize = 60;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub struct Name {
|
||||
/// As shown: NFC, spaces collapsed.
|
||||
pub display: String,
|
||||
/// What two names collide on.
|
||||
pub key: String,
|
||||
}
|
||||
|
||||
fn allowed(c: char) -> bool {
|
||||
c.is_ascii_alphanumeric()
|
||||
|| (c.is_alphabetic() && matches!(c as u32, 0xC0..=0x17F))
|
||||
|| matches!(c, ' ' | '-' | '\'' | '’' | '.')
|
||||
}
|
||||
|
||||
/// Lowercase, no accents, letters and figures only: « Émile », « EMILE »,
|
||||
/// « É-mile » share the key « emile ».
|
||||
pub fn key(s: &str) -> String {
|
||||
s.nfkd().filter(|c| !is_combining_mark(*c)).flat_map(char::to_lowercase).filter(|c| c.is_alphanumeric()).collect()
|
||||
}
|
||||
|
||||
/// A name a person may take, or `None`.
|
||||
pub fn check(raw: &str) -> Option<Name> {
|
||||
let raw = raw.trim();
|
||||
// One line, ordinary spaces only (no line or paragraph separator).
|
||||
if raw.chars().any(|c| c.is_whitespace() && c != ' ') {
|
||||
return None;
|
||||
}
|
||||
let display = raw.nfc().collect::<String>().split(' ').filter(|w| !w.is_empty()).collect::<Vec<_>>().join(" ");
|
||||
let n = display.chars().count();
|
||||
if !(MIN_NAME..=MAX_NAME).contains(&n) || !display.chars().all(allowed) {
|
||||
return None;
|
||||
}
|
||||
let key = key(&display);
|
||||
if key.chars().count() < MIN_NAME {
|
||||
return None;
|
||||
}
|
||||
Some(Name { display, key })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn ordinary_names() {
|
||||
assert_eq!(check(" Émile Zola ").map(|n| n.display), Some("Émile Zola".into()));
|
||||
assert_eq!(check("Jean-Marc O'Neil").map(|n| n.key), Some("jeanmarconeil".into()));
|
||||
assert_eq!(check("Núria").map(|n| n.key), Some("nuria".into()));
|
||||
assert!(check("Chiara 2").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_same_name_dressed_differently_collides() {
|
||||
let emile = key("Émile");
|
||||
for other in ["émile", "EMILE", "E\u{301}mile", "É-mile", "Emile."] {
|
||||
assert_eq!(check(other).map(|n| n.key), Some(emile.clone()), "{other}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn look_alikes_and_invisibles_are_refused() {
|
||||
// Eighth and ninth independent reviews, 2026-10-21.
|
||||
for name in ["Yvв", "Υvv", "Yvv", "Yv\u{2028}v", "Yvv\u{fff9}", "Yv\u{200b}v", "Y\nvv", "Y", "--", "Yvv 🙂"]
|
||||
{
|
||||
assert_eq!(check(name), None, "{name:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user