Icône Prisme # Prisme **Un navigateur n'est pas un afficheur de pages. C'est un lecteur qui comprend ce qu'il affiche.** [![Plateforme](https://img.shields.io/badge/iOS-26.4%2B-0A84FF?logo=apple&logoColor=white)](#-stack-technique) [![Swift](https://img.shields.io/badge/Swift-6.0-F05138?logo=swift&logoColor=white)](#-stack-technique) [![UI](https://img.shields.io/badge/SwiftUI-100%25-8B5CF6)](#-stack-technique) [![IA](https://img.shields.io/badge/IA-100%25%20on--device-34C759)](#-intelligence-locale) [![Tests](https://img.shields.io/badge/tests%20UI-6%2F6%20✓-success)](#-tests) [![Dépendances](https://img.shields.io/badge/d%C3%A9pendances%20tierces-0-success)](#-m%C3%A9triques) [![Concurrence](https://img.shields.io/badge/concurrence-stricte%20(Swift%206)-orange)](#-stack-technique) [![Licence](https://img.shields.io/badge/licence-MIT-lightgrey)](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 | |:---:|:---:|:---:| | Page d'accueil avec logo prisme et univers | Propositions : adresse directe, Google, DuckDuckGo | Bandeau : temps de lecture et bouton Lire | | *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 des blocs de la page | Niveau essentiel : une phrase | Page retrouvée en tapant dans la barre | Favori structuré en données natives | | *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.