v3 : septième relecture — collages limités, titre jugé à part, découpage contrôlé
- Mots collés ou coupés : deux au plus, jamais sur une négation (« Si non » → « Sinon », « ne certifier » → « necertifier » sont du fond). - Titre et corps jugés chacun pour soi, puis ensemble : le titre n'absorbe plus le début du corps. - Caractères invisibles refusés aussi au découpage d'un brouillon et à la soumission. - Tabulation et retour chariot ne sont plus des espaces libres ; les lettres latines rares (ǃ, Ʒ…) font partie de la forme. - Le diff renvoyé après une coquille couvre le titre. Boucle : 62 tests Rust, svelte-check, 9 vitest, e2e 12/12. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+17
-9
@@ -4,10 +4,13 @@
|
|||||||
|
|
||||||
## Où on en est
|
## Où on en est
|
||||||
|
|
||||||
**Chantier 1 — Acte d'engagement Certification** : construit, boucle verte, prêt à livrer.
|
**Chantier 1 — Acte d'engagement Certification** : **en ligne** sur decision.librodrome.org
|
||||||
|
depuis le 2026-10-21 (version `fa8e8dc`, pipeline Woodpecker n° 30). La maquette part sur
|
||||||
|
maquette.decision.librodrome.org. Sur la branche du chantier, les trois points laissés par la
|
||||||
|
septième relecture sont corrigés (pas encore en ligne).
|
||||||
|
|
||||||
- v3 Rust + Svelte à la racine du dépôt ; la maquette v2 intacte dans `maquette/`.
|
- v3 Rust + Svelte à la racine du dépôt ; la maquette v2 intacte dans `maquette/`.
|
||||||
- Boucle : Rust (fmt, clippy `-D warnings`, 60 tests dont 14 scénarios d'API et la
|
- Boucle : Rust (fmt, clippy `-D warnings`, 62 tests dont 14 scénarios d'API et la
|
||||||
publication vers un dépôt nu) · interface (svelte-check 0 avertissement, 9 vitest) ·
|
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 :
|
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
|
santé, interface, volume non-root, clé de publication, lien admin initial (0600, absent
|
||||||
@@ -17,7 +20,8 @@
|
|||||||
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`, puis quatre « à reprendre » (`ac9c904`, `9203fd2`, `1eac6f8`, `6c0754a`),
|
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
|
toutes sur la même question : une « coquille » (sans vote, publiée) qui change un sens, puis
|
||||||
|
`fa8e8dc : ok` (mis en ligne). La
|
||||||
règle est désormais simple et explicable : **une coquille touche les lettres et les espaces,
|
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
|
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
|
conteneurs en `restart: always`, vote réservé aux membres de la toile dans Monnaie libre (une
|
||||||
@@ -29,12 +33,16 @@
|
|||||||
## Ce qui attend Yvv
|
## Ce qui attend Yvv
|
||||||
|
|
||||||
1. **Envoyer la demande à poka** : `docs/chantiers/acte-certification/DEMANDE-CONNECT.md`.
|
1. **Envoyer la demande à poka** : `docs/chantiers/acte-certification/DEMANDE-CONNECT.md`.
|
||||||
2. **Créer le dépôt des textes** sur git.duniter.org : `Yvv/libredecision-textes`, **public**,
|
2. **Créer le dépôt des textes**, public et vide : `Yvv/libredecision-textes` sur
|
||||||
vide ; y ajouter la **clé de déploiement** du serveur, **en écriture** (sa partie publique
|
git.duniter.org (choix du formulaire, « là où regardent Cesium et Ğecko ») — question rouverte
|
||||||
s'affiche dans trustinWallet, `/acces`, section Publication). Le serveur fait lui-même le
|
le 2026-10-21 : git.open.us.org à la place ? (une ligne, `LD_PUBLISH_REMOTE` dans
|
||||||
premier envoi.
|
`docker/app-compose.yml`). Y ajouter la **clé de déploiement** du serveur, **en écriture** (sa
|
||||||
3. **Ouvrir trustinWallet** avec le lien administrateur initial (remis hors conversation),
|
partie publique s'affiche dans trustinWallet, `/acces`, section Publication). Le serveur
|
||||||
puis créer l'**invitation de Chiara** (groupes Monnaie libre + groupe d'essai).
|
réessaie toutes les 60 s et fait lui-même le premier envoi.
|
||||||
|
3. **Ouvrir trustinWallet** avec le lien administrateur initial (remis hors conversation, un
|
||||||
|
seul usage : il crée l'identité administratrice), puis créer l'**invitation de Chiara**
|
||||||
|
(groupes Monnaie libre + groupe d'essai). Question ouverte : un « lien d'équipe »
|
||||||
|
réutilisable (une identité par ouverture, jamais l'administration) ?
|
||||||
4. Donner à Chiara un compte **git.open.us.org** collaboratrice de `yvv/decision`.
|
4. Donner à Chiara un compte **git.open.us.org** collaboratrice de `yvv/decision`.
|
||||||
5. Décider : la **licence** du code ; le texte F1.2 du seed (annonce C = 0 et 30 jours).
|
5. Décider : la **licence** du code ; le texte F1.2 du seed (annonce C = 0 et 30 jours).
|
||||||
|
|
||||||
|
|||||||
@@ -80,15 +80,18 @@ fn skeleton(s: &str) -> String {
|
|||||||
fold(s).chars().filter(|c| c.is_alphanumeric()).collect()
|
fold(s).chars().filter(|c| c.is_alphanumeric()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A letter of the Latin alphabet, precomposed accents included (after NFC):
|
/// A letter of the Latin alphabet as French, English and Spanish write it,
|
||||||
/// the only letters a typo fix may touch. Any other script may hold look-alikes
|
/// precomposed accents included (after NFC, Latin-1 and Latin Extended-A): the
|
||||||
/// (Cyrillic « а ») or reorder the display (Hebrew, Arabic).
|
/// only letters a typo fix may touch. Any other letter may be a look-alike
|
||||||
|
/// (Cyrillic « а », « ǃ » for « ! », « Ʒ » for « 3 ») or reorder the display
|
||||||
|
/// (Hebrew, Arabic): it is kept as it is.
|
||||||
fn is_latin_letter(c: char) -> bool {
|
fn is_latin_letter(c: char) -> bool {
|
||||||
c.is_alphabetic() && matches!(c as u32, 0x41..=0x5A | 0x61..=0x7A | 0xC0..=0x24F | 0x1E00..=0x1EFF)
|
c.is_alphabetic() && matches!(c as u32, 0x41..=0x5A | 0x61..=0x7A | 0xC0..=0x17F)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The only spaces free to change; any other (hair, thin, ogham…) is kept as it is.
|
/// The only spaces free to change; any other (carriage return, tab, hair,
|
||||||
const FREE_SPACES: &[char] = &[' ', '\u{a0}', '\u{202f}', '\n', '\r', '\t'];
|
/// thin, ogham…) is kept as it is.
|
||||||
|
const FREE_SPACES: &[char] = &[' ', '\u{a0}', '\u{202f}', '\n'];
|
||||||
|
|
||||||
/// Invisible format characters (zero-width, bidirectional overrides and
|
/// Invisible format characters (zero-width, bidirectional overrides and
|
||||||
/// isolates, byte order mark): they can make "12" display as "21" ("Trojan
|
/// isolates, byte order mark): they can make "12" display as "21" ("Trojan
|
||||||
@@ -196,20 +199,28 @@ fn words(s: &str) -> Vec<String> {
|
|||||||
fold(s).split(|c: char| !c.is_alphanumeric()).filter(|w| !w.is_empty()).map(str::to_owned).collect()
|
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
|
/// Same letters, at most two words glued or split, never a negation:
|
||||||
/// transmission ». A boundary moved is not a typo (« les avoir » → « le savoir »).
|
/// « 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 {
|
fn only_glued_or_split(a: &[String], b: &[String]) -> bool {
|
||||||
let (mut i, mut j) = (0, 0);
|
let joined = |x: &String, y: &String| !NEGATIONS.contains(&x.as_str()) && !NEGATIONS.contains(&y.as_str());
|
||||||
|
let (mut i, mut j, mut moves) = (0, 0, 0);
|
||||||
while i < a.len() && j < b.len() {
|
while i < a.len() && j < b.len() {
|
||||||
if a[i] == b[j] {
|
if a[i] == b[j] {
|
||||||
(i, j) = (i + 1, j + 1);
|
(i, j) = (i + 1, j + 1);
|
||||||
} else if j + 1 < b.len() && a[i] == format!("{}{}", b[j], b[j + 1]) {
|
continue;
|
||||||
|
}
|
||||||
|
if j + 1 < b.len() && a[i] == format!("{}{}", b[j], b[j + 1]) && joined(&b[j], &b[j + 1]) {
|
||||||
(i, j) = (i + 1, j + 2);
|
(i, j) = (i + 1, j + 2);
|
||||||
} else if i + 1 < a.len() && b[j] == format!("{}{}", a[i], a[i + 1]) {
|
} else if i + 1 < a.len() && b[j] == format!("{}{}", a[i], a[i + 1]) && joined(&a[i], &a[i + 1]) {
|
||||||
(i, j) = (i + 2, j + 1);
|
(i, j) = (i + 2, j + 1);
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
moves += 1;
|
||||||
|
if moves > MAX_TYPO_WORDS {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
i == a.len() && j == b.len()
|
i == a.len() && j == b.len()
|
||||||
}
|
}
|
||||||
@@ -361,6 +372,32 @@ mod tests {
|
|||||||
assert_eq!(classify(old, new), EditKind::Typographic);
|
assert_eq!(classify(old, new), EditKind::Typographic);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn glued_or_split_words_stay_few_and_never_touch_a_negation() {
|
||||||
|
// Seventh independent review, 2026-10-21: a text in force disfigured at will.
|
||||||
|
for (old, new) in [
|
||||||
|
("à ne certifier que", "à necertifier que"),
|
||||||
|
("à ne certifier que", "àne certifier que"),
|
||||||
|
("Si non, la demande attend", "Sinon, la demande attend"),
|
||||||
|
("la transmission de ce document à chacun", "latransmission dece documentà chacun"),
|
||||||
|
] {
|
||||||
|
assert_eq!(classify(old, new), EditKind::Substantive, "{old} → {new}");
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
classify("de latransmission dece document", "de la transmission de ce document"),
|
||||||
|
EditKind::Typographic
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn odd_spaces_and_look_alike_letters_are_kept() {
|
||||||
|
assert_eq!(classify("un texte\tde référence", "un texte de référence"), EditKind::Substantive);
|
||||||
|
assert_eq!(classify("un texte\rde référence", "un texte de référence"), EditKind::Substantive);
|
||||||
|
assert_eq!(classify("toute certification", "toute certiǃfication"), EditKind::Substantive);
|
||||||
|
assert_eq!(classify("toute certification", "toute certifiƷcation"), EditKind::Substantive);
|
||||||
|
assert_eq!(classify("le coeur de l'ŒUVRE, niño", "le Coeur de l'œuvre, Niño"), EditKind::Typographic);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accents_case_apostrophes_quotes() {
|
fn accents_case_apostrophes_quotes() {
|
||||||
assert_eq!(classify("Je m'engage a verifier.", "Je m’engage à vérifier."), EditKind::Typographic);
|
assert_eq!(classify("Je m'engage a verifier.", "Je m’engage à vérifier."), EditKind::Typographic);
|
||||||
|
|||||||
@@ -316,15 +316,20 @@ pub async fn edit(
|
|||||||
}
|
}
|
||||||
// Measured against the text that was voted, not the current one: successive
|
// Measured against the text that was voted, not the current one: successive
|
||||||
// typo fixes are judged together and never add up to a change of meaning.
|
// typo fixes are judged together and never add up to a change of meaning.
|
||||||
|
// Title and body are judged apart (a title never absorbs the start of the
|
||||||
|
// body) and together (two slips at most in all).
|
||||||
let next = format!("{title}\n{body}");
|
let next = format!("{title}\n{body}");
|
||||||
let kind = if next == format!("{}\n{}", p.title, p.body) {
|
let kind = if title == p.title && body == p.body {
|
||||||
EditKind::Unchanged
|
EditKind::Unchanged
|
||||||
} else {
|
} else {
|
||||||
let (voted_title, voted_body) = voted_text(&state, &p).await?;
|
let (voted_title, voted_body) = voted_text(&state, &p).await?;
|
||||||
match edit::classify(&format!("{voted_title}\n{voted_body}"), &next) {
|
let kinds = [
|
||||||
EditKind::Unchanged => EditKind::Typographic, // back to the voted text
|
edit::classify(&voted_title, title),
|
||||||
k => k,
|
edit::classify(&voted_body, body),
|
||||||
}
|
edit::classify(&format!("{voted_title}\n{voted_body}"), &next),
|
||||||
|
];
|
||||||
|
// All unchanged: back to the voted text, itself a correction.
|
||||||
|
if kinds.contains(&EditKind::Substantive) { EditKind::Substantive } else { EditKind::Typographic }
|
||||||
};
|
};
|
||||||
match kind {
|
match kind {
|
||||||
EditKind::Unchanged => Ok(Json(json!({ "edit": "unchanged" }))),
|
EditKind::Unchanged => Ok(Json(json!({ "edit": "unchanged" }))),
|
||||||
@@ -363,7 +368,7 @@ pub async fn edit(
|
|||||||
if p.status == "in_force" {
|
if p.status == "in_force" {
|
||||||
adoption::enqueue_publication(&state, &st.doc, "correction typographique").await?;
|
adoption::enqueue_publication(&state, &st.doc, "correction typographique").await?;
|
||||||
}
|
}
|
||||||
Ok(Json(json!({ "edit": "typo", "diff": edit::diff(&p.body, body) })))
|
Ok(Json(json!({ "edit": "typo", "diff": edit::diff(&format!("{}\n{}", p.title, p.body), &next) })))
|
||||||
}
|
}
|
||||||
EditKind::Substantive => {
|
EditKind::Substantive => {
|
||||||
if p.status == "in_force" {
|
if p.status == "in_force" {
|
||||||
@@ -418,6 +423,9 @@ pub async fn submit(
|
|||||||
if p.status != "draft" {
|
if p.status != "draft" {
|
||||||
return Err(AppError::Conflict("not_draft"));
|
return Err(AppError::Conflict("not_draft"));
|
||||||
}
|
}
|
||||||
|
if edit::has_hidden_controls(&p.title) || edit::has_hidden_controls(&p.body) {
|
||||||
|
return Err(AppError::Invalid("hidden_characters"));
|
||||||
|
}
|
||||||
let now = util::now();
|
let now = util::now();
|
||||||
sqlx::query("UPDATE proposals SET status = 'open', submitted_at = ?, updated_at = ? WHERE id = ?")
|
sqlx::query("UPDATE proposals SET status = 'open', submitted_at = ?, updated_at = ? WHERE id = ?")
|
||||||
.bind(&now)
|
.bind(&now)
|
||||||
@@ -484,6 +492,9 @@ pub async fn split(
|
|||||||
if parts.len() < 2 || parts.len() > 12 || parts.iter().any(|s| s.chars().count() > MAX_BODY) {
|
if parts.len() < 2 || parts.len() > 12 || parts.iter().any(|s| s.chars().count() > MAX_BODY) {
|
||||||
return Err(AppError::Invalid("parts"));
|
return Err(AppError::Invalid("parts"));
|
||||||
}
|
}
|
||||||
|
if parts.iter().any(|s| edit::has_hidden_controls(s)) {
|
||||||
|
return Err(AppError::Invalid("hidden_characters"));
|
||||||
|
}
|
||||||
sqlx::query("UPDATE proposals SET body = ?, updated_at = ? WHERE id = ?")
|
sqlx::query("UPDATE proposals SET body = ?, updated_at = ? WHERE id = ?")
|
||||||
.bind(parts[0])
|
.bind(parts[0])
|
||||||
.bind(util::now())
|
.bind(util::now())
|
||||||
|
|||||||
@@ -251,6 +251,17 @@ async fn editing_is_not_voting() {
|
|||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("hidden_characters")));
|
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("hidden_characters")));
|
||||||
|
// The title never absorbs the start of the body.
|
||||||
|
let title = e2["title"].as_str().expect("title");
|
||||||
|
let (head, rest) = body.split_once(' ').expect("two words");
|
||||||
|
let (s, v) = b
|
||||||
|
.call(
|
||||||
|
"PATCH",
|
||||||
|
&format!("/api/proposals/{e2_id}"),
|
||||||
|
Some(json!({ "title": format!("{title} {head}"), "body": rest })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!((s, v["error"].as_str()), (StatusCode::CONFLICT, Some("substantive_in_force")), "{v}");
|
||||||
// Typo fixes are measured against the voted text: slips never add up.
|
// Typo fixes are measured against the voted text: slips never add up.
|
||||||
let patch = |body: String| json!({ "title": e2["title"], "body": body });
|
let patch = |body: String| json!({ "title": e2["title"], "body": body });
|
||||||
let (_, v) = b
|
let (_, v) = b
|
||||||
@@ -396,6 +407,14 @@ async fn split_a_draft() {
|
|||||||
.call("POST", &format!("/api/docs/{id}/proposals"), Some(json!({ "title": "Deux choses", "body": "a\n\nb" })))
|
.call("POST", &format!("/api/docs/{id}/proposals"), Some(json!({ "title": "Deux choses", "body": "a\n\nb" })))
|
||||||
.await;
|
.await;
|
||||||
let p = v["id"].as_str().expect("id").to_owned();
|
let p = v["id"].as_str().expect("id").to_owned();
|
||||||
|
let (s, v) = a
|
||||||
|
.call(
|
||||||
|
"POST",
|
||||||
|
&format!("/api/proposals/{p}/split"),
|
||||||
|
Some(json!({ "parts": ["Première.", "Au \u{202e}01\u{202c}."] })),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
assert_eq!((s, v["error"].as_str()), (StatusCode::BAD_REQUEST, Some("hidden_characters")));
|
||||||
let (s, v) =
|
let (s, v) =
|
||||||
a.call("POST", &format!("/api/proposals/{p}/split"), Some(json!({ "parts": ["Première.", "Seconde."] }))).await;
|
a.call("POST", &format!("/api/proposals/{p}/split"), Some(json!({ "parts": ["Première.", "Seconde."] }))).await;
|
||||||
assert_eq!(s, StatusCode::OK, "{v}");
|
assert_eq!(s, StatusCode::OK, "{v}");
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ ou de fond), jamais contre la correction précédente :
|
|||||||
comptent pour un même signe) ;
|
comptent pour un même signe) ;
|
||||||
- les adresses, les identifiants qui contiennent un chiffre et le contenu entre accents graves
|
- les adresses, les identifiants qui contiennent un chiffre et le contenu entre accents graves
|
||||||
restent identiques à la lettre ;
|
restent identiques à la lettre ;
|
||||||
- les espaces ne font que coller ou couper des mots, jamais déplacer une frontière ;
|
- les espaces (espace, insécable, fine insécable, saut de ligne) ne font que coller ou couper
|
||||||
|
des mots, deux au plus, jamais sur une négation, jamais en déplaçant une frontière ;
|
||||||
|
- le titre et le corps sont jugés chacun pour soi, puis ensemble ;
|
||||||
- au plus deux mots changent, chacun d'un seul glissement de clavier au cœur du mot (une lettre
|
- 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
|
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
|
lettres ; jamais une lettre remplacée ; jamais sur un mot de négation, de quantité ou de
|
||||||
@@ -28,7 +30,11 @@ donne un conflit. Les brouillons s'éditent librement.
|
|||||||
près ». Six relectures indépendantes (2026-10-21) ont trouvé, l'une après l'autre, des
|
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
|
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
|
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.
|
devant un nombre en gras. D'où le passage d'une liste noire à cette règle. La septième
|
||||||
|
(`fa8e8dc : ok`) a laissé trois points, corrigés aussitôt : mots collés ou coupés sans limite
|
||||||
|
(« Si non » → « Sinon »), titre jugé avec le corps (le titre en absorbait le début), caractères
|
||||||
|
invisibles non contrôlés au découpage d'un brouillon ; et deux remarques : tabulation et retour
|
||||||
|
chariot ne sont plus des espaces libres, les lettres latines rares font partie de la forme.
|
||||||
|
|
||||||
**Coût.** Une ponctuation oubliée, une emphase, un pluriel mal accordé demandent une
|
**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
|
proposition (ou une révision par l'auteur). On préfère ce sens d'erreur : un texte de référence
|
||||||
|
|||||||
@@ -95,12 +95,19 @@ change la portée d'une exception. La règle est donc construite à l'envers d'u
|
|||||||
- la **forme** du texte (tout ce qui n'est ni lettre latine ni espace, dans l'ordre : ponctuation,
|
- 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
|
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
|
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 ;
|
« 15 ») ; les apostrophes et guillemets de tous styles comptent pour un même signe ; « lettre
|
||||||
|
latine » s'entend de celles du français, de l'anglais et de l'espagnol (les lettres latines
|
||||||
|
rares qui imitent un signe, « ǃ » ou « Ʒ », font partie de la forme) ; « espace » s'entend de
|
||||||
|
l'espace, de l'insécable, de la fine insécable et du saut de ligne (ni tabulation ni retour
|
||||||
|
chariot) ;
|
||||||
- les **adresses** (liens, domaines, courriels) et les **identifiants qui contiennent un
|
- 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
|
chiffre** (clés publiques, références) sont comparés à l'identique, casse comprise ; le contenu
|
||||||
entre accents graves aussi ;
|
entre accents graves aussi ;
|
||||||
- les espaces ne font que **coller ou couper** des mots, jamais déplacer une frontière (« les
|
- les espaces ne font que **coller ou couper** des mots, deux au plus, jamais sur un mot de
|
||||||
avoir » → « le savoir » est du fond) ;
|
négation (« Si non » → « Sinon », « ne certifier » → « necertifier » sont du fond), et jamais
|
||||||
|
déplacer une frontière (« les avoir » → « le savoir » est du fond) ;
|
||||||
|
- le **titre** et le **corps** sont jugés chacun pour soi (un titre n'absorbe pas le début du
|
||||||
|
corps), puis ensemble (deux mots au plus en tout) ;
|
||||||
- un **glissement** = une lettre ajoutée, retirée, ou deux voisines inversées, jamais aux deux
|
- 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
|
premières ni aux deux dernières lettres (préfixes « valide » → « invalide », terminaisons
|
||||||
« certifierai » → « certifierais »), jamais une lettre remplacée (« deux » → « dix »), jamais sur
|
« certifierai » → « certifierais »), jamais une lettre remplacée (« deux » → « dix »), jamais sur
|
||||||
|
|||||||
@@ -42,3 +42,4 @@ page, la langue et la taille de l'écran. Les retours se traitent dans trustinWa
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 2026-10-21 | automate | local, serveur jetable :8092 | C1–C10, M1 | 11 / 11 verts |
|
| 2026-10-21 | automate | local, serveur jetable :8092 | C1–C10, M1 | 11 / 11 verts |
|
||||||
| 2026-10-21 | automate | local, après relecture indépendante | C1–C11, M1 | 12 / 12 verts |
|
| 2026-10-21 | automate | local, après relecture indépendante | C1–C11, M1 | 12 / 12 verts |
|
||||||
|
| 2026-10-21 | automate | local, après la septième relecture (corrections) | C1–C11, M1 | 12 / 12 verts |
|
||||||
|
|||||||
Reference in New Issue
Block a user