
# Prisme
**Un navigateur n'est pas un afficheur de pages. C'est un lecteur qui comprend ce qu'il affiche.**
[](#-stack-technique)
[](#-stack-technique)
[](#-stack-technique)
[](#-intelligence-locale)
[](#-tests)
[](#-m%C3%A9triques)
[-orange)](#-stack-technique)
[](LICENSE)
*SwiftUI · WebKit · Foundation Models · SwiftData · NaturalLanguage — zéro serveur, zéro compte, zéro télémétrie.*
---
## Table des matières
- [Vision](#-vision)
- [Captures d'écran](#-captures-décran)
- [Fonctionnalités](#-fonctionnalités)
- [Intelligence locale](#-intelligence-locale)
- [Architecture](#-architecture)
- [Métriques](#-métriques)
- [Compiler et lancer](#-compiler-et-lancer)
- [Tests](#-tests)
- [Vie privée](#-vie-privée)
- [Feuille de route](#-feuille-de-route)
- [Auteur](#-auteur)
---
## 🔭 Vision
Le web moderne est hostile : bannières, murs de consentement, 2 000 mots de remplissage SEO
pour une réponse de 40 mots, patterns manipulateurs, pistage. Les navigateurs actuels rendent
fidèlement cette hostilité. **Prisme s'interpose** : chaque page est comprise localement avant
d'être affichée, puis re-présentée selon l'intention de l'utilisateur.
Trois règles non négociables :
| # | Règle | Concrètement |
|---|-------|--------------|
| 1 | **Rien ne quitte l'appareil par défaut** | Distillation, embeddings, historique, digest : tout est calculé on-device. Aucun serveur opéré, aucun compte. |
| 2 | **Utile dès la première session, pour un seul utilisateur** | Aucun effet de réseau requis. Le bandeau de compréhension démontre la valeur en 10 secondes. |
| 3 | **Le modèle ne remplace jamais la page** | Contenu généré toujours distinct visuellement (violet ✨), toujours ancré vers sa source DOM, page brute toujours à un geste. |
---
## 📱 Captures d'écran
| Page d'accueil | Barre à intention | Bandeau de compréhension |
|:---:|:---:|:---:|
|
|
|
|
| *Univers isolés, logo dessiné en code* | *URL, moteurs au choix, moteur par défaut* | *« ~1 min de lecture » + entrée lecteur* |
| Lecteur — Texte | Lecteur — Essentiel | Rappel d'historique | Bibliothèque |
|:---:|:---:|:---:|:---:|
|
|
|
|
|
| *Rendu natif typé* | *Zoom sémantique max* | *« Déjà visité »* | *Données natives* |
---
## ✨ Fonctionnalités
Légende : ✅ implémenté · 🚧 prévu (P1) · 🔮 plus tard (P2)
### Affichage
| État | Fonctionnalité | Description |
|:---:|---|---|
| ✅ | **Zoom sémantique** | Le pincement ne change pas la taille du texte : il change le **niveau de détail**. 4 niveaux — Texte · Sections · Plan · Essentiel — et écarter au-delà du texte ramène la page brute. *La fonctionnalité signature.* |
| ✅ | **Rendu adaptatif par type** | Le `kind` du digest sélectionne la typographie : un article passe en serif de lecture, une documentation garde son code monospace défilant. |
| ✅ | **Bandeau de compréhension** | Après chaque chargement : « Article · ~4 min de lecture » + l'essentiel généré. Un tap → lecteur. Jamais un popup. |
| 🚧 | Barre de défilement sémantique | La scrollbar devient une carte de la page. |
| 🚧 | Thème sémantique | Le mode sombre par rôle de bloc, pas par inversion. |
| 🚧 | Tiroir du bruit | Tout ce qui a été retiré, consultable. |
| 🔮 | Diff temporel | Ce qui a changé depuis la dernière visite, surligné. *(Les snapshots texte sont déjà archivés à chaque visite.)* |
### Onglets & entrée
| État | Fonctionnalité | Description |
|:---:|---|---|
| ✅ | **Barre à intention** | Un champ unique qui propose — jamais ne devine en silence : adresse directe, recherche DuckDuckGo, recherche Google, pages déjà visitées. Moteur par défaut commutable dans la carte même. |
| ✅ | **Pool de WKWebView** | Jamais une webview par onglet : pool réutilisé, état de session (`interactionState`) préservé au changement d'onglet — historique arrière/avant et défilement intacts. |
| ✅ | Sélecteur d'onglets par univers | Cartes sectionnées par conteneur, bandeau de couleur, badge de site. |
| 🚧 | Regroupement par intention | Proposé, jamais imposé. |
| 🚧 | Reprise narrative | À la réouverture : un paragraphe, pas 47 vignettes. |
| 🚧 | Onglets périssables | Durée de vie estimée, purge proposée. |
### Favoris repensés
| État | Fonctionnalité | Description |
|:---:|---|---|
| ✅ | **L'extrait** | Sélectionner un passage → « Sauver l'extrait » dans le menu d'édition natif. Gardé : le texte, la source, la date, le **contexte de section**, l'ancre DOM. |
| ✅ | **Le favori structuré** | La page sauvée en données natives : titre, essentiel, plan, type — pas un pointeur qui pourrit. Enrichi par le modèle quand disponible (marqué ✨). |
| 🚧 | Le favori vivant | Surveille sa page en tâche de fond (`BGTaskScheduler`), notifie au changement. |
| 🚧 | Le favori-question | « Combien coûte le passeport » plutôt qu'une URL — immunisé contre le lien mort. |
| 🔮 | Collections émergentes · Purge honnête | Suggestions, jamais d'action automatique. |
### Mémoire
| État | Fonctionnalité | Description |
|:---:|---|---|
| ✅ | **Historique sémantique** | Chaque page visitée est distillée et indexée localement (embeddings de phrases NaturalLanguage, par langue). Recherche en langage naturel, rappel directement dans la barre à intention (« Déjà visité »). |
| ✅ | **Snapshots texte** | Le texte extrait de chaque visite est archivé — la matière première du futur diff temporel. |
| 🚧 | Ligne du temps de sujet | Toutes les visites autour d'un thème. |
| 🚧 | Rappel proactif | « Vu en mars, c'était 899 $ » — une ligne, jamais un popup. |
### Vie privée
| État | Fonctionnalité | Description |
|:---:|---|---|
| ✅ | **Conteneurs d'identité** | Perso · Travail · Magasinage · Recherche sensible — cookies, sessions et empreinte cloisonnés par `WKWebsiteDataStore(forIdentifier:)`. Changement d'univers en un geste. |
| ✅ | **Blocage de contenu** | `WKContentRuleList` compilée (14 règles anti-traqueurs), appliquée à chaud dès compilation, hors du chemin critique de rendu. |
| ✅ | **Exclusion du sensible** | Les visites de l'univers « Recherche sensible » ne sont **jamais écrites** dans l'historique — pas stockées-puis-filtrées : jamais vues. |
| 🚧 | Détecteur de patterns manipulateurs | Faux compte à rebours, consentement pré-coché : nommés à l'écran. |
| 🚧 | Traducteur de conditions | Les CGU en trois lignes **avant** d'accepter. |
---
## 🧠 Intelligence locale
**Un LLM n'est pas une réponse à tout — c'est le dernier recours, pas le premier.**
Chaque tâche déclare son niveau ; le routeur décide. Aucun appel direct au modèle ailleurs.
| Tier | Quand | Exemples dans Prisme |
|---|---|---|
| `none` | Une heuristique suffit | Détection URL/recherche, extraction DOM, temps de lecture, plan par titres, résumés de repli (première phrase), embeddings de recherche |
| `local` | Fréquent, gratuit, illimité, hors ligne | Digest de page (type, essentiel, plan, affirmations chiffrées) via `SystemLanguageModel` |
| `cloud` | Action explicite uniquement — *pas encore câblé* | Comparaisons multi-onglets, synthèses longues (Private Cloud Compute) |
### Le Distiller — pièce centrale
Une page fait 30 000 tokens ; le modèle en accepte 4–8 000. Pipeline en 5 étapes,
déterministe sauf la dernière :
```
HTML ──▶ ① Extraction DOM (extractor.js, injecté à documentEnd, zéro IA)
──▶ ② Blocs typés (titre, paragraphe, code, tableau…) + chemins DOM
──▶ ③ Budgétisation (contextSize / tokenCount, 30 % réservés à la réponse)
──▶ ④ Condensation déterministe (jamais de coupe en pleine phrase)
──▶ ⑤ Génération guidée (@Generable PageDigest — jamais de texte libre à parser)
```
- **Cache par hash de contenu** : une même page n'est jamais distillée deux fois.
- **File d'inférence** : max 2 requêtes en vol, priorité geste > page active > arrière-plan.
- **Honnêteté structurelle** : chaque élément généré porte l'index de son bloc source ;
un résumé sans ancre ne s'affiche pas ; les refus du modèle sont des états calmes,
jamais des erreurs système brutes.
- **Dégradation totale** : sur un appareil sans Foundation Models (< A17 Pro), *tout*
fonctionne — lecteur, zoom, historique, favoris — via les chemins déterministes.
La suite de tests tourne avec le modèle indisponible.
---
## 🏛 Architecture
```
Prisme/
├─ App/ point d'entrée, modèle racine
├─ Browser/
│ ├─ Engine/ WKWebView (pool, proxy, délégués, règles) — seul accès WebKit
│ ├─ Tabs/ onglets, insight de page
│ ├─ Chrome/ barre à intention, bandeau, toolbar, sélecteur
│ └─ Reader/ zoom sémantique (4 niveaux, pincement)
├─ Intelligence/
│ ├─ Router/ choix du tier + disponibilité du modèle
│ ├─ Distiller/ extractor.js, blocs, budgets, cache ⚠️ cœur du projet
│ ├─ Schemas/ types @Generable (PageDigest…)
│ └─ Sessions/ file d'inférence à priorité
├─ Memory/
│ ├─ Index/ embeddings de phrases (NaturalLanguage)
│ ├─ Snapshots/ visites SwiftData + textes archivés
│ └─ Recall/ recherche naturelle, vue historique
├─ Library/ extraits + favoris structurés (SwiftData)
├─ Privacy/ conteneurs d'identité
└─ Design/ tokens, logo PrismMark (Canvas)
```
Règles d'or du code :
- `WKWebView` enveloppée **une seule fois** (`Browser/Engine`) ; le reste de l'app parle à `WebPageProxy`.
- **Jamais de `String` libre en sortie de modèle** — toujours `@Generable`.
- Le JS injecté vit dans des fichiers `.js` versionnés, jamais dans des chaînes Swift.
- Chaque store SwiftData a **son fichier** (`history.store`, `library.store`).
- Chaque fichier commence par l'en-tête d'auteur.
---
## 📊 Métriques
| Métrique | Valeur |
|---|---|
| Fichiers Swift | **35** |
| Lignes Swift | **~4 100** |
| JavaScript injecté | **144 lignes** (extraction + sélection + scroll-to-source) |
| Dépendances tierces | **0** — uniquement les frameworks Apple |
| Tests UI de bout en bout | **6/6 ✓** |
| Cible de déploiement | iOS 26.4+ (requis par `tokenCount(for:)`) |
| Concurrence | Swift 6, `SWIFT_STRICT_CONCURRENCY = complete` |
| Règles de blocage | 14 familles de traqueurs (troisième partie) |
Répartition par module (fichiers Swift) :
| Module | Fichiers | Rôle |
|---|:---:|---|
| `Browser/` | 14 | moteur, onglets, chrome, lecteur |
| `Intelligence/` | 7 | routeur, distiller, schémas, sessions |
| `Memory/` | 4 | index, snapshots, rappel |
| `Library/` | 3 | extraits, favoris structurés |
| `App/` · `Design/` · `Privacy/` | 6 | racine, tokens, conteneurs |
| `PrismeUITests/` | 1 | 6 tests E2E |
---
## 🛠 Compiler et lancer
Prérequis : **Xcode 26.6+** (SDK iOS 26.4+), [XcodeGen](https://github.com/yonaskolb/XcodeGen).
```bash
git clone https://git.spboucher.ai/prisme.git
cd prisme
xcodegen generate # génère Prisme.xcodeproj depuis project.yml
open Prisme.xcodeproj # ⌘R sur un simulateur iPhone
```
En ligne de commande :
```bash
xcodebuild -project Prisme.xcodeproj -scheme Prisme \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
build CODE_SIGNING_ALLOWED=NO
```
L'icône se régénère depuis le SVG source :
```bash
rsvg-convert -w 1024 -h 1024 Design/icon/prisme-icon.svg \
-o Prisme/App/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png
```
> **Note simulateur** : `SystemLanguageModel` s'y déclare disponible mais échoue à
> l'inférence (assets absents). Les chemins déterministes prennent le relais — c'est
> le comportement attendu. Pour les digests réels : iPhone A17 Pro+ avec Apple
> Intelligence activée.
---
## ✅ Tests
Six tests XCUITest de bout en bout — réseau réel, gestes réels, zéro mock :
| Test | Ce qu'il prouve |
|---|---|
| `testNavigateToURLLoadsPage` | Saisie → proposition « Aller sur » → la page charge, cadenas + domaine |
| `testSearchProposalsOfferBothEngines` | Chaque requête propose DuckDuckGo **et** Google |
| `testGoogleSearchLoadsGoogle` | La recherche Google aboutit sur Google |
| `testReaderSemanticZoomLevels` | Bandeau → lecteur → 4 niveaux → retour page brute |
| `testSemanticHistoryRecallInAddressBar` | Visite → distillation → rappel « Déjà visité » → réouverture |
| `testStructuredFavoriteFromReaderAppearsInLibrary` | Signet lecteur → favori en données natives dans la bibliothèque |
```bash
xcodebuild -project Prisme.xcodeproj -scheme Prisme \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
test CODE_SIGNING_ALLOWED=NO
```
---
## 🔒 Vie privée
- **Aucun serveur.** Prisme n'opère aucun backend ; rien à héberger, rien qui fuite.
- **Aucun compte, aucune télémétrie, aucun identifiant.**
- **IA 100 % on-device** ; l'escalade cloud (Private Cloud Compute) sera explicite,
visible, et interdite sans exception sur le contenu des conteneurs sensibles.
- **Recherche par défaut : DuckDuckGo** ; Google proposé à chaque requête, jamais imposé.
- **L'univers « Recherche sensible » n'existe pas pour la mémoire** : ni historique,
ni index, ni snapshot.
---
## 🗺 Feuille de route
Ordre de construction strict — chaque étape solide avant la suivante :
- [x] **1. Navigateur nu, excellent** — onglets, pool, gestes, blocage, conteneurs
- [x] **2. Distiller + cache** — le cœur invisible dont tout dépend
- [x] **3. Zoom sémantique + rendu adaptatif** — la démo
- [x] **4. Historique sémantique** — la valeur qui s'accumule
- [x] **5. Favoris repensés (P0)** — l'extrait, le favori structuré
- [ ] 5bis. **Favori vivant** (`BGTaskScheduler`) + favori-question
- [ ] **6. Vie privée avancée** — patterns manipulateurs, traducteur de conditions
- [ ] **7. Agent** — portée étroite, jamais d'action irréversible sans confirmation
---
## 👤 Auteur
**Simon-Pierre Boucher** — [contact@spboucher.ai](mailto:contact@spboucher.ai)
Licence [MIT](LICENSE) © 2026 Simon-Pierre Boucher
Construit en SwiftUI, distillé sur l'appareil, rien ne quitte votre iPhone.