spb/prisme Public MIT
Navigateur iOS intelligent — chaque page comprise localement avant d'être affichée. SwiftUI · WebKit · Foundation Models, 100% on-device.
Swift 96.7%
JavaScript 3.3%
1<!--2 ─────────────────────────────────────────────3 Prisme — Navigateur iOS intelligent4 ─────────────────────────────────────────────5 Author : Simon-Pierre Boucher6 Contact : contact@spboucher.ai7 File : README.md8 Purpose : Documentation principale du projet9 License : MIT © Simon-Pierre Boucher10 ─────────────────────────────────────────────11-->1213<div align="center">1415<img src="docs/screenshots/icon.png" width="160" alt="Icône Prisme" />1617# Prisme1819**Un navigateur n'est pas un afficheur de pages. C'est un lecteur qui comprend ce qu'il affiche.**2021[](#-stack-technique)22[](#-stack-technique)23[](#-stack-technique)24[](#-intelligence-locale)25[](#-tests)26[](#-m%C3%A9triques)27[-orange)](#-stack-technique)28[](LICENSE)2930*SwiftUI · WebKit · Foundation Models · SwiftData · NaturalLanguage — zéro serveur, zéro compte, zéro télémétrie.*3132</div>3334---3536## Table des matières3738- [Vision](#-vision)39- [Captures d'écran](#-captures-décran)40- [Fonctionnalités](#-fonctionnalités)41- [Intelligence locale](#-intelligence-locale)42- [Architecture](#-architecture)43- [Métriques](#-métriques)44- [Compiler et lancer](#-compiler-et-lancer)45- [Tests](#-tests)46- [Vie privée](#-vie-privée)47- [Feuille de route](#-feuille-de-route)48- [Auteur](#-auteur)4950---5152## 🔭 Vision5354Le web moderne est hostile : bannières, murs de consentement, 2 000 mots de remplissage SEO55pour une réponse de 40 mots, patterns manipulateurs, pistage. Les navigateurs actuels rendent56fidèlement cette hostilité. **Prisme s'interpose** : chaque page est comprise localement avant57d'être affichée, puis re-présentée selon l'intention de l'utilisateur.5859Trois règles non négociables :6061| # | Règle | Concrètement |62|---|-------|--------------|63| 1 | **Rien ne quitte l'appareil par défaut** | Distillation, embeddings, historique, digest : tout est calculé on-device. Aucun serveur opéré, aucun compte. |64| 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. |65| 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. |6667---6869## 📱 Captures d'écran7071| Page d'accueil | Barre à intention | Bandeau de compréhension |72|:---:|:---:|:---:|73| <img src="docs/screenshots/start-page.png" width="240" alt="Page d'accueil avec logo prisme et univers" /> | <img src="docs/screenshots/intent-bar.png" width="240" alt="Propositions : adresse directe, Google, DuckDuckGo" /> | <img src="docs/screenshots/insight-strip.png" width="240" alt="Bandeau : temps de lecture et bouton Lire" /> |74| *Univers isolés, logo dessiné en code* | *URL, moteurs au choix, moteur par défaut* | *« ~1 min de lecture » + entrée lecteur* |7576| Lecteur — Texte | Lecteur — Essentiel | Rappel d'historique | Bibliothèque |77|:---:|:---:|:---:|:---:|78| <img src="docs/screenshots/reader-texte.png" width="180" alt="Rendu natif des blocs de la page" /> | <img src="docs/screenshots/reader-essentiel.png" width="180" alt="Niveau essentiel : une phrase" /> | <img src="docs/screenshots/history-recall.png" width="180" alt="Page retrouvée en tapant dans la barre" /> | <img src="docs/screenshots/library.png" width="180" alt="Favori structuré en données natives" /> |79| *Rendu natif typé* | *Zoom sémantique max* | *« Déjà visité »* | *Données natives* |8081---8283## ✨ Fonctionnalités8485Légende : ✅ implémenté · 🚧 prévu (P1) · 🔮 plus tard (P2)8687### Affichage8889| État | Fonctionnalité | Description |90|:---:|---|---|91| ✅ | **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.* |92| ✅ | **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. |93| ✅ | **Bandeau de compréhension** | Après chaque chargement : « Article · ~4 min de lecture » + l'essentiel généré. Un tap → lecteur. Jamais un popup. |94| 🚧 | Barre de défilement sémantique | La scrollbar devient une carte de la page. |95| 🚧 | Thème sémantique | Le mode sombre par rôle de bloc, pas par inversion. |96| 🚧 | Tiroir du bruit | Tout ce qui a été retiré, consultable. |97| 🔮 | Diff temporel | Ce qui a changé depuis la dernière visite, surligné. *(Les snapshots texte sont déjà archivés à chaque visite.)* |9899### Onglets & entrée100101| État | Fonctionnalité | Description |102|:---:|---|---|103| ✅ | **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. |104| ✅ | **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. |105| ✅ | Sélecteur d'onglets par univers | Cartes sectionnées par conteneur, bandeau de couleur, badge de site. |106| 🚧 | Regroupement par intention | Proposé, jamais imposé. |107| 🚧 | Reprise narrative | À la réouverture : un paragraphe, pas 47 vignettes. |108| 🚧 | Onglets périssables | Durée de vie estimée, purge proposée. |109110### Favoris repensés111112| État | Fonctionnalité | Description |113|:---:|---|---|114| ✅ | **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. |115| ✅ | **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é ✨). |116| 🚧 | Le favori vivant | Surveille sa page en tâche de fond (`BGTaskScheduler`), notifie au changement. |117| 🚧 | Le favori-question | « Combien coûte le passeport » plutôt qu'une URL — immunisé contre le lien mort. |118| 🔮 | Collections émergentes · Purge honnête | Suggestions, jamais d'action automatique. |119120### Mémoire121122| État | Fonctionnalité | Description |123|:---:|---|---|124| ✅ | **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é »). |125| ✅ | **Snapshots texte** | Le texte extrait de chaque visite est archivé — la matière première du futur diff temporel. |126| 🚧 | Ligne du temps de sujet | Toutes les visites autour d'un thème. |127| 🚧 | Rappel proactif | « Vu en mars, c'était 899 $ » — une ligne, jamais un popup. |128129### Vie privée130131| État | Fonctionnalité | Description |132|:---:|---|---|133| ✅ | **Conteneurs d'identité** | Perso · Travail · Magasinage · Recherche sensible — cookies, sessions et empreinte cloisonnés par `WKWebsiteDataStore(forIdentifier:)`. Changement d'univers en un geste. |134| ✅ | **Blocage de contenu** | `WKContentRuleList` compilée (14 règles anti-traqueurs), appliquée à chaud dès compilation, hors du chemin critique de rendu. |135| ✅ | **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. |136| 🚧 | Détecteur de patterns manipulateurs | Faux compte à rebours, consentement pré-coché : nommés à l'écran. |137| 🚧 | Traducteur de conditions | Les CGU en trois lignes **avant** d'accepter. |138139---140141## 🧠 Intelligence locale142143**Un LLM n'est pas une réponse à tout — c'est le dernier recours, pas le premier.**144Chaque tâche déclare son niveau ; le routeur décide. Aucun appel direct au modèle ailleurs.145146| Tier | Quand | Exemples dans Prisme |147|---|---|---|148| `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 |149| `local` | Fréquent, gratuit, illimité, hors ligne | Digest de page (type, essentiel, plan, affirmations chiffrées) via `SystemLanguageModel` |150| `cloud` | Action explicite uniquement — *pas encore câblé* | Comparaisons multi-onglets, synthèses longues (Private Cloud Compute) |151152### Le Distiller — pièce centrale153154Une page fait 30 000 tokens ; le modèle en accepte 4–8 000. Pipeline en 5 étapes,155déterministe sauf la dernière :156157```158HTML ──▶ ① Extraction DOM (extractor.js, injecté à documentEnd, zéro IA)159 ──▶ ② Blocs typés (titre, paragraphe, code, tableau…) + chemins DOM160 ──▶ ③ Budgétisation (contextSize / tokenCount, 30 % réservés à la réponse)161 ──▶ ④ Condensation déterministe (jamais de coupe en pleine phrase)162 ──▶ ⑤ Génération guidée (@Generable PageDigest — jamais de texte libre à parser)163```164165- **Cache par hash de contenu** : une même page n'est jamais distillée deux fois.166- **File d'inférence** : max 2 requêtes en vol, priorité geste > page active > arrière-plan.167- **Honnêteté structurelle** : chaque élément généré porte l'index de son bloc source ;168 un résumé sans ancre ne s'affiche pas ; les refus du modèle sont des états calmes,169 jamais des erreurs système brutes.170- **Dégradation totale** : sur un appareil sans Foundation Models (< A17 Pro), *tout*171 fonctionne — lecteur, zoom, historique, favoris — via les chemins déterministes.172 La suite de tests tourne avec le modèle indisponible.173174---175176## 🏛 Architecture177178```179Prisme/180├─ App/ point d'entrée, modèle racine181├─ Browser/182│ ├─ Engine/ WKWebView (pool, proxy, délégués, règles) — seul accès WebKit183│ ├─ Tabs/ onglets, insight de page184│ ├─ Chrome/ barre à intention, bandeau, toolbar, sélecteur185│ └─ Reader/ zoom sémantique (4 niveaux, pincement)186├─ Intelligence/187│ ├─ Router/ choix du tier + disponibilité du modèle188│ ├─ Distiller/ extractor.js, blocs, budgets, cache ⚠️ cœur du projet189│ ├─ Schemas/ types @Generable (PageDigest…)190│ └─ Sessions/ file d'inférence à priorité191├─ Memory/192│ ├─ Index/ embeddings de phrases (NaturalLanguage)193│ ├─ Snapshots/ visites SwiftData + textes archivés194│ └─ Recall/ recherche naturelle, vue historique195├─ Library/ extraits + favoris structurés (SwiftData)196├─ Privacy/ conteneurs d'identité197└─ Design/ tokens, logo PrismMark (Canvas)198```199200Règles d'or du code :201202- `WKWebView` enveloppée **une seule fois** (`Browser/Engine`) ; le reste de l'app parle à `WebPageProxy`.203- **Jamais de `String` libre en sortie de modèle** — toujours `@Generable`.204- Le JS injecté vit dans des fichiers `.js` versionnés, jamais dans des chaînes Swift.205- Chaque store SwiftData a **son fichier** (`history.store`, `library.store`).206- Chaque fichier commence par l'en-tête d'auteur.207208---209210## 📊 Métriques211212| Métrique | Valeur |213|---|---|214| Fichiers Swift | **35** |215| Lignes Swift | **~4 100** |216| JavaScript injecté | **144 lignes** (extraction + sélection + scroll-to-source) |217| Dépendances tierces | **0** — uniquement les frameworks Apple |218| Tests UI de bout en bout | **6/6 ✓** |219| Cible de déploiement | iOS 26.4+ (requis par `tokenCount(for:)`) |220| Concurrence | Swift 6, `SWIFT_STRICT_CONCURRENCY = complete` |221| Règles de blocage | 14 familles de traqueurs (troisième partie) |222223Répartition par module (fichiers Swift) :224225| Module | Fichiers | Rôle |226|---|:---:|---|227| `Browser/` | 14 | moteur, onglets, chrome, lecteur |228| `Intelligence/` | 7 | routeur, distiller, schémas, sessions |229| `Memory/` | 4 | index, snapshots, rappel |230| `Library/` | 3 | extraits, favoris structurés |231| `App/` · `Design/` · `Privacy/` | 6 | racine, tokens, conteneurs |232| `PrismeUITests/` | 1 | 6 tests E2E |233234---235236## 🛠 Compiler et lancer237238Prérequis : **Xcode 26.6+** (SDK iOS 26.4+), [XcodeGen](https://github.com/yonaskolb/XcodeGen).239240```bash241git clone https://git.spboucher.ai/prisme.git242cd prisme243xcodegen generate # génère Prisme.xcodeproj depuis project.yml244open Prisme.xcodeproj # ⌘R sur un simulateur iPhone245```246247En ligne de commande :248249```bash250xcodebuild -project Prisme.xcodeproj -scheme Prisme \251 -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \252 build CODE_SIGNING_ALLOWED=NO253```254255L'icône se régénère depuis le SVG source :256257```bash258rsvg-convert -w 1024 -h 1024 Design/icon/prisme-icon.svg \259 -o Prisme/App/Assets.xcassets/AppIcon.appiconset/AppIcon-1024.png260```261262> **Note simulateur** : `SystemLanguageModel` s'y déclare disponible mais échoue à263> l'inférence (assets absents). Les chemins déterministes prennent le relais — c'est264> le comportement attendu. Pour les digests réels : iPhone A17 Pro+ avec Apple265> Intelligence activée.266267---268269## ✅ Tests270271Six tests XCUITest de bout en bout — réseau réel, gestes réels, zéro mock :272273| Test | Ce qu'il prouve |274|---|---|275| `testNavigateToURLLoadsPage` | Saisie → proposition « Aller sur » → la page charge, cadenas + domaine |276| `testSearchProposalsOfferBothEngines` | Chaque requête propose DuckDuckGo **et** Google |277| `testGoogleSearchLoadsGoogle` | La recherche Google aboutit sur Google |278| `testReaderSemanticZoomLevels` | Bandeau → lecteur → 4 niveaux → retour page brute |279| `testSemanticHistoryRecallInAddressBar` | Visite → distillation → rappel « Déjà visité » → réouverture |280| `testStructuredFavoriteFromReaderAppearsInLibrary` | Signet lecteur → favori en données natives dans la bibliothèque |281282```bash283xcodebuild -project Prisme.xcodeproj -scheme Prisme \284 -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \285 test CODE_SIGNING_ALLOWED=NO286```287288---289290## 🔒 Vie privée291292- **Aucun serveur.** Prisme n'opère aucun backend ; rien à héberger, rien qui fuite.293- **Aucun compte, aucune télémétrie, aucun identifiant.**294- **IA 100 % on-device** ; l'escalade cloud (Private Cloud Compute) sera explicite,295 visible, et interdite sans exception sur le contenu des conteneurs sensibles.296- **Recherche par défaut : DuckDuckGo** ; Google proposé à chaque requête, jamais imposé.297- **L'univers « Recherche sensible » n'existe pas pour la mémoire** : ni historique,298 ni index, ni snapshot.299300---301302## 🗺 Feuille de route303304Ordre de construction strict — chaque étape solide avant la suivante :305306- [x] **1. Navigateur nu, excellent** — onglets, pool, gestes, blocage, conteneurs307- [x] **2. Distiller + cache** — le cœur invisible dont tout dépend308- [x] **3. Zoom sémantique + rendu adaptatif** — la démo309- [x] **4. Historique sémantique** — la valeur qui s'accumule310- [x] **5. Favoris repensés (P0)** — l'extrait, le favori structuré311- [ ] 5bis. **Favori vivant** (`BGTaskScheduler`) + favori-question312- [ ] **6. Vie privée avancée** — patterns manipulateurs, traducteur de conditions313- [ ] **7. Agent** — portée étroite, jamais d'action irréversible sans confirmation314315---316317## 👤 Auteur318319**Simon-Pierre Boucher** — [contact@spboucher.ai](mailto:contact@spboucher.ai)320321Licence [MIT](LICENSE) © 2026 Simon-Pierre Boucher322323<div align="center">324<sub>Construit en SwiftUI, distillé sur l'appareil, rien ne quitte votre iPhone.</sub>325</div>326