From fa911fe94fb266646666ac470f2b8fcf22fab6ba Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 06:26:36 +0000 Subject: [PATCH 1/2] feat(web): show every elementary piece of a chained word in the cut verdict A word whose part is itself a composed word is cut in one go (etymologiste: etymo | logie | iste); the verdict and the lesson now colour those pieces instead of the first-level parts only. An elided part is clipped to its segment, as the content compiler does. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01BkYb6EamApcbLXBiXShSgV --- docs/roadmap.md | 1 + web/src/services/ricochets.test.ts | 44 +++++++++++++++++++++++++++++- web/src/services/ricochets.ts | 33 ++++++++++++++++++++-- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/docs/roadmap.md b/docs/roadmap.md index 2c4173c..212392d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -91,6 +91,7 @@ Reportés : file d’attente des fusions hors connexion (supprimée, ADR 0041), | Ricochets : format « assembler » ([ADR 0067](https://github.com/lunik/EtymoLogique/blob/master/docs/adr/0067-ricochets-assembler-un-mot-depuis-sa-definition.md)) : `FormatRicochet::Assembler`, réponse `Briques`, colonnes tirées par `colonnes_assemblage`, manifeste `ricochets` (`position`, `assemblage`), composant `Assembler` ; essai local, tests à compléter côté PWA | fait | | Ricochets, PWA : moins de touchers ([ADR 0068](https://github.com/lunik/EtymoLogique/blob/master/docs/adr/0068-ricochets-moins-de-touchers-pour-decouper-et-assembler.md)) : la lame de découpe arrive, et repart après chaque coupe, sur une jointure tirée au hasard qui n’est jamais une coupe attendue ; *Assembler* envoie la réponse à la seconde brique posée, sans « Valider » ; tests du composant `Assembler` | fait | | Ricochets, PWA : *Paires* envoie les réponses à la dernière paire reliée, sans « Valider les paires », comme *Découper* et *Assembler* ; la place du numéro de paire est réservée, les colonnes ne se redimensionnent plus à chaque paire reliée ; tests du composant `Paires` | fait | +| Ricochets, *Découper* en chaîne ([ADR 0084](https://github.com/lunik/EtymoLogique/blob/master/docs/adr/0084-ricochets-decouper-toutes-les-decoupes-justes.md)) : le contenu publie toutes les coupes d'un mot (`coupes`), parties composées comprises ; le joueur les pose d'un coup, seule la coupe complète est juste ; le verdict et la leçon montrent les morceaux élémentaires (`morceauxDuMot`, miroir du compilateur de contenu) | fait | | « En savoir plus » (I-16) : chemin des langues, relations, transformations, certitude et sources citées ; dépliée depuis la carte du codex (`plus=1`) | fait (Content publie les types de relation et de transformation dans les référentiels) | | Vue Réseau (App#91) : la constellation en deux rives (1ʳᵉ case à gauche, 2ᵉ à droite, une bande par fascicule débloqué, ordre alphabétique, rangées de 44 px), dernière découverte en Corail, barre de famille Rosée, antennes chiffrées dans la vue « Tous », famille suivie dans un panneau collé, liste en repli ; calculée côté client (`services/constellation.ts`), sans légendaire ni fascicule verrouillé | fait en local ; ADR et maquette à écrire dans Design | diff --git a/web/src/services/ricochets.test.ts b/web/src/services/ricochets.test.ts index 8f1dc1f..04b68b7 100644 --- a/web/src/services/ricochets.test.ts +++ b/web/src/services/ricochets.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from 'vitest' -import { type DonneesRicochets, juger, retour } from './ricochets' +import { type DonneesRicochets, juger, morceauxDuMot, retour } from './ricochets' const d: DonneesRicochets = { briques: new Map([ @@ -54,6 +54,48 @@ describe('juger', () => { }) }) +describe('morceauxDuMot', () => { + const composition = (parties: [string, number, number][]) => ({ + preferee: true, + parties: parties.map(([unite_id, segment_debut, segment_fin], i) => ({ rang: i + 1, unite_id, segment_debut, segment_fin })), + }) + const jeu = (cartes: Record) => ({ cartes: new Map(Object.entries(cartes)) }) as never + + it('descend dans les parties qui sont des mots composés : étymologiste = étymo · logie · iste', () => { + const j = jeu({ + mot: { nature: 'mot', compositions: [composition([['logie_mot', 0, 10], ['iste', 10, 13]])] }, + logie_mot: { nature: 'mot', compositions: [composition([['etymo', 0, 5], ['logie', 5, 10]])] }, + etymo: { nature: 'prefixe' }, + logie: { nature: 'suffixe' }, + iste: { nature: 'suffixe' }, + }) + expect(morceauxDuMot(j, 'mot')).toEqual([ + { debut: 0, fin: 5, nature: 'prefixe' }, + { debut: 5, fin: 10, nature: 'suffixe' }, + { debut: 10, fin: 13, nature: 'suffixe' }, + ]) + }) + it('une partie élidée est bornée par son segment : biologie dans biologiste', () => { + // biologie (8 lettres) n'occupe que [0, 6) de biologiste : sa dernière partie perd « ie ». + const j = jeu({ + mot: { nature: 'mot', compositions: [composition([['biologie', 0, 6], ['iste', 6, 10]])] }, + biologie: { nature: 'mot', compositions: [composition([['bio', 0, 3], ['logie', 3, 8]])] }, + bio: { nature: 'prefixe' }, + logie: { nature: 'suffixe' }, + iste: { nature: 'suffixe' }, + }) + expect(morceauxDuMot(j, 'mot')).toEqual([ + { debut: 0, fin: 3, nature: 'prefixe' }, + { debut: 3, fin: 6, nature: 'suffixe' }, + { debut: 6, fin: 10, nature: 'suffixe' }, + ]) + }) + it('sans segments continus, les parties gardent leurs bornes absentes', () => { + const j = jeu({ mot: { nature: 'mot', compositions: [{ preferee: true, parties: [{ rang: 1, unite_id: 'a' }, { rang: 2, unite_id: 'b' }] }] } }) + expect(morceauxDuMot(j, 'mot').map((m) => m.debut)).toEqual([-1, -1]) + }) +}) + describe('retour', () => { const carte = (jours: number, maitrisee = false) => ({ niveau: 1, diff --git a/web/src/services/ricochets.ts b/web/src/services/ricochets.ts index 574c6d5..187ac2c 100644 --- a/web/src/services/ricochets.ts +++ b/web/src/services/ricochets.ts @@ -120,8 +120,37 @@ export function retour(carte: CarteRicochet, aujourdhui: Date = new Date()): str return carte.maitrisee ? `Maîtrisée · ${delai.toLowerCase()}` : delai } -/** Morceaux du mot dans sa composition préférée, aux couleurs de leurs briques (verdict de « découper »). */ -export function morceauxDuMot(jeu: CatalogueJeu, motId: string): { debut: number; fin: number; nature: BriqueJeu['nature'] }[] { +type Morceau = { debut: number; fin: number; nature: BriqueJeu['nature'] } + +/** + * Morceaux élémentaires d'un mot, une fois toutes ses coupes posées : une partie qui est elle-même un mot + * composé se coupe à son tour (*étymologiste* : étymo · logie · iste). Une partie élidée est bornée par son + * segment. `null` : les segments ne pavent pas le mot, il ne se coupe pas. Miroir des coupes que publie + * le compilateur du contenu (`coupes_completes`, ADR 0084) : changez les deux ensemble. + */ +function feuilles(jeu: CatalogueJeu, motId: string, debut: number, limite: number, vus: string[]): Morceau[] | null { + const compositions = jeu.cartes.get(motId)?.compositions ?? [] + const choisie = compositions.find((c) => c.preferee) ?? compositions[0] + const parties = [...(choisie?.parties ?? [])].sort((a, b) => a.rang - b.rang) + const continues = parties.every((p, i) => p.segment_debut != null && p.segment_fin != null && p.segment_fin > p.segment_debut && p.segment_debut === (i === 0 ? 0 : parties[i - 1]?.segment_fin)) + if (!parties.length || !continues) return null + const fin = Math.min(debut + (parties[parties.length - 1]?.segment_fin ?? 0), limite) + const morceaux: Morceau[] = [] + for (const p of parties) { + const d = debut + (p.segment_debut ?? 0) + if (d >= fin) break + const f = Math.min(debut + (p.segment_fin ?? 0), fin) + const sous = vus.includes(p.unite_id) ? null : feuilles(jeu, p.unite_id, d, f, [...vus, motId]) + if (sous && sous.length > 1) morceaux.push(...sous) + else morceaux.push({ debut: d, fin: f, nature: jeu.cartes.get(p.unite_id)?.nature ?? 'mot' }) + } + return morceaux +} + +/** Morceaux du mot, aux couleurs de leurs briques (verdict de « découper »), chaînage compris. */ +export function morceauxDuMot(jeu: CatalogueJeu, motId: string): Morceau[] { + const elementaires = feuilles(jeu, motId, 0, Number.POSITIVE_INFINITY, []) + if (elementaires) return elementaires const compositions = jeu.cartes.get(motId)?.compositions ?? [] const choisie = compositions.find((c) => c.preferee) ?? compositions[0] return [...(choisie?.parties ?? [])] From ac87fbcee4fa61bca2851605c6f30912dd224953 Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 10 Nov 2026 06:35:36 +0000 Subject: [PATCH 2/2] chore: fix cargo audit on develop's lockfile (tokio, libsqlite3-sys) cargo audit fails on develop's Cargo.lock too. Update tokio (RUSTSEC-2026-0386, lockfile only) and ignore RUSTSEC-2026-0459: libsqlite3-sys is never compiled, as for the existing rsa entry (cargo tree -i libsqlite3-sys --target all -e all is empty). Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01BkYb6EamApcbLXBiXShSgV --- .cargo/audit.toml | 7 ++++++- Cargo.lock | 16 ++++++++-------- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/.cargo/audit.toml b/.cargo/audit.toml index 4f6b98b..6e5d377 100644 --- a/.cargo/audit.toml +++ b/.cargo/audit.toml @@ -3,4 +3,9 @@ # sqlx-macros référence sans l'activer : il n'est jamais compilé ici (vérifié # par `cargo tree -i rsa --target all`, vide). À retirer si rsa apparaît # dans l'arbre des dépendances. -ignore = ["RUSTSEC-2023-0071"] +# +# libsqlite3-sys 0.30 (RUSTSEC-2026-0459, correctif en 0.39) est dans le même cas : +# il n'est dans Cargo.lock que par sqlx-sqlite, que sqlx-macros référence sans +# l'activer (vérifié par `cargo tree -i libsqlite3-sys --target all -e all`, +# vide). À retirer si libsqlite3-sys apparaît dans l'arbre des dépendances. +ignore = ["RUSTSEC-2023-0071", "RUSTSEC-2026-0459"] diff --git a/Cargo.lock b/Cargo.lock index b306375..e008ca2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -580,7 +580,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -1846,7 +1846,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2099,7 +2099,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2157,7 +2157,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2731,7 +2731,7 @@ dependencies = [ "getrandom 0.3.4", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -2781,9 +2781,9 @@ checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee" [[package]] name = "tokio" -version = "1.53.1" +version = "1.54.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +checksum = "b52fd3255258d26fac7264ab5a76b53c13367136dc011d3d6ad4f8ed070396a3" dependencies = [ "bytes", "libc", @@ -3292,7 +3292,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.48.0", ] [[package]]