Suivi opérationnel actuel.
REFONTE-2026.mdconserve le plan initial à titre historique. Convention :[ ]à faire ·[~]en cours ·[x]fait. Chaque tâche note l'effort[S/M/L/XL]et le finding d'audit résolu le cas échéant. Avant tout commit :pnpm run lint+pnpm run build+cargo check+cargo clippy -- -D warnings+pnpm run test(cf. CLAUDE.md).
- Ajouter
frecency_date: i64àLaunchRecord(gardélaunch_count/last_launchedpour les listes « fréquentes »/« récentes » distinctes) - Écriture au launch :
frecency_date = max(now, frecency_date) + WEIGHT(WEIGHT = 1 jour, tunable) - Lecture/classement : suggestions triées par
frecency_date DESC; bonus search borné viafrecency_bonus()(ln, cap +50), 1 seulnow()/requête - Supprimer
calculate_frecency()du hot-path - Nettoyer les 3 projections de map par frappe (search.rs ×2, apps.rs, launcher.rs)
- Migration on-load des données existantes (
backfill_frecency_date, serde default + cap) - Tests : push forward, backfill idempotent + cappé, used > never-launched (6 nouveaux)
- Validé :
cargo test+cargo clippy -- -D warnings+ build front
- Remplacer le polling 500ms par
AddClipboardFormatListener(event-driven primaire) - Fenêtre message-only + boucle
GetMessageW/WM_CLIPBOARDUPDATEsur thread dédié (winapi), signale untokio::Notify - Re-entrancy : déjà couvert par la dédup content-hash existante
- Backstop poll lent (3s) au lieu d'un fallback feature-flag — garantit zéro régression si un event est raté ; non-Windows garde la cadence normale
- App source déjà capturée via
get_foreground_app_name(existant) - Validé :
cargo check+cargo clippy -- -D warnings+ 271 tests -
⚠️ SMOKE TEST MANUEL requis : copier texte puis image → vérifier que l'historique se met à jour, pas de busy-loop (la livraison d'event Win32 n'est pas testable hors runtime)
- Outil choisi : ts-rs (léger, derives) +
TS_RS_EXPORT_DIR(.cargo/config.toml) → bindings danssrc/shared/types/generated/ - 1ère struct générée :
LaunchRecord(64-bit en#[ts(type="number")]car wire Tauri = JSON number, pas bigint) - Frontend re-exporte le type généré comme source unique de vérité (
launcher.types.ts) - Garde-fou CI : tests Rust régénèrent +
git diff --exit-code(ubuntu) ; généré exclu eslint/prettier ; LF via .gitattributes - Bench
search_benchréaligné sur la signature HashMap frecency - Étendre aux autres structs :
AppInfo/FileInfo(champ enumcategoryexporté),Settings - Métriques IPC couvertes :
SystemMetrics/SystemMetricsV2et leurs types imbriqués générés par ts-rs;StorageKindest un enum strict partagé - Commentaires
[SYNC:]supprimés pour les contrats désormais générés; les marqueurs restants sont conservés sur les contrats non couverts (AppCategory,FileSearchResult, résultats/accessoires/actions plugins) - Skew rustfmt
auth.rs/steam.rsvérifié et résolu :cargo fmt --all -- --checkpasse
-
src/shared/events.ts:VOLT_EVENTS+VoltEventMap+declare global WindowEventMap - Helpers typés
emitVoltEvent/onVoltEvent(cleanup retourné) - 18 events DOM migrés (App.tsx, useGlobalHotkey, shell, calculator, timer, ai-chat, clipboard, systemcommands, systemmonitor, ResultItem...)
- Laissés hors scope (par design) : bridge extension dynamique (
volt:${event}runtime), events Taurivolt://*(IPC cross-fenêtre), clés localStoragevolt:*
- 8 directives supprimées en corrigeant la racine : stale-closures (Clipboard/FileSearch/Game/Timer via
useCallback),no-control-regexviaString.fromCharCode(27),@ts-expect-errortest viaunknownnarrowé - EmojiPicker : déjà résolu par react-06 (useMemo) sur cette branche → version supérieure conservée au merge
- Vérifié : 0
eslint-disable/@ts-ignore/@ts-expect-errorrestant danssrc/
- Instrumenter les commandes hot-path via
tracingtiming (time_command!) — logs opt-in, sans changement de payload - Instrumenter le pipeline réel
search_streaming - Conserver les payloads IPC stables : pas de champ
timingstant qu'aucun consommateur UI/télémétrie ne l'utilise
C. SQLCipher + Credential Manager + repositories [M] ✅ durcissement sécurité validé Windows + matrice CI macOS/Linux verte
-
Blueprint détaillé :
REFONTE-PILIER-C-SQLCIPHER.md -
Ajouter la feature Cargo
sqlcipher(rusqlite/bundled-sqlcipher-vendored-openssl) sans changer le build par défaut -
core::encrypted_db::open_db+PRAGMA key+ migration plaintext→SQLCipher atomique -
Rebrancher les call sites SQLite sensibles : clipboard, notes, extension KV
-
Checkpoint WAL + suppression des sidecars plaintext avant installation de la DB chiffrée
-
Clé indépendante par base dans le Credential Manager (évite qu'une clé perdue orpheline tous les stores)
-
Algo clé (réutiliser
keyring_store.rs) :- (a) lire clé dans Credential Manager
- (b) ancien compte global DPAPI/Credential Manager → vérifier la clé sur la DB puis migrer vers le compte par base
- (c) sinon si DB chiffrée/illisible existe → erreur de récupération (NE PAS régénérer)
- (d) sinon générer 64 chars + stocker
-
Décision migrations : ne pas adopter
refinerymaintenant — schémas locaux petits/hétérogènes; conserver migrations idempotentes explicites jusqu'à un vrai besoin multi-version partagé -
Mode WAL explicite sur les stores branchés
-
Décision repositories : pas de couche générique maintenant — clipboard/notes/extension KV ont des modèles distincts; extraire seulement lors d'une duplication réelle
-
Décision frecency SQL : conserver
launch_history.json+ modèle timestamp — faible volume, aucun bottleneck mesuré; migration SQLite différée -
Migration données clair → chiffré (one-shot au 1er lancement post-update)
-
Validation Windows : Clippy strict + 317 tests sous
--no-default-features --features sqlcipher -
Validation macOS/Linux : matrice CI confirmée verte sur les 3 OS (run GitHub 27540669396, push main v0.3.0 du 2026-06-15 — "SQLCipher Clippy"/"SQLCipher Tests" en
successsur macos-latest/ubuntu-latest/windows-latest). En cours de route : bug réel découvert et fixé (hang 6h sur macOS —keyring_storetouchait le vrai Keychain dans les tests ; routé vers un map en mémoire souscfg(test)). -
Tests d'intégration clipboard/notes/extension KV + clé perdue + DB existe + migration legacy DPAPI + récupération interrompue
-
Durcissement SQLCipher suite à l'audit sécurité (validé Windows) :
- Découpler les clés DB du HMAC partagé : lecture/écriture directe dans le keyring, validation par ouverture SQLCipher, aucune suppression/régénération destructive sur perte ou rotation HMAC
- Passer à
rusqlite 0.40.1/libsqlite3-sys 0.38.1, qui embarque SQLCipher 4.14.0 et SQLite 3.51.3, première base corrigée du bug critique de corruption WAL-reset - Vérifier le triplet
PRAGMA wal_checkpoint(TRUNCATE), sortir de WAL, prendre un verrou de migration inter-processus et empêcher le downgrade/réouverture plaintext via un marqueur keyring persistant - Tests ciblés : versions runtime, checkpoint WAL busy/incomplet, clé legacy non réutilisée pour les stores plaintext, refus du downgrade et de la restauration plaintext après migration; 317 tests SQLCipher + Clippy strict
Références officielles : SQLite 3.51.3 corrige le bug WAL-reset; SQLCipher 4.14.0 embarque SQLite 3.51.3 et recommande l'upgrade aux applications utilisant WAL.
- Blueprint détaillé :
REFONTE-PILIER-D-SEARCH.md - Ajouter
tantivyàCargo.tomlderrière feature-flagtantivy-search - Scaffold
indexer::fulltext: index persistant, build/query/upsert/remove + tests - Câbler
search_filesetsearch_streamingvers Tantivy sous feature flag, avec fallback nucleo - Alimenter/reconstruire l'index depuis SQLite + scanner et le maintenir via watcher
- Validation : Clippy strict
tantivy-search+ 327 tests - Schéma production :
name,name_exact,path,ext,category,size,mtime,hidden - Tokenizer lowercase + ASCII folding, testé dans les deux sens accentué/non accentué
- Scoring en couches : exact/BM25 → boost préfixe → fuzzy pondéré
- Requête fuzzy Levenshtein avec transpositions
- Remplacer le matching nucleo sur l'index fichiers sous feature flag (fallback conservé)
- Réutiliser l'index persistant si le compteur et le marqueur de synchronisation SQLite concordent
- Démarrer/arrêter le watcher avec le lifecycle UI et le redémarrer après un rebuild manuel
-
read_dirconservé comme scanner cross-platform et couvert par un test de contrat - Bench Criterion nucleo/Tantivy ajouté : latence 1k/10k, assertions de pertinence, taille disque; workflow CI dédié ajouté. Premier run LOCAL fait (2026-06-16) : nucleo plus rapide que Tantivy sur toutes les requêtes ≤10k docs (Tantivy ~2-8 ms d'overhead fixe) → Tantivy ne gagne qu'au-delà de 10k docs. Cf. decision record dans
REFONTE-PILIER-D-SEARCH.md. Run GitHub CI confirmé vert (search-benchmark.yml, job "Criterion D1 Tantivy", run 27540669407 sur le push main v0.3.0 du 2026-06-15). - Décision : Tantivy reste OFF par défaut (nucleo gagne aux tailles de corpus réelles) ; D2/D3 wiring lifecycle = toujours NO-GO tant qu'un bench d'énumération (
scan_fileswalk vs USN drain) sur volume NTFS réel n'a pas prouvé que l'énumération à froid est le bottleneck.⚠️ Le bench actuel mesure la latence de requête, pas le temps d'énumération à froid — c'est la mesure manquante pour trancher D2/D3.
⚠️ Correction de cap (juin 2026) — l'admin était le mauvais chemin. Raycast/Spotlight n'exigent jamais d'élévation pour chercher : ils interrogent l'index de l'OS. Sur Windows l'équivalent existe et est déjà câblé (indexer/windows_search.rs, OLE DBSearch.CollatorDSO). Et surtout, le journal USN a un mode sans admin :FSCTL_READ_UNPRIVILEGED_USN_JOURNALsur un handle ouvert enFILE_TRAVERSE(vérifié sur Microsoft Learn). Conclusion : D2 (MFT brute = admin obligatoire) est rétrogradé en accélérateur optionnel « si déjà élevé », jamais requis ; la valeur réelle est D3 (USN incrémental) en mode unprivileged, par-dessus le baseline Windows Search. On ne demande jamais d'UAC pour chercher.
-
Stratégie privilèges : service Windows ou élévation ponctuelle→ abandonné (un launcher ne demande jamais l'UAC pour chercher) - Accélérateur opportuniste (lecture
$MFTen un passage si déjà élevé, feature-flagmft-search, edge-cases volumes amovibles/réseau/non-NTFS) → non planifié : aucun besoin réel (le cas « déjà élevé + NTFS » est marginal), le baseline sans admin (Windows Search Index +scan_fileswalk, D1) couvre 100% des cas. Conservé doc-only dansindexer/mft.rs(zérounsafe, zéro appel Win32, zéro dépendance NTFS tierce) au cas où le calcul changerait un jour ; n'est plus dans le backlog actif. - Décision : NE PAS bâtir le service Windows / l'élévation ponctuelle ni l'accélérateur opportuniste. Doc-only dans
indexer/mft.rs.
D3. Incrémental USN Journal — SANS ADMIN [L] ⛔ NO-GO confirmé (2026-06-24) — primitive validée & gardée OFF, wiring bloqué sur télémétrie
- Module
indexer/usn.rs(featureusn-incremental, OFF par défaut ; FFI#[cfg(windows)], parseur portable) - FFI fine maison : volume en
FILE_TRAVERSE(pasGENERIC_READ),FSCTL_QUERY_USN_JOURNAL, boucleFSCTL_READ_UNPRIVILEGED_USN_JOURNAL(=0x000903AB) - Parseur
USN_RECORD_V2/V3pur, 15 tests verts (buffer synthétique, sans volume ; inclut deletes via map, renames, coalescing) - Mapping
reason→RecordChange::{Upsert,Remove,Ignore}(create+delete coalescé = Ignore), reprise(UsnJournalID, NextUsn), erreurs typées (JournalNotActive→fallback, JournalEntryDeleted→rebuild) - Validé au runtime non-élevé (
examples/usn_probe.rs) : 300k+ records drainés à ~1 M/s, zéro admin - Finding : la lecture unprivileged STRIPPE les noms inline (records 64 o,
FileNameLength=0, même pour nos propres fichiers). C'est une propriété de sécurité, pas un bug. -
resolve_path(OpenFileById+GetFinalPathNameByHandleW) implémenté + validé : résout ~73-78% des FRN changés en chemin complet, sans admin (le reste = système inaccessible/déjà supprimé → skip). Donne le chemin complet, pas juste le nom. - Map
FRN→path+ résolution des deletes FAIT (UsnIndexer: delete-after-upsert résolu via la map, rename = removal, untracked-FRN skip) — couvert par les 6 nouveaux tests. - Bench d'énumération RÉEL exécuté (2026-06-24,
examples/enum_bench.rs) — la mesure manquante du NO-GO. Profil completC:\Users\Noluc, non-élevé :- Walk
scan_filesà froid : 2 128 042 fichiers en ~83 min (426 files/s) — mais c'est un upper-bound (inclut AppData/.cargo/node_modules/placeholders OneDrive ; pas la cible réelle d'un launcher) - USN drain : 327 873 records en 148 ms (2.2 M rec/s) MAIS 0 nom (strippés) → résolution 462 µs/fichier (
OpenFileById), 78% de succès ; ~117 s projetés pour résoudre tout le delta - Verdict : walk vs USN drain = apples-to-oranges. L'USN non-privilégié ne fait que des deltas ; le baseline reste forcément un walk (le raw-MFT « instant » est admin-only = D2 abandonné).
- Walk
- ❌ D3 lifecycle wiring = NO-GO confirmé sur données (cf. decision record
REFONTE-PILIER-D-SEARCH.md2026-06-24). Bloqué sur 2 gates non remplis : (2) fréquence de re-scan justifiant l'USN = non mesurée (pas de télémétrie) ; (3) preuve que le chemin USN-delta (resolve 462 µs/fichier inclus) bat un re-walk ciblé sur les volumes de changement réels = non prouvé. Primitive gardée OFF, non jetée. - Suite prioritaire (Vague 3.2, AVANT de reconsidérer l'USN) — découverte : déjà ~80% en place ; seuil configurable + télémétrie re-scan finalisés (2026-06-25).
- (a) scan background hors hot-path → déjà
tokio::spawn+spawn_blocking, depth borné à 3 (start_indexing) - (b) watcher
notifydebounced ciblé → déjàindexer/watcher.rs(100ms, upsert/remove par-path, sync SQLite+mémoire+tantivy) - (c) lazy refresh / réutilisation cache SQLite → déjà fast-path dans
start_indexing - Catch-up offline (le vrai trou) :
refresh_index_if_stale(stale_secs)(commit75de6b81) — réconcilie les changements faits app fermée (le watcher ne voit que le live). Re-walk background gated surlast_full_scan, swap atomique, silencieux. C'est l'alternative no-admin au drain USN. Frontend : déclenché parFileWatcherLifecycleaprès le watcher (fire-and-forget, seuil 1h). - Seuil de staleness configurable en Settings :
indexing.staleThresholdSecs(défaut 3600,0= désactivé) exposé via un<select>dans le panneau File Search (15min/30min/1h/6h/24h/off).FileWatcherLifecycle.start(staleSecs)lit la valeur ; backend#[serde(default)]pour compat ascendante. - Métriques re-scan persistées (la mesure manquante du gate (2) du NO-GO D3) :
record_stale_catchup()bumpestale_catchup_count+ stamplast_stale_catchupen meta DB uniquement sur le chemin catch-up offline (passtart_indexing/rebuild), donc mesure la dérive app-fermée. Exposé viaget_db_index_stats→ 2 tuiles Settings (compteur + dernier rattrapage). C'est la télémétrie qui débloque la reconsidération USN un jour.
- (a) scan background hors hot-path → déjà
- Baseline sans admin = Windows Search Index (déjà là) +
scan_filesfallback ; l'USN = deltas only ⚠️ StartUsndoit être0/FirstUsn/ un USN déjà retourné — offset arbitraire →ERROR_INVALID_PARAMETER(87)
- Découpler : sources / watch / indexing / queue → sources (
indexer/scanner.rs) et watch (indexer/watcher.rs) étaient déjà proprement découplés par fichier, rien à faire. queue : le design reject-if-busy (check-and-set atomique du flagis_indexing) est déjà correct et le frontend s'appuie dessus (désactivation de bouton, polling) — pas converti en vraie queue mpsc, ça changerait un comportement observable pour zéro bénéfice. - Consolidation post D1-D3 :
start_indexing,invalidate_indexetrefresh_index_if_staledupliquaient à l'identique la séquence scan → persist SQLite → rebuild Tantivy → swap cache mémoire → update status → emit progress (c'est ce qui avait failli faire oublier la télémétrie catch-up sur un seul des trois copier-collés). Extrait en une fonction privée uniquereconcile()+ReconcileParams, réutilisée par les 3 commandes ; chacune garde sa logique propre (TOCTOU guard, fast-path SQLite, garde-fous de staleness, abort/stop watcher) et délègue la partie dupliquée. Vérifié quescan_task: Mutex<Option<AbortHandle>>+IndexingGuard(RAII) sont déjà corrects — non modifiés. Zéro changement de comportement observable, zéro nouveau state Tauri/IPC. -21 lignes nettes malgré l'ajout dereconcile()(~250 lignes de copier-collé supprimées).cargo check/clippy -D warnings(défaut +tantivy-search) +cargo test --lib(304 et 328 tests) verts.
E1. Auto-expansion snippet via hook in-process [L] — ✅ implémenté, ⚠️ smoke test manuel requis avant merge
- Hook
WH_KEYBOARD_LLdans le process principal (hors fenêtres élevées) —src-tauri/src/expansion/hook.rs, thread dédié + boucleGetMessageW, feature flagsnippet-global-expansion(OFF par défaut) - Résoudre scancode→char via
ToUnicodeEx+GetKeyboardLayout(multi-layout) —src-tauri/src/expansion/keyboard_layout.rs; modificateurs lus viaGetAsyncKeyState(pasGetKeyState, qui dépend de la queue du thread appelant — le thread processor n'en pompe aucune) et toggle CapsLock au bit 0 du tableaulpKeyState(pas le bit 7, qui encode l'état "enfoncée") - Surveiller les triggers enregistrés, détecter match —
src-tauri/src/expansion/trigger_buffer.rs(buffer borné + règle de frontière de mot + plus-long-trigger-gagne), 19 tests unitaires purs (CI tous OS) - Au match : supprimer le trigger tapé + injecter le texte via
SendInput—src-tauri/src/expansion/injector.rs(backspaces en unités UTF-16, injectionKEYEVENTF_UNICODE) - Anti-boucle sur nos propres injections : filtre
LLKHF_INJECTEDdans le callback hook - Config : enabled (OFF par défaut), liste d'apps exclues (+ Volt lui-même toujours exclu via
current_exe()), longueur max de trigger surveillée —SnippetExpansionSettingsdanscommands/system/settings.rs, toggle dans Settings → nouvelle section "Snippets" - Commande Tauri
set_snippet_expansion_enabled(persiste + démarre/arrête le hook immédiatement) - 80% de la valeur snippet global, sans uiAccess
- Validé :
cargo check/cargo clippy -- -D warnings(défaut +--features snippet-global-expansion), 19 tests purs verts,pnpm run lint/pnpm run build/tsc --noEmitverts -
⚠️ SMOKE TEST MANUEL requis avant merge (comme le clipboard event-driven, Pilier B) : le hook lui-même, l'injectionSendInput, et la résolution multi-layout (FR/US, touches mortes) ne sont délibérément PAS exercés par les tests automatisés — installer un hook clavier global réel ou injecter des frappes depuis un agent aurait manipulé la vraie session Windows de la machine de dev. À tester manuellement : frappe dans Notepad/Chrome/VS Code sans lag ni perte de caractère, expansion correcte avec Shift/AltGr/CapsLock, comportement silencieux (pas de crash) sur une fenêtre élevée en admin.
-
⚠️ Prérequis : chaîne de signature de code valide + install Program Files - Binaire Rust séparé, manifeste
uiAccess="true" - Hook LL + injection
SendInputau-dessus de l'UAC - Tracking fenêtre (
SetWinEventHook)
- Named pipe + vérif identité appelant (
CheckTokenMembership) - Chiffrement + auth (Schannel/TLS)
- Vérif signature de l'appelant (
WinVerifyTrust)
- Remap touche → Ctrl+Alt+Shift+Win
-
Backend séparé supervisé (Node/.NET) + multi-cœurs→ Tauri est déjà le bon modèle -
Réécrire la recherche d'apps→ nucleo OK sur liste bornée (seul l'index fichiers passe à Tantivy) -
Calcul en langage naturel propriétaire→ hors scope
L'ordre ci-dessous était celui du plan initial. A, B, C, F1, F3 et D1 sont implémentés et leurs CI dédiées (matrice SQLCipher macOS/Linux/Windows, bench Tantivy) confirmées vertes sur GitHub (run 27540669396 / 27540669407, push main v0.3.0 du 2026-06-15). D2/D3 sont désormais en NO-GO confirmé sur données (bench d'énumération réel exécuté le 2026-06-24, cf. decision record dans REFONTE-PILIER-D-SEARCH.md) : le baseline reste un walk, l'USN non-privilégié ne peut pas l'accélérer (deltas only + 462 µs/fichier de résolution). Prochain travail = Vague 3.2 (scan background + watcher debounced ciblé + lazy refresh) avant toute reconsidération de l'USN.
Ordre initial :
- A (frecency) → débloque aussi l'audit
rust-history-clone-01 - B (clipboard event-driven)
- F1 + F3 (IPC/events typés) → garde-fou, converge avec l'audit
- C (SQLCipher)
- D1 (Tantivy) → décision GO/NO-GO pour D2/D3
- E1 si besoin produit snippet global ; E2-E4 seulement si signature dispo