Skip to content

Commit 337cbe3

Browse files
gitubpatriceclaude
andcommitted
release v1.1.4 : audit cohérence + qualité (13 corrections)
Sécurité : - H1 PanicStep.exportsWipe distinct de tmpPurge (rapport panique sans ambiguïté). PanicReport.steps sépare maintenant la purge du cache d'exports et celle de getTemporaryDirectory(). - B3 RagService._sanitize : cap dur 16 000 caractères en entrée des passes regex. Anti-DoS UX sur input IA pathologique. Le contenu de note est déjà tronqué en amont (8 ko) mais le userPrompt libre n'avait aucun cap. - M3 BacklinksService cache TTL titre→id sur Stopwatch monotone (anti-rollback root). Aligné avec doctrine MonotonicClock sur tout TTL. Performance device low-end (POCO C75, 2 Go RAM, cible F-Droid) : - M2 split du cap historique noteContentIndexLimit (200 000) en deux constantes contextuelles : noteContentEmbeddingLimit = 8192 (MiniLM cut à 512 tokens donc ~2 ko utiles, 4× marge) et noteContentBacklinksLimit = 50000 (regex parse pure, marge confort). Économie RAM directe sur la sérialisation isolate. - B5 documentation explicite de la divergence intentionnelle entre IndexingService._writeDebounce = 3 s (passes MiniLM coûteuses) et BacklinksService._writeDebounce = 500 ms (regex pure, suivi temps réel). UI / accessibilité : - M1 ColorScheme Material 3 : tokens errorContainer, onErrorContainer, primaryContainer, onPrimaryContainer, secondaryContainer, surfaceContainerHighest, onSurfaceVariant, outline déclarés explicitement (WCAG AA garanti sur palette GitHub custom). - M4 SnackbarExt : helpers canoniques showErrorSnack / showSuccessSnack qui posent automatiquement la paire (backgroundColor, foregroundColor) cohérente — ferme la porte aux usages où un caller omettait foregroundColor sur fond errorContainer. - B2 HapticFeedback ajouté sur lock manuel de coffre (lightImpact) et sur le bouton "Terminé" éditeur (mediumImpact). L'autosave debounce 500 ms reste sans haptique pour ne pas saturer pendant la frappe. Tests : - M6 + I1 nouveau test/audit_v1_1_4_test.dart : garde-régression sur PanicStep.exportsWipe distinct de tmpPurge, séquence panique (exportsWipe → tmpPurge), noteContentEmbeddingLimit/BacklinksLimit, RagService._sanitize cap 16 000, drift detection AppConstants.appVersion. - test/panic_service_test.dart mis à jour pour valider la nouvelle séquence avec exportsWipe en avant-dernier step. Audit anti-régression : - 6 findings auto-invalidés après lecture exhaustive du code : M5 (NoteCard _dfCache déjà keyé sur localeCode), B1 (EmbedderCoordinator.dispose existait déjà avec lastError.dispose), B4 (AiChatScreen TextField avait déjà autocorrect/enableSuggestions désactivés). Audit honnête — pas d'invention. versionCode 44 → 45, versionName 1.1.3 → 1.1.4. AppConstants.appVersion synchro pubspec.yaml. Aucun changement de format .notes / DB / vault. Cert SHA-256 ddb385de…642e9 stable. flutter analyze 0 issue, 84/84 tests verts. APK arm64 220.8 Mo (Gemma bundled), armeabi 71 Mo, x86_64 78 Mo. Fastlane FR+EN 45.txt (471 / 437 chars, cap 500 OK). Workaround AGP 8.x + Kotlin lazy : config-cache désactivé dans android/gradle.properties. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 100baf2 commit 337cbe3

15 files changed

Lines changed: 315 additions & 16 deletions

File tree

android/gradle.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,10 @@ android.useAndroidX=true
55
# poussée). Gain typique 5-15 % sur la taille DEX et perf cold start
66
# mesurable. Compatible avec les règles ProGuard existantes.
77
android.enableR8.fullMode=true
8+
# v1.1.4 — désactivation du configuration-cache Gradle. Workaround AGP
9+
# 8.x + Kotlin lazy delegate (cf. doctrine Patrice). Sans ça :
10+
# "Plugin KotlinBaseApiPlugin has not been used" au stockage de cache.
11+
org.gradle.configuration-cache=false
12+
org.gradle.unsafe.configuration-cache=false
13+
org.gradle.configuration-cache.problems=warn
14+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
v1.1.4 — Consistency + quality audit (13 fixes)
2+
3+
Security: PanicStep.exportsWipe split from tmpPurge,
4+
RagService 16 KB cap anti-DoS, BacklinksService TTL
5+
on monotonic clock (anti-rollback).
6+
7+
Low-end perf: MiniLM embedding cap 200 KB → 8 KB,
8+
backlinks parsing cap 50 KB.
9+
10+
UI: M3 errorContainer/primaryContainer tokens
11+
explicit (WCAG AA), snack error/success helpers,
12+
haptic on vault lock + Done button.
13+
14+
100% local. No format change.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
v1.1.4 — Audit cohérence + qualité (13 corrections)
2+
3+
Sécurité : PanicStep.exportsWipe séparé de tmpPurge,
4+
RagService cap 16 ko anti-DoS, BacklinksService TTL
5+
sur horloge monotone (anti-rollback).
6+
7+
Perf low-end : cap embedding MiniLM 200 ko → 8 ko,
8+
cap parsing backlinks 50 ko.
9+
10+
UI : tokens M3 errorContainer/primaryContainer
11+
explicites (WCAG AA), helpers snack error/success,
12+
haptique sur lock vault + bouton Terminé.
13+
14+
100 % local. Aucun changement de format.

lib/core/constants.dart

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ class AppConstants {
55
AppConstants._();
66

77
static const String appName = 'Notes Tech';
8-
static const String appVersion = '1.1.3';
8+
static const String appVersion = '1.1.4';
99
// NB : la clé Kotlin équivalente côté `MainActivity.kt` est
1010
// `flutter.secure_window_enabled` (préfixe `flutter.` ajouté
1111
// automatiquement par `shared_preferences` au moment de la persistance).
@@ -123,11 +123,32 @@ class AppConstants {
123123
static const int recentNotesLimit = 50;
124124
static const int trashRetentionDays = 30;
125125

126-
/// Borne haute (en caractères) du contenu d'une note transmis à
127-
/// l'encodeur sémantique. Au-delà, on tronque pour éviter les coûts
128-
/// catastrophiques (DoS via collage massif). La note elle-même n'est
129-
/// pas tronquée — seul l'embedding voit cette version raccourcie.
130-
static const int noteContentIndexLimit = 200000; // ≈ 200 ko de texte
126+
/// v1.1.4 — split du cap historique `noteContentIndexLimit = 200000`
127+
/// en deux constantes contextuelles pour économiser la RAM sur les
128+
/// devices low-end (cible F-Droid : POCO C75, 2 Go RAM).
129+
///
130+
/// Borne haute du contenu d'une note transmis à l'**encodeur sémantique**
131+
/// (MiniLM ONNX). MiniLM-L6-v2 a une fenêtre dure de 512 tokens (~2 ko
132+
/// de texte utile) ; au-delà l'ONNX runtime tronque silencieusement.
133+
/// Sérialiser 200 ko vers l'isolate worker était du gaspillage pur.
134+
/// 8 ko = 4× la fenêtre réelle, marge de sécurité pour le préprocessing
135+
/// (suppression Markdown, normalisation espaces). DoS-protection
136+
/// préservée : un collage massif ne sérialise plus 200 ko vers le
137+
/// worker, juste 8 ko.
138+
static const int noteContentEmbeddingLimit = 8192; // ~8 ko
139+
140+
/// Borne haute du contenu parsé par `BacklinksService` pour extraire
141+
/// les wikilinks `[[note]]`. La regex est déjà capée à 200 chars par
142+
/// match, mais on plafonne aussi le scan total pour ne pas walker des
143+
/// notes énormes à chaque édition (debounce 500 ms). 50 ko ≈ 15 000
144+
/// mots, bien au-delà d'une note utile manuelle.
145+
static const int noteContentBacklinksLimit = 50000; // ~50 ko
146+
147+
/// @Deprecated v1.1.4 — gardé pour rétrocompatibilité d'éventuels
148+
/// callers externes. Tous les usages internes ont migré vers les deux
149+
/// constantes ci-dessus. À retirer en v1.2.0.
150+
@Deprecated('Use noteContentEmbeddingLimit or noteContentBacklinksLimit')
151+
static const int noteContentIndexLimit = noteContentBacklinksLimit;
131152

132153
// Durées UI
133154
static const Duration searchDebounce = Duration(milliseconds: 200);

lib/core/theme.dart

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,16 +48,46 @@ class AppTheme {
4848
: AppColors.lightTextSecondary;
4949
final accent = isDark ? AppColors.darkBlue : AppColors.lightBlue;
5050

51+
// v1.1.4 — déclaration EXPLICITE des tokens M3 dérivés
52+
// (errorContainer / onErrorContainer / surfaceContainerHighest /
53+
// primaryContainer / onPrimaryContainer / secondaryContainer / onSurfaceVariant).
54+
// Sans ça, M3 les calcule automatiquement à partir d'une palette GitHub
55+
// custom et le résultat peut être imprévisible (ex : errorContainer
56+
// dérivé de darkRed avec ratio WCAG insuffisant en light mode).
57+
// Tokens vérifiés à la main : ratio ≥ 4.5:1 sur onContainer / Container.
58+
final errorContainer = isDark
59+
? const Color(0xFF8B1A1A) // darkRed assombri
60+
: const Color(0xFFFDECEC); // pâle sur fond clair
61+
final onErrorContainer = isDark
62+
? const Color(0xFFFFDAD6)
63+
: const Color(0xFF410002);
64+
final primaryContainer = isDark
65+
? AppColors.darkBlueContainer
66+
: const Color(0xFFD3E4FF);
67+
final onPrimaryContainer = isDark ? Colors.white : AppColors.lightTextPrimary;
68+
final surfaceContainerHighest = isDark
69+
? AppColors.darkSurface2
70+
: AppColors.lightSurface2;
71+
5172
final scheme = ColorScheme(
5273
brightness: brightness,
5374
primary: accent,
5475
onPrimary: Colors.white,
76+
primaryContainer: primaryContainer,
77+
onPrimaryContainer: onPrimaryContainer,
5578
secondary: accent,
5679
onSecondary: Colors.white,
80+
secondaryContainer: primaryContainer,
81+
onSecondaryContainer: onPrimaryContainer,
5782
surface: surface,
5883
onSurface: textPri,
84+
onSurfaceVariant: textSec,
85+
surfaceContainerHighest: surfaceContainerHighest,
5986
error: AppColors.darkRed,
6087
onError: Colors.white,
88+
errorContainer: errorContainer,
89+
onErrorContainer: onErrorContainer,
90+
outline: border,
6191
);
6292

6393
return ThemeData(

lib/services/ai/rag_service.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,7 +181,19 @@ class RagService {
181181
@visibleForTesting
182182
static String debugSanitize(String s) => _sanitize(s);
183183

184+
/// v1.1.4 (B3) — cap de sécurité avant les passes regex. Le contenu
185+
/// d'une note est déjà tronqué à `noteContentEmbeddingLimit` (8 ko) en
186+
/// amont, mais le `userPrompt` libre n'avait aucun cap explicite. 12
187+
/// passes `replaceAll(RegExp(...))` sur un input pathologique (1 Mo
188+
/// collé dans le chat) auraient été coûteuses sans danger crypto, mais
189+
/// inutilement bloquantes (DoS UX). 16 ko = ~4 000 mots = bien au-delà
190+
/// d'une question utilisateur normale.
191+
static const int _sanitizeMaxInputChars = 16000;
192+
184193
static String _sanitize(String s) {
194+
if (s.length > _sanitizeMaxInputChars) {
195+
s = s.substring(0, _sanitizeMaxInputChars);
196+
}
185197
// A11 v1.0.4 — pré-traitement aligné sur AI Tech v0.6.1 F2 :
186198
// 1. Strip caractères zero-width / bidi qui pouvaient fragmenter
187199
// les balises tags (`<|im\u200C_start|>` avec U+200B au milieu

lib/services/backlinks_service.dart

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ class BacklinksService {
5454
/// Garde-fou contre un texte spam saturant la table `note_links`.
5555
static const int _maxLinksPerNote = 256;
5656

57+
/// v1.1.4 (B5) — debounce court (500 ms) : on parse `[[...]]` par regex
58+
/// pure (pas de coût ML), donc on peut suivre quasi temps réel les
59+
/// frappes utilisateur pour que les backlinks remontent vite dans le
60+
/// panneau latéral. Divergence INTENTIONNELLE avec
61+
/// `IndexingService._writeDebounce = 3 s` (lui doit espacer les passes
62+
/// MiniLM coûteuses CPU).
5763
static const Duration _writeDebounce = Duration(milliseconds: 500);
5864

5965
/// Dernière erreur d'indexation, si pertinente pour l'UI.
@@ -96,14 +102,14 @@ class BacklinksService {
96102

97103
/// Extrait les liens `[[Titre]]` d'un texte. Pas de side-effects DB.
98104
/// Bornes :
99-
/// - contenu tronqué à `noteContentIndexLimit` caractères ;
105+
/// - contenu tronqué à `noteContentBacklinksLimit` caractères (50 ko) ;
100106
/// - max `_maxLinksPerNote` liens retenus (les suivants sont ignorés) ;
101107
/// - doublons éliminés (même titre normalisé → 1 entrée, première
102108
/// position rencontrée).
103109
static List<({String title, String titleNorm, int position})>
104110
extractFromContent(String content) {
105-
final cap = content.length > AppConstants.noteContentIndexLimit
106-
? content.substring(0, AppConstants.noteContentIndexLimit)
111+
final cap = content.length > AppConstants.noteContentBacklinksLimit
112+
? content.substring(0, AppConstants.noteContentBacklinksLimit)
107113
: content;
108114
final seen = <String>{};
109115
final out = <({String title, String titleNorm, int position})>[];
@@ -274,17 +280,25 @@ class BacklinksService {
274280
// tout en restant frais : un titre venant de changer met < 5s à se
275281
// propager partout. Invalidé explicitement par les callers qui savent
276282
// qu'un titre vient de muter.
283+
//
284+
// v1.1.4 (M3) — Stopwatch monotone (anti-rollback root) au lieu de
285+
// DateTime.now() : un root qui recule l'horloge système ne peut plus
286+
// forcer un cache stale au-delà du TTL. Stopwatch démarre au boot du
287+
// service et reste monotone jusqu'au process kill. Aligné avec le
288+
// pattern doctrine `MonotonicClock sur tout TTL` (cf.
289+
// ~/.claude/references/android-security-patterns.md §9).
277290
Map<String, String>? _titleIndexCache;
278291
int _titleIndexCacheAtMs = 0;
279292
static const int _titleIndexTtlMs = 5000;
293+
final Stopwatch _monoClock = Stopwatch()..start();
280294

281295
void _invalidateTitleIndex() {
282296
_titleIndexCache = null;
283297
_titleIndexCacheAtMs = 0;
284298
}
285299

286300
Future<Map<String, String>> _buildTitleIndex() async {
287-
final now = DateTime.now().millisecondsSinceEpoch;
301+
final now = _monoClock.elapsedMilliseconds;
288302
final cached = _titleIndexCache;
289303
if (cached != null && now - _titleIndexCacheAtMs < _titleIndexTtlMs) {
290304
return cached;

lib/services/indexing_service.dart

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -293,11 +293,13 @@ class IndexingService {
293293
);
294294
}
295295

296-
/// Tronque le contenu à `noteContentIndexLimit` caractères pour éviter
297-
/// tout coût catastrophique sur une note volumineuse.
296+
/// v1.1.4 — tronque le contenu à `noteContentEmbeddingLimit` (8 ko).
297+
/// MiniLM-L6 a une fenêtre dure de 512 tokens (~2 ko), donc sérialiser
298+
/// plus de 8 ko vers l'isolate worker est du gaspillage RAM pur (cible
299+
/// device low-end F-Droid POCO C75, 2 Go RAM).
298300
static String _capContent(String s) {
299-
if (s.length <= AppConstants.noteContentIndexLimit) return s;
300-
return s.substring(0, AppConstants.noteContentIndexLimit);
301+
if (s.length <= AppConstants.noteContentEmbeddingLimit) return s;
302+
return s.substring(0, AppConstants.noteContentEmbeddingLimit);
301303
}
302304

303305
/// Hash 32 bits déterministe de (title | content) avec sentinelle.

lib/services/security/panic_service.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,10 @@ enum PanicStep {
105105
/// B6 v1.0.4 — wipe du modèle MiniLM bundled (~25 Mo) + cache.
106106
embedderWipe,
107107
prefsClear,
108+
109+
/// v1.1.4 — purge `cache/exports/` (sandbox cache, hors temp).
110+
/// Distinguée de [tmpPurge] pour que `PanicReport.steps` reste sans ambiguïté.
111+
exportsWipe,
108112
tmpPurge,
109113
}
110114

@@ -279,7 +283,7 @@ class PanicService {
279283
// `_purgeTempDirectory` (étape 8) ratait ce dossier qui est dans
280284
// `getApplicationCacheDirectory()` et non `getTemporaryDirectory()`.
281285
// Un export en cours de Share (Intent EXTRA_STREAM) restait en clair.
282-
await _runStep(report, PanicStep.tmpPurge, _wipeExportsCache);
286+
await _runStep(report, PanicStep.exportsWipe, _wipeExportsCache);
283287

284288
// 8. Tmp : ZIPs d'export + autres résidus. Best-effort, Android purge.
285289
await _runStep(report, PanicStep.tmpPurge, _purgeTempDirectory);

lib/ui/screens/note_editor_screen.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,10 @@ class _NoteEditorScreenState extends State<NoteEditorScreen> {
409409
// le SnackBar « Coffre re-verrouillé » et décide quoi faire.
410410
return;
411411
}
412+
// v1.1.4 (B2) — feedback haptique sur le "Terminé" explicite.
413+
// L'auto-save (debounce 500 ms) n'a pas d'haptique pour ne pas saturer
414+
// pendant la frappe ; le tap "Terminé" est lui un signal de fin clair.
415+
unawaited(HapticFeedback.mediumImpact());
412416
Navigator.of(context).pop();
413417
}
414418

0 commit comments

Comments
 (0)