v3 : cinquième relecture — rien de ce qui touche un chiffre n'est une coquille
- Signature : tout caractère qui n'est pas une lettre latine et touche un chiffre compte, espaces et ponctuation comprises (« 5, 0 », « 1:5 », « 1…5 ») ; un tiret compte devant ou après un chiffre à travers les espaces (« - 5 ») ; seules quatre espaces sont libres (ordinaire, insécable, fine insécable, saut de ligne). - Négations complétées (guère, point, nothing, nobody, nadie, jamás…). - Adoption : le texte qui entre en vigueur est celui qui a été compté — une révision arrivée pendant le passage l'arrête. - Déploiement : « previous » est gardé avant le témoin de déploiement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+5
-5
@@ -99,6 +99,11 @@ steps:
|
|||||||
- /opt/libredecision-app:/opt/libredecision-app
|
- /opt/libredecision-app:/opt/libredecision-app
|
||||||
commands:
|
commands:
|
||||||
- DOMAIN=decision.librodrome.org
|
- DOMAIN=decision.librodrome.org
|
||||||
|
# La version en service (celle qui tourne, validée par son propre healthcheck) est
|
||||||
|
# gardée sous « previous » : « retablir » la relance si ce déploiement échoue.
|
||||||
|
- |
|
||||||
|
IMG=$(docker inspect --format '{{if .State.Running}}{{.Image}}{{end}}' lpk-libredecision-app 2>/dev/null || true)
|
||||||
|
if [ -n "$IMG" ]; then docker tag "$IMG" libredecision-app:previous && echo "Version en service gardee ($IMG)"; fi
|
||||||
# Témoin : à partir d'ici, un échec appelle « retablir ». Le healthcheck réussi l'efface.
|
# Témoin : à partir d'ici, un échec appelle « retablir ». Le healthcheck réussi l'efface.
|
||||||
- touch /opt/libredecision-app/deploying
|
- touch /opt/libredecision-app/deploying
|
||||||
- echo "APP_DOMAIN=$DOMAIN" > /opt/libredecision-app/.env
|
- echo "APP_DOMAIN=$DOMAIN" > /opt/libredecision-app/.env
|
||||||
@@ -125,11 +130,6 @@ steps:
|
|||||||
docker stop "$C" >/dev/null
|
docker stop "$C" >/dev/null
|
||||||
done
|
done
|
||||||
done
|
done
|
||||||
# La version en service (celle qui tourne, validée par son propre healthcheck) est
|
|
||||||
# gardée sous « previous » : « retablir » la relance si ce déploiement échoue.
|
|
||||||
- |
|
|
||||||
IMG=$(docker inspect --format '{{if .State.Running}}{{.Image}}{{end}}' lpk-libredecision-app 2>/dev/null || true)
|
|
||||||
if [ -n "$IMG" ]; then docker tag "$IMG" libredecision-app:previous && echo "Version en service gardee ($IMG)"; fi
|
|
||||||
# Arrêter avant le challenge ACME : la route hôte /* masquerait le webroot de sonic-acme-1.
|
# Arrêter avant le challenge ACME : la route hôte /* masquerait le webroot de sonic-acme-1.
|
||||||
- cd /opt/libredecision-app && docker compose stop
|
- cd /opt/libredecision-app && docker compose stop
|
||||||
- |
|
- |
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@
|
|||||||
racine tout conteneur qui la revendique), puis `.woodpecker/maquette.yml` (la maquette part
|
racine tout conteneur qui la revendique), puis `.woodpecker/maquette.yml` (la maquette part
|
||||||
sur maquette.decision.librodrome.org, seulement si le produit a réussi).
|
sur maquette.decision.librodrome.org, seulement si le produit a réussi).
|
||||||
- Relectures indépendantes avant mise en ligne, chacune traitée avant de livrer : `1f9c241 :
|
- Relectures indépendantes avant mise en ligne, chacune traitée avant de livrer : `1f9c241 :
|
||||||
ok`, `94907af : ok`, `ac9c904 : à reprendre`, `9203fd2 : à reprendre`. Les deux dernières ont
|
ok`, `94907af : ok`, `ac9c904 : à reprendre`, `9203fd2 : à reprendre`, `1eac6f8 : à reprendre`. Les trois dernières ont
|
||||||
montré qu'une « coquille » (sans vote, publiée) pouvait encore changer un sens — mots
|
montré qu'une « coquille » (sans vote, publiée) pouvait encore changer un sens — mots
|
||||||
retournés, caractères invisibles, sosies de chiffres, corrections enchaînées, adresses. La
|
retournés, caractères invisibles, sosies de chiffres, corrections enchaînées, adresses. La
|
||||||
règle est désormais une liste blanche mesurée contre le texte voté (voir CHANTIER.md). Les
|
règle est désormais une liste blanche mesurée contre le texte voté (voir CHANTIER.md). Les
|
||||||
|
|||||||
@@ -29,9 +29,9 @@ pub enum EditKind {
|
|||||||
const NEGATIONS: &[&str] = &[
|
const NEGATIONS: &[&str] = &[
|
||||||
// fr
|
// fr
|
||||||
"ne", "n", "pas", "jamais", "non", "sans", "aucun", "aucune", "ni", "plus", "rien", "nul", "nulle", "personne",
|
"ne", "n", "pas", "jamais", "non", "sans", "aucun", "aucune", "ni", "plus", "rien", "nul", "nulle", "personne",
|
||||||
// en
|
"guere", "point", // en
|
||||||
"not", "no", "never", "without", "none", "nor", // es
|
"not", "no", "never", "without", "none", "nor", "nothing", "nobody", "nowhere", "neither", // es
|
||||||
"nunca", "sin", "ningun", "ninguna", "ninguno", "tampoco", "nada",
|
"nunca", "sin", "ningun", "ninguna", "ninguno", "tampoco", "nada", "nadie", "jamas", "ni",
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Words that carry a quantity, a scope or a relation (folded); changing one is
|
/// Words that carry a quantity, a scope or a relation (folded); changing one is
|
||||||
@@ -90,9 +90,9 @@ fn is_latin_letter(c: char) -> bool {
|
|||||||
/// Ordinary punctuation, free to change in a typo fix — except where noted below.
|
/// Ordinary punctuation, free to change in a typo fix — except where noted below.
|
||||||
const PUNCTUATION: &[char] =
|
const PUNCTUATION: &[char] =
|
||||||
&['.', ',', ';', ':', '!', '?', '\'', '’', '‘', '"', '“', '”', '«', '»', '…', '‐', '-', '–', '—', '*', '_'];
|
&['.', ',', ';', ':', '!', '?', '\'', '’', '‘', '"', '“', '”', '«', '»', '…', '‐', '-', '–', '—', '*', '_'];
|
||||||
/// Separators that become part of a number between two figures ("10 000", "10,000", "182.625").
|
/// The only spaces free to change; any other (hair, thin, ogham…) is kept as it is.
|
||||||
const NUMBER_SEPARATORS: &[char] = &['.', ',', '\'', '’'];
|
const FREE_SPACES: &[char] = &[' ', '\u{a0}', '\u{202f}', '\n', '\r', '\t'];
|
||||||
/// Dashes that become a sign next to a figure ("-5", "5-10").
|
/// Dashes become a sign when a figure follows or precedes, even across spaces ("- 5").
|
||||||
const DASHES: &[char] = &['‐', '-', '–', '—'];
|
const DASHES: &[char] = &['‐', '-', '–', '—'];
|
||||||
|
|
||||||
/// Invisible format characters (zero-width, bidirectional overrides and
|
/// Invisible format characters (zero-width, bidirectional overrides and
|
||||||
@@ -109,22 +109,31 @@ pub fn has_hidden_controls(s: &str) -> bool {
|
|||||||
/// Latin letter, a space or ordinary punctuation — figures in any script, signs,
|
/// Latin letter, a space or ordinary punctuation — figures in any script, signs,
|
||||||
/// relations, brackets, look-alikes (U+201A for a comma, U+2044 for a slash,
|
/// relations, brackets, look-alikes (U+201A for a comma, U+2044 for a slash,
|
||||||
/// full-width forms), stray combining marks (U+0336, U+0338), other alphabets.
|
/// full-width forms), stray combining marks (U+0336, U+0338), other alphabets.
|
||||||
/// Spaces and punctuation count too where they belong to a number.
|
/// Anything that touches a figure counts too, spaces and punctuation included:
|
||||||
|
/// "5,0", "5,\u{202f}0", "1:5", "1…5" never read like "50" or "15".
|
||||||
fn signature(s: &str) -> String {
|
fn signature(s: &str) -> String {
|
||||||
let cs: Vec<char> = s.nfc().collect();
|
let cs: Vec<char> = s.nfc().collect();
|
||||||
let is_figure = |i: Option<usize>| i.and_then(|i| cs.get(i)).is_some_and(|c| c.is_numeric());
|
let is_figure = |i: Option<usize>| i.and_then(|i| cs.get(i)).is_some_and(|c| c.is_numeric());
|
||||||
|
// The next character that is not a free space, on either side.
|
||||||
|
let across = |i: usize, step: isize| {
|
||||||
|
let mut j = i as isize + step;
|
||||||
|
while j >= 0 && (j as usize) < cs.len() && FREE_SPACES.contains(&cs[j as usize]) {
|
||||||
|
j += step;
|
||||||
|
}
|
||||||
|
j >= 0 && is_figure(Some(j as usize))
|
||||||
|
};
|
||||||
cs.iter()
|
cs.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.filter(|&(i, &c)| {
|
.filter(|&(i, &c)| {
|
||||||
let (before, after) = (is_figure(i.checked_sub(1)), is_figure(Some(i + 1)));
|
let touches_figure = is_figure(i.checked_sub(1)) || is_figure(Some(i + 1));
|
||||||
if is_latin_letter(c) {
|
if is_latin_letter(c) {
|
||||||
false
|
false
|
||||||
} else if c.is_whitespace() {
|
} else if touches_figure {
|
||||||
before && after
|
|
||||||
} else if NUMBER_SEPARATORS.contains(&c) && before && after {
|
|
||||||
true
|
true
|
||||||
|
} else if FREE_SPACES.contains(&c) {
|
||||||
|
false
|
||||||
} else if DASHES.contains(&c) {
|
} else if DASHES.contains(&c) {
|
||||||
before || after
|
across(i, -1) || across(i, 1)
|
||||||
} else {
|
} else {
|
||||||
!PUNCTUATION.contains(&c)
|
!PUNCTUATION.contains(&c)
|
||||||
}
|
}
|
||||||
@@ -412,6 +421,19 @@ mod tests {
|
|||||||
("je certifierai", "je certifierais"),
|
("je certifierai", "je certifierais"),
|
||||||
("será válido", "sería válido"),
|
("será válido", "sería válido"),
|
||||||
("they must certify", "they mustn certify"),
|
("they must certify", "they mustn certify"),
|
||||||
|
// Fifth independent review: anything touching a figure, special spaces, signs across spaces.
|
||||||
|
("au moins 50 voix", "au moins 5,\u{202f}0 voix"),
|
||||||
|
("**12 mois**", "**1,\u{202f}2 mois**"),
|
||||||
|
("100 certifications", "10,\u{200a}0 certifications"),
|
||||||
|
("80%", "8,\u{200a}0%"),
|
||||||
|
("15 jours", "1:5 jours"),
|
||||||
|
("15 jours", "1…5 jours"),
|
||||||
|
("15 jours", "1*5 jours"),
|
||||||
|
("15 jours", "1;5 jours"),
|
||||||
|
("15 jours", "1‘5 jours"),
|
||||||
|
("un écart de 5", "un écart de -\u{200a}5"),
|
||||||
|
("un écart de 5", "un écart de - 5"),
|
||||||
|
("un écart de 5", "un écart de \u{1680}5"),
|
||||||
] {
|
] {
|
||||||
assert_eq!(classify(old, new), EditKind::Substantive, "{old} → {new}");
|
assert_eq!(classify(old, new), EditKind::Substantive, "{old} → {new}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,6 +78,22 @@ pub async fn run(state: &AppState, doc_id: &str) -> AppResult<Vec<Entered>> {
|
|||||||
let params = us.map(|u| compose::params_for(u, p.kind(), st.doc.vote_floor));
|
let params = us.map(|u| compose::params_for(u, p.kind(), st.doc.vote_floor));
|
||||||
let now = util::now();
|
let now = util::now();
|
||||||
let mut tx = state.db.begin().await?;
|
let mut tx = state.db.begin().await?;
|
||||||
|
// The text that enters is the text that was counted: a revision that landed
|
||||||
|
// since the state was read stops it (the next pass will judge it again).
|
||||||
|
let unchanged = sqlx::query(
|
||||||
|
"UPDATE proposals SET updated_at = updated_at
|
||||||
|
WHERE id = ? AND revision = ? AND title = ? AND body = ? AND status = ?",
|
||||||
|
)
|
||||||
|
.bind(&p.id)
|
||||||
|
.bind(p.revision)
|
||||||
|
.bind(&p.title)
|
||||||
|
.bind(&p.body)
|
||||||
|
.bind(&p.status)
|
||||||
|
.execute(&mut *tx)
|
||||||
|
.await?;
|
||||||
|
if unchanged.rows_affected() != 1 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let kind = match p.kind() {
|
let kind = match p.kind() {
|
||||||
ProposalKind::Text | ProposalKind::Remove => {
|
ProposalKind::Text | ProposalKind::Remove => {
|
||||||
// The displaced version goes back among the proposals, with its votes.
|
// The displaced version goes back among the proposals, with its votes.
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ correction, pour que les glissements ne s'additionnent pas :
|
|||||||
- **jamais coquille** si change quoi que ce soit d'autre que des lettres latines, des espaces
|
- **jamais coquille** si change quoi que ce soit d'autre que des lettres latines, des espaces
|
||||||
et la ponctuation ordinaire (liste blanche : chiffres de toute écriture, signes, relations,
|
et la ponctuation ordinaire (liste blanche : chiffres de toute écriture, signes, relations,
|
||||||
parenthèses, sosies, marques superposées, autres alphabets restent figés), ni une adresse, ni
|
parenthèses, sosies, marques superposées, autres alphabets restent figés), ni une adresse, ni
|
||||||
le contenu entre accents graves, ni un séparateur ou un tiret collé à un chiffre ;
|
le contenu entre accents graves, ni rien de ce qui touche un chiffre (espaces et ponctuation
|
||||||
|
comprises), ni un tiret devant ou après un chiffre même à travers des espaces ; seules quatre
|
||||||
|
espaces sont libres (ordinaire, insécable, fine insécable, saut de ligne) ;
|
||||||
- **coquille** si le squelette (sans accents, casse, ponctuation, espaces) est identique, ou si
|
- **coquille** si le squelette (sans accents, casse, ponctuation, espaces) est identique, ou si
|
||||||
au plus 2 mots diffèrent chacun d'un seul glissement de clavier au cœur du mot — une lettre
|
au plus 2 mots diffèrent chacun d'un seul glissement de clavier au cœur du mot — une lettre
|
||||||
ajoutée, retirée ou deux voisines inversées, jamais aux deux premières lettres (préfixes) ni
|
ajoutée, retirée ou deux voisines inversées, jamais aux deux premières lettres (préfixes) ni
|
||||||
|
|||||||
@@ -86,8 +86,10 @@ ponctuation ordinaire. Tout le reste compte comme le sens et ne bouge pas : chif
|
|||||||
écriture), signes, relations, parenthèses, sosies (« ‚ » pour une virgule, « ⁄ » pour une barre,
|
écriture), signes, relations, parenthèses, sosies (« ‚ » pour une virgule, « ⁄ » pour une barre,
|
||||||
formes pleine chasse), marques superposées (rayure, barre), autres alphabets (cyrillique,
|
formes pleine chasse), marques superposées (rayure, barre), autres alphabets (cyrillique,
|
||||||
hébreu), rayure Markdown, tout ce qui est entre accents graves, les **adresses** (domaines,
|
hébreu), rayure Markdown, tout ce qui est entre accents graves, les **adresses** (domaines,
|
||||||
liens, courriels), et un séparateur ou un tiret collé à un chiffre (« 50 » → « 5,0 »,
|
liens, courriels), **tout ce qui touche un chiffre**, espaces et ponctuation comprises (« 50 » →
|
||||||
« 182.625 » → « 182 625 », « -5 »). Pour les mots : au plus deux mots, chacun **un seul
|
« 5,0 », « 5, 0 », « 1:5 » ; « 182.625 » → « 182 625 »), et un tiret devant ou après un chiffre,
|
||||||
|
même séparé par des espaces (« - 5 »). Seules quatre espaces sont libres (ordinaire,
|
||||||
|
insécable, fine insécable, saut de ligne) : les autres (fine, ultra-fine…) sont figées. Pour les mots : au plus deux mots, chacun **un seul
|
||||||
glissement de clavier au cœur du mot** — jamais aux deux premières ni aux deux dernières lettres
|
glissement de clavier au cœur du mot** — jamais aux deux premières ni aux deux dernières lettres
|
||||||
(préfixes « valide » → « invalide », terminaisons « certifierai » → « certifierais »), jamais
|
(préfixes « valide » → « invalide », terminaisons « certifierai » → « certifierais »), jamais
|
||||||
une lettre remplacée (« deux » → « dix »), jamais un mot de négation, de quantité ou de position
|
une lettre remplacée (« deux » → « dix »), jamais un mot de négation, de quantité ou de position
|
||||||
|
|||||||
Reference in New Issue
Block a user