From 1aad0bbfb6607669bdff49c5b38cb9f8fa44aa58 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 18:48:53 +0000 Subject: [PATCH 1/2] feat(quetes): add per-fascicule collection quests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The manifest may now carry `collections` (Content, App#90): all the fascicule's prefixes, all its suffixes, all its common, uncommon or rare words, with their members (never a legendary) and an ink and hourglass reward. The server counts each one on the codex and returns `Etat.progression_collections` (serde default) for unlocked fascicules only, so a locked fascicule exposes no total (ADR 0069). POST /palier claims a completed collection like a long-running quest palier, through `palier_recupere`: 403 when locked or incomplete, 409 when claimed. The Quêtes › Fascicules tab shows a "Collection" block under each unlocked fascicule's jalons, reusing CartePalier with the quest title in place of the threshold; claimable ones count in the tab and nav badges. Closes #90 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0151stDRaZV3ScbhwvwjrfQ7 --- contrat/openapi.json | 89 +++++++++++++++++++++++++++++++ crates/api/src/progression.rs | 94 +++++++++++++++++++++++++-------- crates/api/tests/deblocage.rs | 18 +++++++ crates/api/tests/quetes.rs | 50 +++++++++++++++++- crates/catalogue/src/essai.rs | 7 +++ crates/catalogue/src/format.rs | 5 +- crates/catalogue/src/jeu.rs | 25 +++++++-- crates/contrat/src/lib.rs | 15 ++++++ crates/domaine/src/quete.rs | 64 ++++++++++++++++++++++ docs/roadmap.md | 1 + web/src/composants/Quetes.tsx | 25 +++++++-- web/src/contrat/api.d.ts | 40 ++++++++++++++ web/src/contrat/index.ts | 3 ++ web/src/quetes.css | 3 ++ web/src/services/quetes.test.ts | 43 ++++++++++++++- web/src/services/quetes.ts | 40 +++++++++++++- 16 files changed, 487 insertions(+), 35 deletions(-) diff --git a/contrat/openapi.json b/contrat/openapi.json index 11dbf8a..1ddd751 100644 --- a/contrat/openapi.json +++ b/contrat/openapi.json @@ -2293,6 +2293,41 @@ "premiere_carte" ] }, + "Collection": { + "type": "object", + "description": "Quête de collection d'un fascicule (App#90) : tous ses préfixes, tous ses\nsuffixes, tous ses mots d'une rareté. Le catalogue en publie les membres,\nhors légendaires (ADR 0043) ; sa récompense se récupère comme un palier.", + "required": [ + "id", + "critere", + "membres" + ], + "properties": { + "critere": { + "$ref": "#/components/schemas/CritereCollection" + }, + "encre": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "id": { + "$ref": "#/components/schemas/IdCatalogue", + "description": "Identifiant de palier : la récupération passe par `palier_recupere`." + }, + "membres": { + "type": "array", + "items": { + "$ref": "#/components/schemas/IdCatalogue" + }, + "description": "Briques (préfixes, suffixes) ou mots (raretés) à réunir." + }, + "sabliers": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + } + }, "CompositionCarte": { "type": "object", "required": [ @@ -2391,6 +2426,17 @@ "non_retenue" ] }, + "CritereCollection": { + "type": "string", + "description": "Ce qu'une quête de collection rassemble (App#90) : la liste fermée.", + "enum": [ + "prefixes", + "suffixes", + "mots_communs", + "mots_peu_communs", + "mots_rares" + ] + }, "Decouverte": { "type": "object", "required": [ @@ -3314,6 +3360,13 @@ }, "description": "Prix de chaque indice, en gouttes : silhouette, sens littéral, définition." }, + "progression_collections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ProgressionCollection" + }, + "description": "Quêtes de collection des fascicules débloqués (App#90) ; vide avant ce\nchamp ou sans catalogue. Leur récupération passe par `paliers_recuperes`." + }, "progression_quetes": { "type": "array", "items": { @@ -3800,6 +3853,13 @@ "$ref": "#/components/schemas/CarteManifeste" } }, + "collections": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Collection" + }, + "description": "Quêtes de collection du fascicule (App#90), membres hors légendaires." + }, "compteurs": {}, "exclusions": { "oneOf": [ @@ -4107,6 +4167,35 @@ "suffixe" ] }, + "ProgressionCollection": { + "type": "object", + "description": "Progression d'une quête de collection (App#90), d'un fascicule débloqué\nseulement : un fascicule verrouillé n'expose aucun total (ADR 0069).", + "required": [ + "palier_id", + "fascicule_id", + "progression", + "total" + ], + "properties": { + "fascicule_id": { + "$ref": "#/components/schemas/IdCatalogue" + }, + "palier_id": { + "$ref": "#/components/schemas/IdCatalogue" + }, + "progression": { + "type": "integer", + "format": "int32", + "minimum": 0 + }, + "total": { + "type": "integer", + "format": "int32", + "minimum": 0 + } + }, + "additionalProperties": false + }, "ProgressionQuete": { "type": "object", "required": [ diff --git a/crates/api/src/progression.rs b/crates/api/src/progression.rs index e91dd94..d9e7abd 100644 --- a/crates/api/src/progression.rs +++ b/crates/api/src/progression.rs @@ -8,7 +8,8 @@ use chrono::{DateTime, TimeDelta, Utc}; use etymo_catalogue::Jeu; use etymo_contrat::{ CadeauEtat, CompteEtat, CompteurPli, Decouverte, EnergieEtat, Etat, Exemplaires, - FasciculeDebloque, IndiceObtenu, OuverturePli, ProgressionQuete, SabliersEtat, + FasciculeDebloque, IndiceObtenu, OuverturePli, ProgressionCollection, ProgressionQuete, + SabliersEtat, }; use etymo_domaine::IdCatalogue; use etymo_domaine::carte_de_lecteur; @@ -144,9 +145,41 @@ pub async fn etat( quetes_initiales: quetes_initiales::etat(&mut *tx, joueur, jeu, maintenant).await?, fascicules_debloques: lire_fascicules_debloques(&mut *tx, joueur).await?, compte: compte_etat(&mut *tx, joueur).await?, + progression_collections: progression_collections(&mut *tx, joueur, jeu).await?, }) } +/// Quêtes de collection des fascicules débloqués (App#90), à la valeur du codex. +async fn progression_collections( + tx: &mut PgConnection, + joueur: Uuid, + jeu: Option<&Jeu>, +) -> Result, ErreurApi> { + let Some(jeu) = jeu.filter(|j| !j.collections.is_empty()) else { + return Ok(Vec::new()); + }; + let debloques = fascicules_debloques(&mut *tx, joueur).await?; + let mots = mots_decouverts(&mut *tx, joueur).await?; + let briques = briques_connues(&mut *tx, joueur).await?; + let codex = Codex { + mots: &mots, + briques: &briques, + }; + Ok(jeu + .collections + .iter() + .filter(|(f, _)| debloques.contains(f)) + .flat_map(|(f, quetes)| { + quetes.iter().map(|q| ProgressionCollection { + palier_id: q.id.clone(), + fascicule_id: f.clone(), + progression: q.progression(&codex), + total: q.total(), + }) + }) + .collect()) +} + /// Compte du joueur, absent pour une partie invitée (ADR 0070). pub(crate) async fn compte_etat( tx: &mut PgConnection, @@ -1233,30 +1266,47 @@ pub async fn recuperer_palier( commande: Uuid, palier_id: &IdCatalogue, ) -> Result<(), ErreurApi> { - let (quete, palier) = jeu - .quetes - .iter() - .find_map(|q| { - q.paliers - .iter() - .find(|p| &p.id == palier_id) - .map(|p| (q, p)) - }) - .ok_or_else(|| ErreurApi::RequeteInvalide("palier : inconnu".into()))?; - let progression = progression_quetes(&mut *tx, joueur, jeu) - .await? - .into_iter() - .find(|p| p.quete_id == quete.id) - .map_or(0, |p| p.progression); - if !quete::atteint(palier, progression) { - return Err(ErreurApi::Interdit("palier pas encore atteint")); - } + let (encre, sabliers) = if let Some((quete, palier)) = jeu.quetes.iter().find_map(|q| { + q.paliers + .iter() + .find(|p| &p.id == palier_id) + .map(|p| (q, p)) + }) { + let progression = progression_quetes(&mut *tx, joueur, jeu) + .await? + .into_iter() + .find(|p| p.quete_id == quete.id) + .map_or(0, |p| p.progression); + if !quete::atteint(palier, progression) { + return Err(ErreurApi::Interdit("palier pas encore atteint")); + } + (palier.encre, palier.sabliers) + } else { + // Quête de collection (App#90) : un fascicule verrouillé ne se récupère pas. + let collection = jeu + .collections + .iter() + .flat_map(|(_, q)| q) + .find(|q| &q.id == palier_id) + .ok_or_else(|| ErreurApi::RequeteInvalide("palier : inconnu".into()))?; + let progression = progression_collections(&mut *tx, joueur, Some(jeu)) + .await? + .into_iter() + .find(|p| &p.palier_id == palier_id); + match progression { + None => return Err(ErreurApi::Interdit("fascicule verrouillé")), + Some(p) if p.progression < p.total => { + return Err(ErreurApi::Interdit("palier pas encore atteint")); + } + Some(_) => (collection.encre, collection.sabliers), + } + }; let maintenant = Utc::now(); let nouveau = sqlx::query_scalar!( "INSERT INTO palier_recupere (joueur_id, palier_id, recupere_le) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING RETURNING palier_id", joueur, - palier.id.as_str(), + palier_id.as_str(), maintenant, ) .fetch_optional(&mut *tx) @@ -1264,7 +1314,7 @@ pub async fn recuperer_palier( if nouveau.is_none() { return Err(ErreurApi::Conflit("palier déjà récupéré")); } - let gouttes = i32::try_from(palier.encre).map_err(|e| ErreurApi::Interne(e.to_string()))?; + let gouttes = i32::try_from(encre).map_err(|e| ErreurApi::Interne(e.to_string()))?; crediter_encre( &mut *tx, joueur, @@ -1274,7 +1324,7 @@ pub async fn recuperer_palier( maintenant, ) .await?; - let sabliers = i32::try_from(palier.sabliers).map_err(|e| ErreurApi::Interne(e.to_string()))?; + let sabliers = i32::try_from(sabliers).map_err(|e| ErreurApi::Interne(e.to_string()))?; crediter_sabliers( &mut *tx, joueur, diff --git a/crates/api/tests/deblocage.rs b/crates/api/tests/deblocage.rs index 3fd0121..f4e0e47 100644 --- a/crates/api/tests/deblocage.rs +++ b/crates/api/tests/deblocage.rs @@ -146,3 +146,21 @@ async fn un_seul_jalon_ne_debloque_rien(base: PgPool) { assert_eq!(statut, StatusCode::OK, "{corps}"); assert_eq!(debloques(&corps["etat"]), vec![PREMIER]); } + +#[sqlx::test(migrations = "../../migrations")] +async fn les_collections_d_un_fascicule_verrouille_ne_se_montrent_pas(base: PgPool) { + let banc = banc(base, Limiteurs::default()).await; + publier(&banc, true).await; + let jeton = session(&banc.routeur).await; + let (_, _, etat) = appeler(&banc.routeur, get_etat(&jeton)).await; + assert_eq!(debloques(&etat), vec![PREMIER]); + // Aucun total d'un fascicule verrouillé (ADR 0069, App#90). + let fascicules: Vec<&str> = etat["progression_collections"] + .as_array() + .unwrap() + .iter() + .map(|p| p["fascicule_id"].as_str().unwrap()) + .collect(); + assert!(!fascicules.is_empty()); + assert!(fascicules.iter().all(|f| *f == PREMIER), "{fascicules:?}"); +} diff --git a/crates/api/tests/quetes.rs b/crates/api/tests/quetes.rs index 60955dc..44e5fba 100644 --- a/crates/api/tests/quetes.rs +++ b/crates/api/tests/quetes.rs @@ -7,7 +7,8 @@ mod commun; use axum::http::StatusCode; use etymo_api::Limiteurs; use etymo_catalogue::essai::{ - GEO, LOGIE, PALIER_ELISION, PALIER_LANGUE_1, PALIER_LANGUE_3, PALIER_RICOCHETS, QUETE_LANGUE, + COLLECTION_MOTS, COLLECTION_PREFIXES, GEO, LOGIE, PALIER_ELISION, PALIER_LANGUE_1, + PALIER_LANGUE_3, PALIER_RICOCHETS, QUETE_LANGUE, }; use serde_json::{Value, json}; use sqlx::PgPool; @@ -100,3 +101,50 @@ async fn un_palier_pas_encore_atteint_ou_inconnu_est_refuse(base: PgPool) { let (statut, _) = recuperer(&banc, &jeton, etat, PALIER_ELISION).await; assert_eq!(statut, StatusCode::OK); } + +// ------------------------------------------------------------ quêtes de collection (App#90) + +fn collection(etat: &Value, palier: &str) -> (u64, u64) { + etat["progression_collections"] + .as_array() + .unwrap() + .iter() + .find(|p| p["palier_id"] == palier) + .map_or((u64::MAX, u64::MAX), |p| { + ( + p["progression"].as_u64().unwrap(), + p["total"].as_u64().unwrap(), + ) + }) +} + +#[sqlx::test(migrations = "../../migrations")] +async fn une_collection_se_recupere_une_fois_complete(base: PgPool) { + let banc = banc(base.clone(), Limiteurs::default()).await; + let jeton = session(&banc.routeur).await; + let (_, _, etat) = appeler(&banc.routeur, get_etat(&jeton)).await; + // Graines : géo- et -logie ; bio- manque aux préfixes, le mot n'est pas trouvé. + assert_eq!(collection(&etat, COLLECTION_PREFIXES), (1, 2)); + assert_eq!(collection(&etat, COLLECTION_MOTS), (0, 1)); + let (statut, _) = recuperer(&banc, &jeton, &etat, COLLECTION_MOTS).await; + assert_eq!(statut, StatusCode::FORBIDDEN); + + let demande = commande(&etat, json!({ "briques": [GEO, LOGIE] })); + let (_, _, reponse) = appeler(&banc.routeur, post_fusion(&jeton, &demande)).await; + let etat = reponse["etat"].clone(); + assert_eq!(collection(&etat, COLLECTION_MOTS), (1, 1)); + // Le mot offre sa brique-mot bio- : les deux préfixes sont réunis. + assert_eq!(collection(&etat, COLLECTION_PREFIXES), (2, 2)); + + let encre = somme_mouvements(&base, "encre").await; + let (statut, reponse) = recuperer(&banc, &jeton, &etat, COLLECTION_PREFIXES).await; + assert_eq!(statut, StatusCode::OK, "{reponse}"); + assert_eq!(somme_mouvements(&base, "encre").await, encre + 20); + assert_eq!(somme_mouvements(&base, "sablier").await, 1); + assert_eq!( + reponse["etat"]["paliers_recuperes"], + json!([COLLECTION_PREFIXES]) + ); + let (statut, _) = recuperer(&banc, &jeton, &reponse["etat"], COLLECTION_PREFIXES).await; + assert_eq!(statut, StatusCode::CONFLICT); +} diff --git a/crates/catalogue/src/essai.rs b/crates/catalogue/src/essai.rs index 3d2cd05..92a0d5a 100644 --- a/crates/catalogue/src/essai.rs +++ b/crates/catalogue/src/essai.rs @@ -41,6 +41,9 @@ pub const PALIER_LANGUE_1: &str = "pal_0000000a"; pub const PALIER_LANGUE_3: &str = "pal_0000000b"; pub const PALIER_ELISION: &str = "pal_0000000c"; pub const PALIER_RICOCHETS: &str = "pal_0000000d"; +/// Quêtes de collection du fascicule d'essai (App#90) : ses deux préfixes, son mot commun. +pub const COLLECTION_PREFIXES: &str = "pal_000c0001"; +pub const COLLECTION_MOTS: &str = "pal_000c0002"; /// Écrit un fascicule minimal (une carte, une recette, des exclusions, une /// jaquette, des graines) et renvoie le chemin de son manifeste. @@ -138,6 +141,10 @@ pub fn fascicule_avec_jalons(racine: &Path, numero: u32, parution: &str, jalons: } }, "jalons": jalons, + "collections": [ + { "id": COLLECTION_PREFIXES, "critere": "prefixes", "encre": 20, "sabliers": 1, "membres": [BIO, GEO] }, + { "id": COLLECTION_MOTS, "critere": "mots_communs", "encre": 20, "sabliers": 0, "membres": [MOT] } + ], "graines": [ { "brique_id": GEO, "exemplaires": 2 }, { "brique_id": LOGIE, "exemplaires": 1 } diff --git a/crates/catalogue/src/format.rs b/crates/catalogue/src/format.rs index 07bf671..87c68a5 100644 --- a/crates/catalogue/src/format.rs +++ b/crates/catalogue/src/format.rs @@ -10,7 +10,7 @@ use etymo_domaine::IdCatalogue; use etymo_domaine::cadeau::CadeauDeBienvenue; use etymo_domaine::jalon::Jalon; use etymo_domaine::pli::{Equilibrage, NaturePli, Rarete}; -use etymo_domaine::quete::QueteLongCours; +use etymo_domaine::quete::{Collection, QueteLongCours}; use serde::{Deserialize, Serialize}; use crate::Chemin; @@ -88,6 +88,9 @@ pub struct Manifeste { /// De quoi juger un ricochet sans lire un texte (ADR 0044). #[serde(default)] pub ricochets: RicochetsManifeste, + /// Quêtes de collection du fascicule (App#90), membres hors légendaires. + #[serde(default)] + pub collections: Vec, } /// Données de jugement des ricochets d'un fascicule : des identifiants, jamais de texte. diff --git a/crates/catalogue/src/jeu.rs b/crates/catalogue/src/jeu.rs index eeadea8..de48df5 100644 --- a/crates/catalogue/src/jeu.rs +++ b/crates/catalogue/src/jeu.rs @@ -9,7 +9,7 @@ use etymo_domaine::deblocage::{self, Maillon}; use etymo_domaine::fusion::{ExclusionJeu, RecetteJeu}; use etymo_domaine::jalon::Jalon; use etymo_domaine::pli::{self, BriquePli}; -use etymo_domaine::quete::{CarteQuete, Famille, Mesures, QueteLongCours}; +use etymo_domaine::quete::{CarteQuete, Collection, Famille, Mesures, QueteLongCours}; use crate::carte::Nature; use crate::format::{BriqueRicochet, Difficulte, MotRicochet, Role}; @@ -43,6 +43,8 @@ pub struct Jeu { pub mesures: Mesures, /// Cadeau de bienvenue déclaré par l'index (ADR 0046), réduit à ses briques publiées. pub cadeau: Option, + /// Quêtes de collection de chaque fascicule paru (App#90), par identifiant de fascicule. + pub collections: Vec<(IdCatalogue, Vec)>, } /// Données de jugement des ricochets, par carte. @@ -129,6 +131,14 @@ impl Jeu { } impl Jeu { + /// Ce qui se lit sur l'ensemble des fascicules parus : la chaîne de + /// déblocage, les quêtes de collection, les ricochets sans légendaire. + fn completer(&mut self, fascicules: &[FasciculeCharge]) { + self.chaine = chaine(fascicules); + self.collections = collections(fascicules); + self.ecarter_les_legendaires_des_ricochets(); + } + /// Un ricochet ne vise ni une légendaire ni un mot qui en exige une : même /// règle que les recettes secrètes (ADR 0015), le contenu l'applique déjà. fn ecarter_les_legendaires_des_ricochets(&mut self) { @@ -335,12 +345,21 @@ impl Jeu { .mots .extend(ricochets.mots.iter().map(|m| (m.id.clone(), m.clone()))); } - jeu.chaine = chaine(fascicules); - jeu.ecarter_les_legendaires_des_ricochets(); + jeu.completer(fascicules); jeu } } +/// Quêtes de collection de chaque fascicule paru qui en déclare (App#90). +fn collections(fascicules: &[FasciculeCharge]) -> Vec<(IdCatalogue, Vec)> { + fascicules + .iter() + .map(|f| &f.manifeste) + .filter(|m| !m.collections.is_empty()) + .map(|m| (m.id.clone(), m.collections.clone())) + .collect() +} + /// Fascicules parus par numéro, chacun avec le jalon qui ouvre le suivant (ADR 0068). fn chaine(fascicules: &[FasciculeCharge]) -> Vec { let mut parus: Vec<_> = fascicules.iter().map(|f| &f.manifeste).collect(); diff --git a/crates/contrat/src/lib.rs b/crates/contrat/src/lib.rs index dd02ad6..04b1f09 100644 --- a/crates/contrat/src/lib.rs +++ b/crates/contrat/src/lib.rs @@ -120,6 +120,10 @@ pub struct Etat { /// Compte facultatif (ADR 0070) ; absent pour une partie invitée. #[serde(default, skip_serializing_if = "Option::is_none")] pub compte: Option, + /// Quêtes de collection des fascicules débloqués (App#90) ; vide avant ce + /// champ ou sans catalogue. Leur récupération passe par `paliers_recuperes`. + #[serde(default)] + pub progression_collections: Vec, } /// Compte du joueur (ADR 0070). L'adresse n'y figure jamais : le serveur ne @@ -571,6 +575,17 @@ pub struct ReponseRicochet { pub etat: Etat, } +/// Progression d'une quête de collection (App#90), d'un fascicule débloqué +/// seulement : un fascicule verrouillé n'expose aucun total (ADR 0069). +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] +#[serde(deny_unknown_fields)] +pub struct ProgressionCollection { + pub palier_id: IdCatalogue, + pub fascicule_id: IdCatalogue, + pub progression: u32, + pub total: u32, +} + #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(deny_unknown_fields)] pub struct ProgressionQuete { diff --git a/crates/domaine/src/quete.rs b/crates/domaine/src/quete.rs index 7789c74..6086414 100644 --- a/crates/domaine/src/quete.rs +++ b/crates/domaine/src/quete.rs @@ -146,6 +146,51 @@ impl Mesures { } } +/// Ce qu'une quête de collection rassemble (App#90) : la liste fermée. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +#[serde(rename_all = "snake_case")] +pub enum CritereCollection { + Prefixes, + Suffixes, + MotsCommuns, + MotsPeuCommuns, + MotsRares, +} + +/// Quête de collection d'un fascicule (App#90) : tous ses préfixes, tous ses +/// suffixes, tous ses mots d'une rareté. Le catalogue en publie les membres, +/// hors légendaires (ADR 0043) ; sa récompense se récupère comme un palier. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, utoipa::ToSchema)] +pub struct Collection { + /// Identifiant de palier : la récupération passe par `palier_recupere`. + pub id: IdCatalogue, + pub critere: CritereCollection, + #[serde(default)] + pub encre: u32, + #[serde(default)] + pub sabliers: u32, + /// Briques (préfixes, suffixes) ou mots (raretés) à réunir. + pub membres: Vec, +} + +impl Collection { + /// Membres déjà au codex : un mot découvert, une brique connue. + #[must_use] + pub fn progression(&self, codex: &Codex<'_, S>) -> u32 { + let n = self + .membres + .iter() + .filter(|m| codex.mots.contains(*m) || codex.briques.contains(*m)) + .count(); + u32::try_from(n).unwrap_or(u32::MAX) + } + + #[must_use] + pub fn total(&self) -> u32 { + u32::try_from(self.membres.len()).unwrap_or(u32::MAX) + } +} + /// Palier atteint à cette progression. #[must_use] pub fn atteint(palier: &Palier, progression: u32) -> bool { @@ -289,4 +334,23 @@ mod tests { assert!(!atteint(&palier, 2)); assert!(atteint(&palier, 3)); } + + #[test] + fn une_collection_compte_ses_membres_au_codex() { + let collection = Collection { + id: id("pal_00000c01"), + critere: CritereCollection::Prefixes, + encre: 20, + sabliers: 1, + membres: vec![id("brq_00000001"), id("brq_00000002"), id("unt_00000003")], + }; + let briques = HashSet::from([id("brq_00000001"), id("brq_00000009")]); + let mots = HashSet::from([id("unt_00000003")]); + let codex = Codex { + mots: &mots, + briques: &briques, + }; + assert_eq!(collection.progression(&codex), 2); + assert_eq!(collection.total(), 3); + } } diff --git a/docs/roadmap.md b/docs/roadmap.md index b6ff7a0..956ad00 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -159,6 +159,7 @@ Reportés : file d’attente des fusions hors connexion (supprimée, ADR 0041), | Tâche | État | |---|---| | ADR 0042 : page Quêtes, récompenses de jalon à récupérer d’un geste ; ADR 0038 marqué partiellement remplacé (Design) | proposé | +| Quêtes de collection par fascicule (App#90) : tous les préfixes, tous les suffixes, tous les mots communs, peu communs ou rares, déclarés par le fascicule (Content, membres publiés hors légendaires) ; `Etat.progression_collections` des seuls fascicules débloqués ; récupération par `POST /palier` et `palier_recupere` (`403` verrouillé ou incomplet, `409` déjà récupérée) ; bloc « Collection » sous les jalons de l’onglet Fascicules, compté dans les pastilles | fait en local ; ADR à écrire (Design) ; reste : briques reprises dans le filtre du codex, 3ᵉ jalon hors légendaires | | Base : `jalon_atteint.recupere_le` (les jalons déjà accordés sont marqués récupérés), commande `jalon` | fait | | API : la découverte et `GET /etat` marquent les jalons atteints sans rien accorder ; `POST /jalon` récupère la récompense (pli de jalon déterministe, encre si réserve pleine, sabliers), idempotente, `403` pas encore atteint, `409` déjà récupérée, `400` inconnu ; `jalons_recuperes` dans l’état | fait | | PWA : destination Quêtes (`#/quetes`), onglets Journalières (bientôt disponibles) et Fascicules (`#/quetes/fascicules?fascicule=0`) ; sélecteur de fascicule partagé avec le codex, compteur Corail des récompenses qui attendent (navigation, onglet, fascicule) ; jalons retirés de la vue Objectifs du codex, qui renvoie aux quêtes ; « Récupérer la récompense » sous la découverte qui atteint un jalon | fait | diff --git a/web/src/composants/Quetes.tsx b/web/src/composants/Quetes.tsx index 26f3d55..cdcc9c8 100644 --- a/web/src/composants/Quetes.tsx +++ b/web/src/composants/Quetes.tsx @@ -6,7 +6,7 @@ import { type CatalogueJeu, libelleFascicule } from '../services/jeu' import { briquesDuPli, chances, LIBELLES_RARETES, pourcentage } from '../services/plis' import { sensLitteral } from '../services/ricochets' import { conditionDeblocage, jalonDeblocant, useDeblocage } from '../services/deblocage' -import { type PalierSuivi, type QueteJalon, type QueteLongue, SORTES, quetesAuLongCours, quetesDeFascicule } from '../services/quetes' +import { CRITERES, type PalierSuivi, type QueteJalon, type QueteLongue, SORTES, quetesAuLongCours, quetesDeCollection, quetesDeFascicule } from '../services/quetes' import type { OngletQuetes, Route } from '../services/route' import { PetiteBrique } from './Brique' import { SelecteurFascicules, TitreFascicule } from './Fascicules' @@ -133,13 +133,15 @@ function texteRecompensePalier({ palier }: PalierSuivi): string { interface PalierProps { suivi: PalierSuivi + /** À la place du seuil : l'intitulé d'une quête de collection (« Tous les préfixes »). */ + titre?: string inactif: string | null recuperer: (palier: string) => Promise onAller?: (d: Destination) => void } /** Un palier : seuil, progression, récompense annoncée d'avance et, une fois atteint, le bouton pour la récupérer. */ -function CartePalier({ suivi, inactif, recuperer, onAller }: PalierProps) { +function CartePalier({ suivi, titre, inactif, recuperer, onAller }: PalierProps) { const [enCours, setEnCours] = useState(false) const [message, setMessage] = useState<{ texte: string; ton: 'win' | 'fail' } | null>(null) const { palier, etat, avancee } = suivi @@ -152,7 +154,7 @@ function CartePalier({ suivi, inactif, recuperer, onAller }: PalierProps) { return (
  • - {palier.seuil} + {titre ? {titre} : {palier.seuil}} {etat === 'recupere' ? '✓ Récupérée' : etat === 'a_recuperer' ? '✓ Atteint' : `${avancee} / ${palier.seuil}`}
    @@ -535,12 +537,14 @@ export function Quetes({ jeu, etat, entrees, vue, onChangerVue, urlMedia, recupe const deblocage = useDeblocage() const quetes = quetesDeFascicule(jeu, etat) const aRecuperer = quetes.filter((q) => q.etat === 'a_recuperer') + const collections = quetesDeCollection(jeu, etat) + const collectionsARecuperer = collections.filter((q) => q.suivi.etat === 'a_recuperer') const longues = quetesAuLongCours(jeu, etat) const paliersARecuperer = longues.flatMap((q) => q.paliers).filter((p) => p.etat === 'a_recuperer').length const attente: Record = { journalieres: (etat?.quetes_du_jour ?? []).filter((q) => q.recompense && q.faite && !q.recuperee).length + (etat?.carte_de_lecteur?.a_recuperer ?? 0) + (etat?.cadeau_de_bienvenue && !etat.cadeau_de_bienvenue.recupere ? 1 : 0), ponctuelles: (etat?.quetes_initiales ?? []).filter((q) => q.faite && !q.recuperee).length, - fascicules: aRecuperer.length, + fascicules: aRecuperer.length + collectionsARecuperer.length, 'au-long-cours': paliersARecuperer, } // Sans onglet choisi, le premier onglet où une récompense attend passe devant. @@ -656,10 +660,11 @@ export function Quetes({ jeu, etat, entrees, vue, onChangerVue, urlMedia, recupe actif={fascicule} onChoisir={(f) => onChangerVue({ fascicule: f === 'tous' ? null : f }, { remplacer: true })} urlMedia={urlMedia} - aRecuperer={(f) => aRecuperer.filter((q) => q.fascicule.numero === f.numero).length} + aRecuperer={(f) => aRecuperer.filter((q) => q.fascicule.numero === f.numero).length + collectionsARecuperer.filter((q) => q.fascicule.numero === f.numero).length} /> {affichees.map((f) => { const siennes = quetes.filter((q) => q.fascicule.numero === f.numero) + const sesCollections = collections.filter((q) => q.fascicule.numero === f.numero) const verrouille = deblocage ? !deblocage.debloques.has(f.manifeste.id) : false return (
    @@ -680,6 +685,16 @@ export function Quetes({ jeu, etat, entrees, vue, onChangerVue, urlMedia, recupe ))} )} + {!verrouille && sesCollections.length > 0 && ( + <> +

    Collection

    +
      + {sesCollections.map((q) => ( + + ))} +
    + + )}
    ) })} diff --git a/web/src/contrat/api.d.ts b/web/src/contrat/api.d.ts index edd0b89..292e654 100644 --- a/web/src/contrat/api.d.ts +++ b/web/src/contrat/api.d.ts @@ -644,6 +644,22 @@ export interface components { * @enum {string} */ CodeQueteInitiale: "trois_mots" | "transformation" | "premiers_ricochets" | "premiere_carte"; + /** + * @description Quête de collection d'un fascicule (App#90) : tous ses préfixes, tous ses + * suffixes, tous ses mots d'une rareté. Le catalogue en publie les membres, + * hors légendaires (ADR 0043) ; sa récompense se récupère comme un palier. + */ + Collection: { + critere: components["schemas"]["CritereCollection"]; + /** Format: int32 */ + encre?: number; + /** @description Identifiant de palier : la récupération passe par `palier_recupere`. */ + id: components["schemas"]["IdCatalogue"]; + /** @description Briques (préfixes, suffixes) ou mots (raretés) à réunir. */ + membres: components["schemas"]["IdCatalogue"][]; + /** Format: int32 */ + sabliers?: number; + }; CompositionCarte: { fait?: components["schemas"]["Fait"] | null; id: components["schemas"]["IdCatalogue"]; @@ -684,6 +700,11 @@ export interface components { }; /** @enum {string} */ Confiance: "etablie" | "probable" | "discutee" | "non_retenue"; + /** + * @description Ce qu'une quête de collection rassemble (App#90) : la liste fermée. + * @enum {string} + */ + CritereCollection: "prefixes" | "suffixes" | "mots_communs" | "mots_peu_communs" | "mots_rares"; Decouverte: { /** Format: date-time */ decouverte_le: string; @@ -1093,6 +1114,11 @@ export interface components { paliers_recuperes?: components["schemas"]["IdCatalogue"][]; /** @description Prix de chaque indice, en gouttes : silhouette, sens littéral, définition. */ prix_indices?: number[]; + /** + * @description Quêtes de collection des fascicules débloqués (App#90) ; vide avant ce + * champ ou sans catalogue. Leur récupération passe par `paliers_recuperes`. + */ + progression_collections?: components["schemas"]["ProgressionCollection"][]; /** * @description Progression de chaque quête au long cours, calculée par le serveur sur * le codex (ADR 0045). Un palier est atteint quand elle franchit son seuil. @@ -1249,6 +1275,8 @@ export interface components { }; Manifeste: { cartes?: components["schemas"]["CarteManifeste"][]; + /** @description Quêtes de collection du fascicule (App#90), membres hors légendaires. */ + collections?: components["schemas"]["Collection"][]; compteurs?: unknown; exclusions?: components["schemas"]["Chemin"] | null; /** Format: int32 */ @@ -1349,6 +1377,18 @@ export interface components { * @enum {string} */ PositionBrique: "prefixe" | "suffixe"; + /** + * @description Progression d'une quête de collection (App#90), d'un fascicule débloqué + * seulement : un fascicule verrouillé n'expose aucun total (ADR 0069). + */ + ProgressionCollection: { + fascicule_id: components["schemas"]["IdCatalogue"]; + palier_id: components["schemas"]["IdCatalogue"]; + /** Format: int32 */ + progression: number; + /** Format: int32 */ + total: number; + }; ProgressionQuete: { /** Format: int32 */ progression: number; diff --git a/web/src/contrat/index.ts b/web/src/contrat/index.ts index 4b74ac1..68a7910 100644 --- a/web/src/contrat/index.ts +++ b/web/src/contrat/index.ts @@ -31,6 +31,9 @@ export type Erreur = S['Erreur'] export type Calendrier = S['Calendrier'] export type EntreeFascicule = S['EntreeFascicule'] export type Manifeste = S['Manifeste'] +export type Collection = S['Collection'] +export type CritereCollection = S['CritereCollection'] +export type ProgressionCollection = S['ProgressionCollection'] export type Recette = S['Recette'] export type Exclusions = S['Exclusions'] export type Carte = S['Carte'] diff --git a/web/src/quetes.css b/web/src/quetes.css index 53d2b89..56b2444 100644 --- a/web/src/quetes.css +++ b/web/src/quetes.css @@ -38,6 +38,9 @@ .quete-tete { display: flex; flex-wrap: wrap; align-items: baseline; justify-content: space-between; gap: 6px 12px; } .quetes-liste .quete-tete { flex-direction: column; align-items: stretch; justify-content: flex-start; gap: 6px; } .quete-seuil { font-family: var(--serif); font-size: 20px; font-weight: 600; } +/* Quête de collection (App#90) : son intitulé à la place du seuil. */ +.quete-titre-collection { font-family: var(--serif); font-size: 17px; font-weight: 600; line-height: 1.25; } +.quetes-sous-titre { margin: 18px 0 8px; color: var(--ink-2); font-family: var(--mono); font-size: 12px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; } .quete-avancee { font-family: var(--mono); font-size: 12px; font-weight: 700; } .quete-pied { display: flex; flex-wrap: wrap; align-items: center; gap: 8px 10px; margin-top: auto; } .quete-pied .btn { margin-left: auto; } diff --git a/web/src/services/quetes.test.ts b/web/src/services/quetes.test.ts index 8a30c68..833a714 100644 --- a/web/src/services/quetes.test.ts +++ b/web/src/services/quetes.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it } from 'vitest' import { GEO, MOT, RECETTE, etat, jeu, manifeste } from '../test/fixtures' -import { quetesAuLongCours, quetesDeFascicule, recompensesARecuperer } from './quetes' +import { quetesAuLongCours, quetesDeCollection, quetesDeFascicule, recompensesARecuperer } from './quetes' const JALON = 'jal_0000000a' const SUIVANT = 'jal_0000000b' @@ -98,3 +98,44 @@ describe('quêtes d’un fascicule verrouillé (ADR 0068)', () => { expect(quetesDeFascicule(avecJalons(), etat({ decouvertes: decouvert }))).toHaveLength(2) }) }) + +describe('quêtes de collection (App#90)', () => { + const PREFIXES = 'pal_000c0001' + const RARES = 'pal_000c0002' + function avecCollections() { + const j = avecJalons() + const m = { + ...j.fascicules[0]!.manifeste, + collections: [ + { id: RARES, critere: 'mots_rares' as const, encre: 40, sabliers: 1, membres: ['unt_1', 'unt_2', 'unt_3'] }, + { id: PREFIXES, critere: 'prefixes' as const, encre: 20, sabliers: 1, membres: [GEO, 'brq_2', 'brq_3'] }, + ], + } + return { ...j, fascicules: [{ ...j.fascicules[0]!, manifeste: m }] } + } + const progression = (prefixes: number, rares: number) => [ + { palier_id: PREFIXES, fascicule_id: manifeste.id, progression: prefixes, total: 3 }, + { palier_id: RARES, fascicule_id: manifeste.id, progression: rares, total: 3 }, + ] + + it('lit la progression du serveur, dans l’ordre des critères', () => { + const quetes = quetesDeCollection(avecCollections(), etat({ progression_collections: progression(1, 0) })) + expect(quetes.map((q) => [q.collection.critere, q.suivi.avancee, q.suivi.palier.seuil, q.suivi.etat])).toEqual([ + ['prefixes', 1, 3, 'en_cours'], + ['mots_rares', 0, 3, 'en_cours'], + ]) + }) + + it('attend la récupération d’une collection complète et la compte', () => { + const j = avecCollections() + const complete = etat({ progression_collections: progression(3, 0) }) + expect(quetesDeCollection(j, complete)[0]?.suivi.etat).toBe('a_recuperer') + expect(recompensesARecuperer(j, complete)).toBe(1) + expect(quetesDeCollection(j, { ...complete, paliers_recuperes: [PREFIXES] })[0]?.suivi.etat).toBe('recupere') + }) + + it('ne montre rien que le serveur n’a pas compté (fascicule verrouillé)', () => { + expect(quetesDeCollection(avecCollections(), etat())).toEqual([]) + expect(quetesDeCollection(avecCollections(), null)).toEqual([]) + }) +}) diff --git a/web/src/services/quetes.ts b/web/src/services/quetes.ts index e28fcc9..8287e8f 100644 --- a/web/src/services/quetes.ts +++ b/web/src/services/quetes.ts @@ -3,7 +3,7 @@ // récupérer. Le serveur fait foi : un jalon n'est « à récupérer » que s'il le // dit atteint. -import type { Etat, Jalon, Palier, QueteLongCours } from '../contrat' +import type { Collection, CritereCollection, Etat, Jalon, Palier, QueteLongCours } from '../contrat' import { fasciculesDebloques } from './deblocage' import type { CatalogueJeu, FasciculeParu } from './jeu' @@ -98,6 +98,41 @@ export function quetesAuLongCours(jeu: CatalogueJeu, etat: Etat | null): QueteLo }) } +/** Intitulés des quêtes de collection (App#90), dans l'ordre d'affichage. */ +export const CRITERES: Record = { + prefixes: 'Tous les préfixes', + suffixes: 'Tous les suffixes', + mots_communs: 'Tous les mots communs', + mots_peu_communs: 'Tous les mots peu communs', + mots_rares: 'Tous les mots rares', +} +const ORDRE_CRITERES = Object.keys(CRITERES) as CritereCollection[] + +export interface QueteCollection { + collection: Collection + fascicule: FasciculeParu + /** Comme un palier : son seuil est le total, sa récompense celle de la collection. */ + suivi: PalierSuivi +} + +/** + * Quêtes de collection (App#90) : tous les préfixes, tous les suffixes, tous les + * mots d'une rareté d'un fascicule. Le serveur compte, et ne renvoie que les + * fascicules débloqués : un fascicule verrouillé n'expose aucun total (ADR 0069). + * La récompense se récupère comme un palier (ADR 0042). + */ +export function quetesDeCollection(jeu: CatalogueJeu, etat: Etat | null): QueteCollection[] { + const recuperes = new Set(etat?.paliers_recuperes ?? []) + return (etat?.progression_collections ?? []).flatMap((p) => { + const fascicule = jeu.fascicules.find((f) => f.manifeste.id === p.fascicule_id) + const collection = fascicule?.manifeste.collections?.find((c) => c.id === p.palier_id) + if (!fascicule || !collection || p.total === 0) return [] + const palier: Palier = { id: collection.id, seuil: p.total, encre: collection.encre, sabliers: collection.sabliers } + const etatPalier: EtatJalon = recuperes.has(collection.id) ? 'recupere' : p.progression >= p.total ? 'a_recuperer' : 'en_cours' + return [{ collection, fascicule, suivi: { palier, avancee: Math.min(p.progression, p.total), etat: etatPalier } }] + }).sort((a, b) => a.fascicule.numero - b.fascicule.numero || ORDRE_CRITERES.indexOf(a.collection.critere) - ORDRE_CRITERES.indexOf(b.collection.critere)) +} + /** Récompenses qui attendent le joueur, jalons et paliers : le compteur de la navigation. */ export function recompensesARecuperer(jeu: CatalogueJeu | null, etat: Etat | null): number { if (!jeu || !etat) return 0 @@ -105,5 +140,6 @@ export function recompensesARecuperer(jeu: CatalogueJeu | null, etat: Etat | nul const cartes = etat.carte_de_lecteur?.a_recuperer ?? 0 const cadeau = etat.cadeau_de_bienvenue && !etat.cadeau_de_bienvenue.recupere ? 1 : 0 const initiales = (etat.quetes_initiales ?? []).filter((q) => q.faite && !q.recuperee).length - return quetesDeFascicule(jeu, etat).filter((q) => q.etat === 'a_recuperer').length + paliers + cartes + cadeau + initiales + const collections = quetesDeCollection(jeu, etat).filter((q) => q.suivi.etat === 'a_recuperer').length + return quetesDeFascicule(jeu, etat).filter((q) => q.etat === 'a_recuperer').length + paliers + cartes + cadeau + initiales + collections } From d795b6f7c18863aa4a4c73a9803e072fb3694141 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 18:53:09 +0000 Subject: [PATCH 2/2] fix(deps): update rustls-webpki, webpki-roots, ctutils and vite for audits cargo audit flagged RUSTSEC-2026-0381/0382/0383/0385 and npm audit a high advisory on vite 8.3.1 (and moderate ones on vitest): lockfile-only updates to the patched versions. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0151stDRaZV3ScbhwvwjrfQ7 (cherry picked from commit a140f55df0d3ce823acc6ff6d9534bce30e72ab8) --- Cargo.lock | 18 +-- web/package-lock.json | 248 +++++++++++++++++++++--------------------- 2 files changed, 133 insertions(+), 133 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d4332a..b306375 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,9 +479,9 @@ dependencies = [ [[package]] name = "ctutils" -version = "0.4.2" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +checksum = "38d28fb6aa7dd680b3ffd1a3e64e115c177bdba63c0ae7f9cd48129ff05c4fa0" dependencies = [ "cmov", ] @@ -2168,9 +2168,9 @@ checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f" [[package]] name = "rustls-webpki" -version = "0.103.15" +version = "0.103.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2" +checksum = "4443b1b6d39d7c663139942cc4558262562d2c84123bb6282ef177009ca8b281" dependencies = [ "aws-lc-rs", "ring", @@ -3235,9 +3235,9 @@ dependencies = [ [[package]] name = "webpki-root-certs" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +checksum = "e49d0e65abc00c142e8f9428da991da9f1a250c39be97459901eb12b9b49f62a" dependencies = [ "rustls-pki-types", ] @@ -3248,14 +3248,14 @@ version = "0.26.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9" dependencies = [ - "webpki-roots 1.0.9", + "webpki-roots 1.0.11", ] [[package]] name = "webpki-roots" -version = "1.0.9" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a" +checksum = "f5f280bbb2b287bc8e7de7e3852945a0ff50d1bbcb5ed42b6c7a568c546df171" dependencies = [ "rustls-pki-types", ] diff --git a/web/package-lock.json b/web/package-lock.json index c5f9c8a..1050295 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1900,9 +1900,9 @@ } }, "node_modules/@oxc-project/types": { - "version": "0.151.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", - "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "version": "0.155.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.155.0.tgz", + "integrity": "sha512-SeE8JPwW1xs1tHeNycjUtUBdhorRa8o4jlvjwk49R7/Z291i+4DQa7JJIgOm/TaYQZx+QrPQbVJ/9c8dMIZN8Q==", "dev": true, "license": "MIT", "funding": { @@ -2279,9 +2279,9 @@ } }, "node_modules/@rolldown/binding-android-arm-eabi": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", - "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.15.tgz", + "integrity": "sha512-aLcgK9gIJ3YRvrrtfSBbRa27F5WrpiTaPulnFpsHPxpUHgFlSgnU4854MHwoRGEBhfRJDWCS2WQ7lIjskE9Olw==", "cpu": [ "arm" ], @@ -2296,9 +2296,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", - "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.15.tgz", + "integrity": "sha512-xD7DdZCrbThKuxNg7ooQmvp07ys4J/pnSEMTG3/OdOWl6daC67m/Q4fL78sRHMxTH/N4fGUbYhcPM0MF5WFHkg==", "cpu": [ "arm64" ], @@ -2313,9 +2313,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", - "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.15.tgz", + "integrity": "sha512-m2WuQ6r0B4OTYxyPpNLtXaMRbRDUbG8YQV2Q3KTdSSK8Th6KHCnC8jVCJt8bvYeSB5WivkkNbc85mKadaSdOxA==", "cpu": [ "arm64" ], @@ -2330,9 +2330,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", - "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.15.tgz", + "integrity": "sha512-5RtnsHM36nodM9WbMy+zip0LGS/G+y5wro5RHrpOlr4ac24+1zSi9kiV2MP1myZeZ/ifEHfulQcWjnAXzc2C2A==", "cpu": [ "x64" ], @@ -2347,9 +2347,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", - "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.15.tgz", + "integrity": "sha512-g3l09pyFqhu+135JKkbkwiKGQmwU5hUMUPJ4VdBADvuqR8d33L6H578rh8m7rtxbdp+VmX/+nzI7nvhSZlzpUQ==", "cpu": [ "x64" ], @@ -2364,9 +2364,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", - "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.15.tgz", + "integrity": "sha512-nIe3VugI97nGQtspPxDqA4sXMUpeiocKIP/CqpY/u/vgbT+alX+cVxu1C/oVBPKg4feNTmYovE91v4DgXvXNwQ==", "cpu": [ "arm" ], @@ -2381,9 +2381,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", - "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.15.tgz", + "integrity": "sha512-iTBjXx+B5QSvgI89lJfmqh4wTh8NabR1YZIIN3hNx4zSG0EVfVqkOvLulNpG4251pbd6/IAmZ/LefVk1Y4rPhw==", "cpu": [ "arm64" ], @@ -2398,9 +2398,9 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", - "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.15.tgz", + "integrity": "sha512-uN26XOnVR7rqQupG3V9oAG2EGGatWjcJHOeDOHUFCh4TBWmP2tHTFoVN13aYbwfm0aHqJAEmcwhAu6sooIjfqQ==", "cpu": [ "arm64" ], @@ -2415,9 +2415,9 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", - "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.15.tgz", + "integrity": "sha512-9nWkcdd+H+XhaA3fQIwpHnKIrfx/PQ6rO0pivr78Og07tXlUlctBBJoEYLPfhTmCR/QxEVdn6uPjHV5SymPhdw==", "cpu": [ "ppc64" ], @@ -2432,9 +2432,9 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", - "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.15.tgz", + "integrity": "sha512-Ggh0ciFjLwIusLSLz7LZYEaGdxOAUJXI+Qu5JCbhPJJHEt8vP+IjRATTJheiJsdzzTYgUiHUiw3X1Ea3ZpMBdA==", "cpu": [ "s390x" ], @@ -2449,9 +2449,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", - "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.15.tgz", + "integrity": "sha512-FHg60rS6DBePAYZ9X9F67x+JObFdmgn6tupA1C/a3B+CufFmTD4mX+e8M75tCiAO3foR/6xwVBI0KQiI7sn/rw==", "cpu": [ "x64" ], @@ -2466,9 +2466,9 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", - "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.15.tgz", + "integrity": "sha512-/cvV/7v5VGz/moiz0XKRj2dE8DzsKyhIu1KpOvJGPjRywg3yscON889UGvAe8zfiJLHup7VLjBduoGWi3HusWQ==", "cpu": [ "x64" ], @@ -2483,9 +2483,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", - "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.15.tgz", + "integrity": "sha512-MyzC5Hrv/sm+Z7TSGKP/j8001RO2xQm1zOpCNVL4j0cY/VhBZviludA2YFQ7i4lH6ZAXxQm0lcMdBzM+99mE7w==", "cpu": [ "arm64" ], @@ -2500,9 +2500,9 @@ } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", - "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.15.tgz", + "integrity": "sha512-+PgYiS+AH5omy6L45ZLxybasauJaj3M8buoT83D+nzPmCayXlxUV9hydhciCKVyIg0B/v0BtXPDE1uGqNwCPpg==", "cpu": [ "arm64" ], @@ -2517,9 +2517,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", - "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.15.tgz", + "integrity": "sha512-HFZd2QNQYn1JbIi+Hpd7H+r/lkWxcfIu28nirZ6eCn2LafuvmExMlb/P96Sfx5tf7pPArw39IdhmCF8vmV+NhQ==", "cpu": [ "x64" ], @@ -3246,22 +3246,22 @@ } }, "node_modules/@vitest/mocker": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.2.tgz", - "integrity": "sha512-Z5FS00Q1SJHkB35xATsmWGdQ5WA1/0MV3CDjqyv7GavHv1OfOj145MNfHOlHk7QLes21dKFDHr8EO2zvL+9WGA==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-5.0.5.tgz", + "integrity": "sha512-xLL3lkDegJZw+MGoGaCCsFayDZthGgpAtr3Vziyfc4p7xH89K7KRPcRBBh/kEYm0Aqkt5DtTmkYpBtRV/FrHZg==", "dev": true, "license": "MIT", "dependencies": { "@jridgewell/trace-mapping": "0.3.31", - "@vitest/spy": "5.0.2", + "@vitest/spy": "5.0.5", "estree-walker": "^3.0.3", - "magic-string": "^1.2.3" + "magic-string": "^1.4.3" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "msw": "^2.4.9", + "msw": "^2.4.9 || ^3.0.0", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { @@ -3274,9 +3274,9 @@ } }, "node_modules/@vitest/spy": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.2.tgz", - "integrity": "sha512-Ijc7T1nT9efNb5LxvjaBrEqw3f/QwUv5EE0nKqZxgqsaV/FxAAZ8baGylA8X/Z2oS4Lp+K74Jr6dTJsDKxJDeg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-5.0.5.tgz", + "integrity": "sha512-lwJRExuH417257Q5wv9QYugN9IMJkoxcrKrYxGPQkBzl7wlNpTUxD/5yNX4ESKVUIShKPgkGchYPFMWOOk0wzw==", "dev": true, "license": "MIT", "funding": { @@ -3672,9 +3672,9 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -5670,9 +5670,9 @@ } }, "node_modules/magic-string": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", - "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5737,9 +5737,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.19", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", - "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "dev": true, "funding": [ { @@ -5810,9 +5810,9 @@ } }, "node_modules/obug": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.2.1.tgz", - "integrity": "sha512-XrsrhT5sybtKI6wakr2SPOlGZWWYbUXZ7a0jT8/QOeAPau+1X/bSegNe5YR75oJmEZQbKningirmGOEJCIk61Q==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", "dev": true, "funding": [ "https://github.com/sponsors/sxzz", @@ -6025,9 +6025,9 @@ } }, "node_modules/postcss": { - "version": "8.5.28", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", - "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "version": "8.5.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.31.tgz", + "integrity": "sha512-YUbWYCgErwnMAVkjUnYa+KlMBzGpHwiIt/DE7f0G33AuX2LH9UcTk6KgWxd3zRU+pN3u/C+Xl3EnMLM4Hea7Sg==", "dev": true, "funding": [ { @@ -6045,9 +6045,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.18", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -6270,13 +6270,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.11", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", - "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "version": "1.2.15", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.15.tgz", + "integrity": "sha512-6odND+fel5zVw3I4Yc2e69gvguDnX0CAAGiNDZSq4ik5WUcaLrkD2GLuog0XcODbor31hZ5ayEMgVDno+KPV5g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.151.0", + "@oxc-project/types": "=0.155.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -6286,21 +6286,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm-eabi": "1.2.11", - "@rolldown/binding-android-arm64": "1.2.11", - "@rolldown/binding-darwin-arm64": "1.2.11", - "@rolldown/binding-darwin-x64": "1.2.11", - "@rolldown/binding-freebsd-x64": "1.2.11", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", - "@rolldown/binding-linux-arm64-gnu": "1.2.11", - "@rolldown/binding-linux-arm64-musl": "1.2.11", - "@rolldown/binding-linux-ppc64-gnu": "1.2.11", - "@rolldown/binding-linux-s390x-gnu": "1.2.11", - "@rolldown/binding-linux-x64-gnu": "1.2.11", - "@rolldown/binding-linux-x64-musl": "1.2.11", - "@rolldown/binding-openharmony-arm64": "1.2.11", - "@rolldown/binding-win32-arm64-msvc": "1.2.11", - "@rolldown/binding-win32-x64-msvc": "1.2.11" + "@rolldown/binding-android-arm-eabi": "1.2.15", + "@rolldown/binding-android-arm64": "1.2.15", + "@rolldown/binding-darwin-arm64": "1.2.15", + "@rolldown/binding-darwin-x64": "1.2.15", + "@rolldown/binding-freebsd-x64": "1.2.15", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.15", + "@rolldown/binding-linux-arm64-gnu": "1.2.15", + "@rolldown/binding-linux-arm64-musl": "1.2.15", + "@rolldown/binding-linux-ppc64-gnu": "1.2.15", + "@rolldown/binding-linux-s390x-gnu": "1.2.15", + "@rolldown/binding-linux-x64-gnu": "1.2.15", + "@rolldown/binding-linux-x64-musl": "1.2.15", + "@rolldown/binding-openharmony-arm64": "1.2.15", + "@rolldown/binding-win32-arm64-msvc": "1.2.15", + "@rolldown/binding-win32-x64-msvc": "1.2.15" } }, "node_modules/rollup": { @@ -6656,9 +6656,9 @@ } }, "node_modules/std-env": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.2.0.tgz", - "integrity": "sha512-oCUKSupKTHX53EyjDtuZQ64pjLJ6yYCtpmEw0goYxtjG9KpbRe8KAsl2tBUGU9DyMcJ0RwJ8GqJAFzMXcXW1Rw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.3.0.tgz", + "integrity": "sha512-OtU/EgQ1kIm5KwqQpBC6ZEMXrZRui11w8zgfTWp8cdO9B8OaPsbA8bTHO2P+HNo1VlUTGMVBwPhydu6poeXiag==", "dev": true, "license": "MIT" }, @@ -6890,9 +6890,9 @@ } }, "node_modules/tinybench": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.0.tgz", - "integrity": "sha512-78U2TlB2CnVenajOFzf3BKSm0J6oz5L0NV7g32LCPccvYc0lbWvys4d3uUUCS2B1N8PAf2+aekR8i1KbC3HO7Q==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-6.2.2.tgz", + "integrity": "sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==", "dev": true, "license": "MIT", "engines": { @@ -7229,16 +7229,16 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", - "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.5.tgz", + "integrity": "sha512-z1YpjieJ9g0ebTmQpLBv/oq9E3F9EIlOe+7LahW8SrE3LTiG3udBRUXQOlYXP9b+IC2q2phHeVxj9xGlw34OEQ==", "dev": true, "license": "MIT", "dependencies": { "lightningcss": "^1.33.0", "picomatch": "^4.0.7", - "postcss": "^8.5.28", - "rolldown": "~1.2.9", + "postcss": "^8.5.30", + "rolldown": "~1.2.14", "tinyglobby": "^0.2.17" }, "bin": { @@ -7255,7 +7255,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.7.1", + "@vitejs/devtools": "^0.7.1 || ^0.8.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -7338,25 +7338,25 @@ } }, "node_modules/vitest": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.2.tgz", - "integrity": "sha512-7MQrx9pDv5aHiUcovIb/70Ys3tgtkUVgCtledvKdCmEO+/1Dicq5ZqoSxOW034m03oqC+oHOKui2dM6qtMLoJg==", + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-5.0.5.tgz", + "integrity": "sha512-cPODOMF8YnkiiYtDdHo6n4wWmBrOBBbOIYH2d33M4l3CdNn72h+qMmO6uXffUN6A5AZFXNND/d+krS9mgkXRfg==", "dev": true, "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/mocker": "5.0.2", - "chai": "^6.2.2", + "@vitest/mocker": "5.0.5", + "chai": "^6.3.0", "es-module-lexer": "^2.3.2", "expect-type": "^1.4.0", - "magic-string": "^1.2.3", - "obug": "^2.1.4", + "magic-string": "^1.4.3", + "obug": "^3.0.0", "picomatch": "^4.0.7", - "std-env": "^4.2.0", - "tinybench": "^6.1.4", - "tinyexec": "^1.3.0", + "std-env": "^4.3.0", + "tinybench": "^6.2.2", + "tinyexec": "^1.3.1", "tinyglobby": "^0.2.17", - "why-is-node-running": "^3.2.1" + "why-is-node-running": "3.2.1" }, "bin": { "vitest": "vitest.mjs" @@ -7371,12 +7371,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "5.0.2", - "@vitest/browser-preview": "5.0.2", + "@vitest/browser-playwright": "5.0.5", + "@vitest/browser-preview": "5.0.5", "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", - "@vitest/coverage-istanbul": "5.0.2", - "@vitest/coverage-v8": "5.0.2", - "@vitest/ui": "5.0.2", + "@vitest/coverage-istanbul": "5.0.5", + "@vitest/coverage-v8": "5.0.5", + "@vitest/ui": "5.0.5", "happy-dom": "*", "jsdom": "*", "vite": "^6.4.0 || ^7.0.0 || ^8.0.0" @@ -7574,9 +7574,9 @@ } }, "node_modules/why-is-node-running": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.2.tgz", - "integrity": "sha512-NKUzAelcoCXhXL4dJzKIwXeR8iEVqsA0Lq6Vnd0UXvgaKbzVo4ZTHROF2Jidrv+SgxOQ03fMinnNhzZATxOD3A==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-3.2.1.tgz", + "integrity": "sha512-Tb2FUhB4vUsGQlfSquQLYkApkuPAFQXGFzxWKHHumVz2dK+X1RUm/HnID4+TfIGYJ1kTcwOaCk/buYCEJr6YjQ==", "dev": true, "license": "MIT", "bin": {