v3 : sixième relecture — une coquille touche les lettres et les espaces, rien d'autre
- La forme du texte (tout ce qui n'est ni lettre latine ni espace, dans l'ordre, avec les espaces qui touchent un chiffre) ne bouge plus : ponctuation, emphase, signes, parenthèses passent par une proposition (un signe moins devant un nombre en gras, deux conditions fusionnées, une parenthèse déplacée). Apostrophes et guillemets de tous styles comptent pour un même signe. - Les espaces ne font que coller ou couper des mots, jamais déplacer une frontière (« les avoir » → « le savoir »). - Identifiants qui contiennent un chiffre (clés, références) comparés à l'identique, casse comprise, comme les adresses. - Correction écrite seulement sur le texte jugé (révision, statut, texte) : sinon conflit « changed » — plus de course avec une entrée en vigueur. - Déploiement : « previous » n'est pas repris d'un pipeline annulé. - Interface (fr/en/es) : la note de correction dit la règle telle qu'elle est. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+4
-1
@@ -101,9 +101,12 @@ steps:
|
||||
- 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.
|
||||
# Un témoin resté d'un pipeline annulé : ce qui tourne n'a pas été validé, on garde l'ancien « previous ».
|
||||
- |
|
||||
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
|
||||
if [ -n "$IMG" ] && [ ! -e /opt/libredecision-app/deploying ]; 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.
|
||||
- touch /opt/libredecision-app/deploying
|
||||
- echo "APP_DOMAIN=$DOMAIN" > /opt/libredecision-app/.env
|
||||
|
||||
+10
-11
@@ -7,7 +7,7 @@
|
||||
**Chantier 1 — Acte d'engagement Certification** : construit, boucle verte, prêt à livrer.
|
||||
|
||||
- v3 Rust + Svelte à la racine du dépôt ; la maquette v2 intacte dans `maquette/`.
|
||||
- Boucle : Rust (fmt, clippy `-D warnings`, 59 tests dont 14 scénarios d'API et la
|
||||
- Boucle : Rust (fmt, clippy `-D warnings`, 60 tests dont 14 scénarios d'API et la
|
||||
publication vers un dépôt nu) · interface (svelte-check 0 avertissement, 9 vitest) ·
|
||||
e2e 12 / 12 (C1–C11, M1). L'image Docker rejoue tout et a été essayée localement :
|
||||
santé, interface, volume non-root, clé de publication, lien admin initial (0600, absent
|
||||
@@ -16,16 +16,15 @@
|
||||
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).
|
||||
- Relectures indépendantes avant mise en ligne, chacune traitée avant de livrer : `1f9c241 :
|
||||
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
|
||||
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
|
||||
autres points : ordre des workflows, anciens conteneurs en
|
||||
`restart: always`, vote réservé aux membres de la toile dans Monnaie libre (une personne,
|
||||
une voix), lien d'invitation jamais vers une identité Ğ1, réservation du lien avant usage,
|
||||
un nombre n'est jamais une coquille, redirection après connexion, en-têtes anti-encadrement,
|
||||
cookies préfixés `__Host-`, retour arrière automatique du déploiement (maquette au premier,
|
||||
version précédente ensuite), syft épinglé.
|
||||
ok`, `94907af : ok`, puis quatre « à reprendre » (`ac9c904`, `9203fd2`, `1eac6f8`, `6c0754a`),
|
||||
toutes sur la même question : une « coquille » (sans vote, publiée) qui change un sens. La
|
||||
règle est désormais simple et explicable : **une coquille touche les lettres et les espaces,
|
||||
rien d'autre** (CHANTIER.md, ADR 0003). Autres points traités : ordre des workflows, anciens
|
||||
conteneurs en `restart: always`, vote réservé aux membres de la toile dans Monnaie libre (une
|
||||
personne, une voix), lien d'invitation jamais vers une identité Ğ1, réservation du lien avant
|
||||
usage, redirection après connexion, en-têtes anti-encadrement, cookies `__Host-`, retour
|
||||
arrière automatique du déploiement (maquette au premier, version précédente ensuite), courses
|
||||
entre correction et entrée en vigueur, syft épinglé.
|
||||
|
||||
## Ce qui attend Yvv
|
||||
|
||||
|
||||
@@ -3,12 +3,12 @@
|
||||
//! Once a proposal is under vote, its author may still correct it. The engine
|
||||
//! tells two kinds of corrections apart:
|
||||
//!
|
||||
//! - **typographic**: accents, case, punctuation, spacing, emphasis, or at most
|
||||
//! two misspelt words fixed by a single slip each (one letter added, removed
|
||||
//! or swapped, never at the start of the word, never on a negation or a
|
||||
//! quantity) — and never a figure, a sign, a separator inside a number, a
|
||||
//! relation, a bracket or anything between backticks. The votes stay; the
|
||||
//! correction is shown with its diff.
|
||||
//! - **typographic**: letters and spaces only — accents, case, the style of
|
||||
//! apostrophes and quotes, words glued or split, or at most two misspelt
|
||||
//! words fixed by a single slip each (one letter added, removed or swapped
|
||||
//! in the middle of the word, never on a negation or a quantity). Never a
|
||||
//! punctuation mark, a figure, a sign, an emphasis, a formula or an address.
|
||||
//! 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.
|
||||
//!
|
||||
@@ -87,13 +87,8 @@ fn is_latin_letter(c: char) -> bool {
|
||||
c.is_alphabetic() && matches!(c as u32, 0x41..=0x5A | 0x61..=0x7A | 0xC0..=0x24F | 0x1E00..=0x1EFF)
|
||||
}
|
||||
|
||||
/// Ordinary punctuation, free to change in a typo fix — except where noted below.
|
||||
const PUNCTUATION: &[char] =
|
||||
&['.', ',', ';', ':', '!', '?', '\'', '’', '‘', '"', '“', '”', '«', '»', '…', '‐', '-', '–', '—', '*', '_'];
|
||||
/// The only spaces free to change; any other (hair, thin, ogham…) is kept as it is.
|
||||
const FREE_SPACES: &[char] = &[' ', '\u{a0}', '\u{202f}', '\n', '\r', '\t'];
|
||||
/// Dashes become a sign when a figure follows or precedes, even across spaces ("- 5").
|
||||
const DASHES: &[char] = &['‐', '-', '–', '—'];
|
||||
|
||||
/// Invisible format characters (zero-width, bidirectional overrides and
|
||||
/// isolates, byte order mark): they can make "12" display as "21" ("Trojan
|
||||
@@ -104,42 +99,43 @@ pub fn has_hidden_controls(s: &str) -> bool {
|
||||
)
|
||||
}
|
||||
|
||||
/// Everything a typo fix may not change, in order. Built the other way round
|
||||
/// from a list of dangerous symbols: what is kept is everything that is NOT a
|
||||
/// 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,
|
||||
/// full-width forms), stray combining marks (U+0336, U+0338), other alphabets.
|
||||
/// 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 {
|
||||
/// Apostrophes and quotation marks of every style are one and the same sign:
|
||||
/// « Je m'engage » → « Je m’engage » is typography.
|
||||
fn same_mark(c: char) -> char {
|
||||
match c {
|
||||
'’' | '‘' | 'ʼ' => '\'',
|
||||
'“' | '”' | '«' | '»' => '"',
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
/// The shape of a text: every run of Latin letters becomes one mark, free spaces
|
||||
/// vanish, and **everything else stays as it is, in order** — punctuation,
|
||||
/// figures, signs, emphasis, brackets, look-alikes, other alphabets. A typo fix
|
||||
/// keeps the shape: it touches letters and spaces only (2026-10-21, after six
|
||||
/// independent reviews showed that any free punctuation can carry a meaning:
|
||||
/// a minus sign before a bold number, a list merged into one condition…).
|
||||
fn shape(s: &str) -> String {
|
||||
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());
|
||||
// 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()
|
||||
.enumerate()
|
||||
.filter(|&(i, &c)| {
|
||||
let touches_figure = is_figure(i.checked_sub(1)) || is_figure(Some(i + 1));
|
||||
if is_latin_letter(c) {
|
||||
false
|
||||
} else if touches_figure {
|
||||
true
|
||||
} else if FREE_SPACES.contains(&c) {
|
||||
false
|
||||
} else if DASHES.contains(&c) {
|
||||
across(i, -1) || across(i, 1)
|
||||
} else {
|
||||
!PUNCTUATION.contains(&c)
|
||||
let mut out = String::new();
|
||||
let mut in_word = false;
|
||||
for (i, &c) in cs.iter().enumerate() {
|
||||
if is_latin_letter(c) {
|
||||
if !in_word {
|
||||
out.push('a');
|
||||
}
|
||||
})
|
||||
.map(|(_, &c)| c)
|
||||
.collect()
|
||||
in_word = true;
|
||||
} else if FREE_SPACES.contains(&c) && !is_figure(i.checked_sub(1)) && !is_figure(Some(i + 1)) {
|
||||
// A space between letters may come and go: « latransmission » and
|
||||
// « la transmission » share a shape (segmentation is checked apart).
|
||||
// Next to a figure it stays: « 1 5 » is not « 15 ».
|
||||
} else {
|
||||
out.push(same_mark(c));
|
||||
in_word = false;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// What stands between backticks: formulas and identifiers, compared exactly.
|
||||
@@ -147,15 +143,16 @@ fn code_spans(s: &str) -> Vec<&str> {
|
||||
s.split('`').skip(1).step_by(2).collect()
|
||||
}
|
||||
|
||||
/// Web and mail addresses, domains, paths: compared exactly ("duniter" /
|
||||
/// "dunniter" is a typo of a word, not of an address).
|
||||
fn addresses(s: &str) -> Vec<String> {
|
||||
/// Addresses (links, domains, mails, paths) and identifiers that hold a figure
|
||||
/// (keys, fingerprints, references): compared exactly, case included.
|
||||
fn exact_tokens(s: &str) -> Vec<String> {
|
||||
s.split_whitespace()
|
||||
.map(|t| t.trim_matches(|c: char| !c.is_alphanumeric()))
|
||||
.filter(|t| {
|
||||
let cs: Vec<char> = t.chars().collect();
|
||||
t.contains("://")
|
||||
|| t.contains('@')
|
||||
|| cs.iter().any(|c| c.is_numeric()) && cs.iter().any(|c| c.is_alphabetic())
|
||||
|| (1..cs.len().saturating_sub(1))
|
||||
.any(|i| matches!(cs[i], '.' | '/') && cs[i - 1].is_alphanumeric() && cs[i + 1].is_alphanumeric())
|
||||
})
|
||||
@@ -199,6 +196,24 @@ fn words(s: &str) -> Vec<String> {
|
||||
fold(s).split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()).map(str::to_owned).collect()
|
||||
}
|
||||
|
||||
/// Same letters, words only glued or split: « latransmission » → « la
|
||||
/// transmission ». A boundary moved is not a typo (« les avoir » → « le savoir »).
|
||||
fn only_glued_or_split(a: &[String], b: &[String]) -> bool {
|
||||
let (mut i, mut j) = (0, 0);
|
||||
while i < a.len() && j < b.len() {
|
||||
if a[i] == b[j] {
|
||||
(i, j) = (i + 1, j + 1);
|
||||
} else if j + 1 < b.len() && a[i] == format!("{}{}", b[j], b[j + 1]) {
|
||||
(i, j) = (i + 1, j + 2);
|
||||
} else if i + 1 < a.len() && b[j] == format!("{}{}", a[i], a[i + 1]) {
|
||||
(i, j) = (i + 2, j + 1);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
i == a.len() && j == b.len()
|
||||
}
|
||||
|
||||
/// Classify `new` against `old`. For a text under vote, `old` must be the text
|
||||
/// that was **voted** (its last initial or substantive revision), not the
|
||||
/// current one: typo fixes are measured together, so slips never add up.
|
||||
@@ -207,16 +222,17 @@ pub fn classify(old: &str, new: &str) -> EditKind {
|
||||
return EditKind::Unchanged;
|
||||
}
|
||||
if has_hidden_controls(new)
|
||||
|| signature(old) != signature(new)
|
||||
|| shape(old) != shape(new)
|
||||
|| code_spans(old) != code_spans(new)
|
||||
|| addresses(old) != addresses(new)
|
||||
|| exact_tokens(old) != exact_tokens(new)
|
||||
{
|
||||
return EditKind::Substantive;
|
||||
}
|
||||
if skeleton(old) == skeleton(new) {
|
||||
return EditKind::Typographic;
|
||||
}
|
||||
let (a, b) = (words(old), words(new));
|
||||
if skeleton(old) == skeleton(new) {
|
||||
// Accents, case, apostrophes; words glued or split, never re-cut.
|
||||
return if only_glued_or_split(&a, &b) { EditKind::Typographic } else { EditKind::Substantive };
|
||||
}
|
||||
if a.len() != b.len() {
|
||||
return EditKind::Substantive;
|
||||
}
|
||||
@@ -346,12 +362,36 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accents_case_punctuation_emphasis() {
|
||||
assert_eq!(classify("Je m'engage a verifier.", "Je m’engage à vérifier !"), EditKind::Typographic);
|
||||
assert_eq!(classify("un **être humain**", "un être humain"), EditKind::Typographic);
|
||||
fn accents_case_apostrophes_quotes() {
|
||||
assert_eq!(classify("Je m'engage a verifier.", "Je m’engage à vérifier."), EditKind::Typographic);
|
||||
assert_eq!(classify("le \"Pacte\" de la monnaie", "le « Pacte » de la Monnaie"), EditKind::Typographic);
|
||||
assert_eq!(classify("same", "same"), EditKind::Unchanged);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn punctuation_and_emphasis_go_through_a_proposal() {
|
||||
// Sixth independent review: free punctuation can always carry a meaning.
|
||||
for (old, new) in [
|
||||
("Je m'engage à vérifier.", "Je m'engage à vérifier !"),
|
||||
("Je m'engage à vérifier.", "Je m'engage à vérifier ?"),
|
||||
("un **être humain**", "un être humain"),
|
||||
("tous les **5 jours**", "tous les -**5 jours**"),
|
||||
("Seuil = C + B^W", "Seuil = C + B^-W"),
|
||||
("max(0, T-C)", "max(0, TC)"),
|
||||
("(Y = ceil(N))", "(Y = -ceil(N))"),
|
||||
("- avoir un compte\n- être joignable", "- avoir un compte, être joignable"),
|
||||
("(sauf le cas a) et le cas b", "(sauf le cas a et le cas b)"),
|
||||
("libre - le texte", "libre — le texte"),
|
||||
("- une liste", "* une liste"),
|
||||
("clé DsEx1pS33abc", "clé dSeX1Ps33abc"),
|
||||
("les avoir vérifiés", "le savoir vérifiés"),
|
||||
] {
|
||||
assert_eq!(classify(old, new), EditKind::Substantive, "{old} → {new}");
|
||||
}
|
||||
assert_eq!(classify("toute latransmission", "toute la transmission"), EditKind::Typographic);
|
||||
assert_eq!(classify("la trans mission", "la transmission"), EditKind::Typographic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_figure_a_sign_or_a_relation_is_never_a_typo() {
|
||||
// Same letters, different meaning (independent reviews, 2026-10-21).
|
||||
@@ -369,13 +409,10 @@ mod tests {
|
||||
] {
|
||||
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("50 voix pour", "50 voix Pour"), EditKind::Typographic);
|
||||
// A bracket always counts, even as a list marker: conservative.
|
||||
assert_eq!(classify("1. premier point", "1) premier point"), EditKind::Substantive);
|
||||
assert_eq!(classify("1. premier point", "1. premier point."), EditKind::Typographic);
|
||||
assert_eq!(classify("libre - le texte", "libre — le texte"), EditKind::Typographic);
|
||||
assert_eq!(classify("1. premier point", "1. Premier point"), EditKind::Typographic);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -333,13 +333,25 @@ pub async fn edit(
|
||||
if !(is_author || p.origin == "seed" || p.status == "in_force") {
|
||||
return Err(AppError::Forbidden);
|
||||
}
|
||||
sqlx::query("UPDATE proposals SET title = ?, body = ?, updated_at = ? WHERE id = ?")
|
||||
.bind(title)
|
||||
.bind(body)
|
||||
.bind(&now)
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
// Written only over the text that was judged: an entry into force or another
|
||||
// correction in between makes it a conflict, never a silent change.
|
||||
let done = sqlx::query(
|
||||
"UPDATE proposals SET title = ?, body = ?, updated_at = ?
|
||||
WHERE id = ? AND revision = ? AND status = ? AND title = ? AND body = ?",
|
||||
)
|
||||
.bind(title)
|
||||
.bind(body)
|
||||
.bind(&now)
|
||||
.bind(&id)
|
||||
.bind(p.revision)
|
||||
.bind(&p.status)
|
||||
.bind(&p.title)
|
||||
.bind(&p.body)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
if done.rows_affected() != 1 {
|
||||
return Err(AppError::Conflict("changed"));
|
||||
}
|
||||
record_revision(&state, &id, p.revision, "typo", title, body, Some(&user.id)).await?;
|
||||
// A typo fix does not change the meaning: up-to-date translations stay up to date.
|
||||
sqlx::query("UPDATE translations SET source_hash = ? WHERE proposal_id = ? AND source_hash = ?")
|
||||
@@ -361,14 +373,25 @@ pub async fn edit(
|
||||
return Err(AppError::Conflict("substantive_not_author"));
|
||||
}
|
||||
let revision = p.revision + 1;
|
||||
sqlx::query("UPDATE proposals SET title = ?, body = ?, revision = ?, updated_at = ? WHERE id = ?")
|
||||
.bind(title)
|
||||
.bind(body)
|
||||
.bind(revision)
|
||||
.bind(&now)
|
||||
.bind(&id)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
// Only on the open proposal that was read: never on a text that entered into
|
||||
// force in the meantime.
|
||||
let done = sqlx::query(
|
||||
"UPDATE proposals SET title = ?, body = ?, revision = ?, updated_at = ?
|
||||
WHERE id = ? AND revision = ? AND status = 'open' AND title = ? AND body = ?",
|
||||
)
|
||||
.bind(title)
|
||||
.bind(body)
|
||||
.bind(revision)
|
||||
.bind(&now)
|
||||
.bind(&id)
|
||||
.bind(p.revision)
|
||||
.bind(&p.title)
|
||||
.bind(&p.body)
|
||||
.execute(&state.db)
|
||||
.await?;
|
||||
if done.rows_affected() != 1 {
|
||||
return Err(AppError::Conflict("changed"));
|
||||
}
|
||||
record_revision(&state, &id, revision, "substantive", title, body, Some(&user.id)).await?;
|
||||
// Signals were about the former text: they lapse, their authors may signal again.
|
||||
sqlx::query(
|
||||
|
||||
@@ -1,28 +1,35 @@
|
||||
# 0003 — Corriger n'est pas voter
|
||||
|
||||
**Contexte.** « Ça ne peut pas devenir un système qui nécessite un vote pour corriger une
|
||||
coquille. » Mais une correction ne doit pas pouvoir changer le sens d'un texte voté.
|
||||
coquille. » Mais une correction ne doit pas pouvoir changer le sens d'un texte voté — et tout
|
||||
membre peut corriger un texte en vigueur, qui est republié aussitôt.
|
||||
|
||||
**Décision.** Le moteur classe chaque modification (`ld-engine/src/edit.rs`), toujours
|
||||
**contre le texte voté** (sa dernière révision initiale ou de fond) — jamais contre la dernière
|
||||
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
|
||||
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
|
||||
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
|
||||
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
|
||||
aux deux dernières (nombre, genre, temps, mode), jamais une lettre remplacée, jamais sur un mot
|
||||
de négation, de quantité ou de position ; elle s'applique sans vote,
|
||||
garde les votes et les traductions ;
|
||||
- les caractères invisibles de mise en forme (inversion du sens d'écriture, largeur nulle) sont
|
||||
refusés partout où l'on écrit un texte ;
|
||||
- **de fond** sinon : nouvelle révision d'une proposition ouverte (par son auteur), votes
|
||||
antérieurs « à reconfirmer », signalements levés ; refusée sur un texte en vigueur.
|
||||
L'éditeur montre le verdict avant d'enregistrer. Les brouillons s'éditent librement.
|
||||
**Décision.** Une coquille touche **les lettres et les espaces, rien d'autre**. Le moteur
|
||||
(`ld-engine/src/edit.rs`) la juge toujours **contre le texte voté** (dernière révision initiale
|
||||
ou de fond), jamais contre la correction précédente :
|
||||
- la **forme** du texte — tout ce qui n'est ni lettre latine ni espace, dans l'ordre, avec les
|
||||
espaces qui touchent un chiffre — reste identique (apostrophes et guillemets de tous styles
|
||||
comptent pour un même signe) ;
|
||||
- les adresses, les identifiants qui contiennent un chiffre et le contenu entre accents graves
|
||||
restent identiques à la lettre ;
|
||||
- les espaces ne font que coller ou couper des mots, jamais déplacer une frontière ;
|
||||
- au plus deux mots changent, 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 ni aux deux dernières
|
||||
lettres ; jamais une lettre remplacée ; jamais sur un mot de négation, de quantité ou de
|
||||
position) ;
|
||||
- les caractères invisibles de mise en forme sont refusés partout où l'on écrit.
|
||||
|
||||
**Coût.** Le classement est prudent : certaines vraies coquilles (« persone » → « personne »)
|
||||
passent pour du fond et demandent une proposition. On préfère ce sens d'erreur.
|
||||
Sinon c'est **du fond** : nouvelle révision d'une proposition ouverte (par son auteur), votes
|
||||
antérieurs « à reconfirmer », signalements levés ; refusé sur un texte en vigueur ou de départ.
|
||||
L'écriture ne vaut que sur le texte jugé : un texte entré en vigueur ou changé entre-temps
|
||||
donne un conflit. Les brouillons s'éditent librement.
|
||||
|
||||
**Historique.** La première version tolérait la ponctuation et deux mots « à deux lettres
|
||||
près ». Six relectures indépendantes (2026-10-21) ont trouvé, l'une après l'autre, des
|
||||
coquilles qui changeaient le sens : mots retournés (« valide » → « invalide »), caractères
|
||||
invisibles, sosies de chiffres, corrections enchaînées, adresse typosquattée, signe moins
|
||||
devant un nombre en gras. D'où le passage d'une liste noire à cette règle.
|
||||
|
||||
**Coût.** Une ponctuation oubliée, une emphase, un pluriel mal accordé demandent une
|
||||
proposition (ou une révision par l'auteur). On préfère ce sens d'erreur : un texte de référence
|
||||
publié vaut plus qu'un point final.
|
||||
|
||||
@@ -74,28 +74,44 @@ l'inertie du cran au-dessus** (« inertie + ») : alourdir ou alléger demande p
|
||||
|
||||
### Corriger n'est pas voter
|
||||
|
||||
**Une coquille touche les lettres et les espaces, rien d'autre.** Accents, majuscules, le style
|
||||
des apostrophes et des guillemets, un mot collé ou coupé (« latransmission » → « la
|
||||
transmission »), ou au plus deux mots mal tapés — chacun un seul glissement de clavier au cœur du
|
||||
mot (« certifcation » → « certification »). Tout le reste passe par une proposition : la
|
||||
ponctuation, les chiffres, les signes, l'emphase, les formules, les adresses, et tout changement
|
||||
de sens.
|
||||
|
||||
| Geste | Qui | Effet sur les votes |
|
||||
|---|---|---|
|
||||
| Corriger une **coquille** (accents, casse, ponctuation, espaces ; ou jusqu'à 2 mots mal tapés, une lettre ajoutée, retirée ou inversée chacun, au cœur du mot) — mesurée contre le texte **voté**, jamais contre la dernière correction | tout membre sur un texte de départ ou en vigueur ; l'auteur sur sa proposition | aucun — les votes restent, les traductions restent à jour |
|
||||
| Corriger une **coquille** | tout membre sur un texte de départ ou en vigueur ; l'auteur sur sa proposition | aucun — les votes restent, les traductions restent à jour |
|
||||
| Modifier **le fond** d'une proposition | son auteur | nouvelle révision : les votes antérieurs sont « à reconfirmer », les signalements tombent |
|
||||
| Modifier le fond d'un texte **en vigueur** ou d'un texte de départ | personne | refusé : on propose une nouvelle version |
|
||||
|
||||
Le classement est prudent — quatre relectures indépendantes l'ont éprouvé, et il fonctionne
|
||||
**par liste blanche** : une coquille ne peut toucher que des lettres latines, des espaces et la
|
||||
ponctuation ordinaire. Tout le reste compte comme le sens et ne bouge pas : chiffres (de toute
|
||||
écriture), signes, relations, parenthèses, sosies (« ‚ » pour une virgule, « ⁄ » pour une barre,
|
||||
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,
|
||||
liens, courriels), **tout ce qui touche un chiffre**, espaces et ponctuation comprises (« 50 » →
|
||||
« 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
|
||||
(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
|
||||
(ne, jamais, deux, majorité, tous, dessus, avant…). Chaque correction est mesurée contre le
|
||||
**texte voté** : les glissements ne s'additionnent pas d'une correction à l'autre. Les caractères
|
||||
invisibles (inversion du sens d'écriture, largeur nulle) sont refusés partout. Moteur : `crates/ld-engine/src/edit.rs`.
|
||||
Pourquoi si strict : six relectures indépendantes ont montré qu'une ponctuation laissée libre
|
||||
finit toujours par porter un sens — un signe moins devant un nombre en gras, deux conditions
|
||||
d'une liste fusionnées en une, une virgule glissée dans « 50 », une parenthèse déplacée qui
|
||||
change la portée d'une exception. La règle est donc construite à l'envers d'une liste noire :
|
||||
|
||||
- la **forme** du texte (tout ce qui n'est ni lettre latine ni espace, dans l'ordre : ponctuation,
|
||||
chiffres de toute écriture, signes, emphase, parenthèses, sosies, marques superposées, autres
|
||||
alphabets) ne bouge pas ; une espace à côté d'un chiffre en fait partie (« 1 5 » n'est pas
|
||||
« 15 ») ; les apostrophes et guillemets de tous styles comptent pour un même signe ;
|
||||
- les **adresses** (liens, domaines, courriels) et les **identifiants qui contiennent un
|
||||
chiffre** (clés publiques, références) sont comparés à l'identique, casse comprise ; le contenu
|
||||
entre accents graves aussi ;
|
||||
- les espaces ne font que **coller ou couper** des mots, jamais déplacer une frontière (« les
|
||||
avoir » → « le savoir » est du fond) ;
|
||||
- un **glissement** = une lettre ajoutée, retirée, ou deux voisines inversées, jamais aux deux
|
||||
premières ni aux deux dernières lettres (préfixes « valide » → « invalide », terminaisons
|
||||
« certifierai » → « certifierais »), jamais une lettre remplacée (« deux » → « dix »), jamais sur
|
||||
un mot de négation, de quantité ou de position (ne, jamais, deux, majorité, tous, dessus…) ;
|
||||
- chaque correction est mesurée contre le **texte voté**, jamais contre la correction
|
||||
précédente : les glissements ne s'additionnent pas ;
|
||||
- les caractères invisibles (inversion du sens d'écriture, largeur nulle) sont refusés partout ;
|
||||
- une correction ne s'écrit que sur le texte qui a été jugé : si le texte est entré en vigueur
|
||||
ou a changé entre-temps, c'est un conflit (« recharge, puis recommence »).
|
||||
|
||||
Moteur : `crates/ld-engine/src/edit.rs` ; règle d'écriture : `crates/ld-server/src/proposals.rs`.
|
||||
|
||||
### Deux articles qui se contredisent
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ export const en: Messages = {
|
||||
'history.by': 'by {name}',
|
||||
|
||||
'typo.title': 'Fix a typo',
|
||||
'typo.note': 'Accents, punctuation, typos: the fix applies without a vote, with its author and its diff. A change in meaning goes through a proposal.',
|
||||
'typo.note': 'Accents, capitals, typos, words run together or split: the fix applies without a vote, with its author and its diff. Punctuation, figures, formulas, addresses and any change in meaning go through a proposal.',
|
||||
'typo.save': 'Save the fix',
|
||||
'typo.done': 'Typo fixed, votes untouched.',
|
||||
'typo.substantive': 'This edit affects the meaning: propose it as a change.',
|
||||
@@ -312,6 +312,7 @@ export const en: Messages = {
|
||||
'error.invitation_invalid': 'This invitation link is no longer valid.',
|
||||
'error.identity_kind': 'A new link can only be for an invited person, never a Ğ1 identity.',
|
||||
'error.hidden_characters': 'The text contains invisible characters (writing-direction overrides, zero-width spaces): remove them.',
|
||||
'error.changed': 'The text has changed in the meantime: reload it, then try again.',
|
||||
'error.wot_members_only': 'Here, only members of the Ğ1 web of trust vote.',
|
||||
'error.retry': 'Try again',
|
||||
|
||||
|
||||
@@ -198,7 +198,7 @@ export const es: Messages = {
|
||||
'history.by': 'por {name}',
|
||||
|
||||
'typo.title': 'Corregir una errata',
|
||||
'typo.note': 'Acentos, puntuación, erratas: la corrección se aplica sin votación, con su autor y su diff. Un cambio de sentido pasa por una propuesta.',
|
||||
'typo.note': 'Acentos, mayúsculas, erratas, palabras pegadas o separadas: la corrección se aplica sin votación, con su autor y su diff. La puntuación, las cifras, las fórmulas, las direcciones y todo cambio de sentido pasan por una propuesta.',
|
||||
'typo.save': 'Guardar la corrección',
|
||||
'typo.done': 'Errata corregida, sin tocar los votos.',
|
||||
'typo.substantive': 'Este cambio afecta al sentido: proponlo como modificación.',
|
||||
@@ -312,6 +312,7 @@ export const es: Messages = {
|
||||
'error.invitation_invalid': 'Este enlace de invitación ya no es válido.',
|
||||
'error.identity_kind': 'Un nuevo enlace solo puede ser para una persona invitada, nunca para una identidad Ğ1.',
|
||||
'error.hidden_characters': 'El texto contiene caracteres invisibles (inversión del sentido de escritura, espacios de ancho cero): quítalos.',
|
||||
'error.changed': 'El texto ha cambiado mientras tanto: vuelve a cargarlo y empieza de nuevo.',
|
||||
'error.wot_members_only': 'Aquí solo votan los miembros de la red de confianza Ğ1.',
|
||||
'error.retry': 'Reintentar',
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ export const fr = {
|
||||
'history.by': 'par {name}',
|
||||
|
||||
'typo.title': 'Corriger une coquille',
|
||||
'typo.note': "Accents, ponctuation, faute de frappe : la correction s'applique sans vote, avec son auteur et son diff. Un changement de sens passe par une proposition.",
|
||||
'typo.note': "Accents, majuscules, faute de frappe, mot collé ou coupé : la correction s'applique sans vote, avec son auteur et son diff. La ponctuation, les chiffres, les formules, les adresses et tout changement de sens passent par une proposition.",
|
||||
'typo.save': 'Enregistrer la correction',
|
||||
'typo.done': 'Coquille corrigée, sans toucher aux votes.',
|
||||
'typo.substantive': 'Ce changement touche au sens : propose-le comme modification.',
|
||||
@@ -315,6 +315,7 @@ export const fr = {
|
||||
'error.invitation_invalid': "Ce lien d'invitation n'est plus valable.",
|
||||
'error.identity_kind': "Un nouveau lien ne peut viser qu'une personne invitée, jamais une identité Ğ1.",
|
||||
'error.hidden_characters': 'Le texte contient des caractères invisibles (inversion du sens d\'écriture, espaces de largeur nulle) : retire-les.',
|
||||
'error.changed': 'Le texte a changé entre-temps : recharge-le, puis recommence.',
|
||||
'error.wot_members_only': 'Ici, seuls les membres de la toile de confiance Ğ1 votent.',
|
||||
'error.retry': 'Réessayer',
|
||||
|
||||
|
||||
Reference in New Issue
Block a user