CLAUDE.md — Prisme
Navigateur iOS intelligent. SwiftUI + WebKit + Foundation Models. Ce fichier est la source de vérité du projet. Le lire avant toute modification.
1. Thèse produit
Un navigateur n'est pas un afficheur de pages. C'est un lecteur qui comprend ce qu'il affiche.
Le web moderne est hostile : bannières, murs de consentement, 2000 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 :
- Rien ne quitte l'appareil par défaut. Le contenu des pages est traité on-device. L'escalade vers Private Cloud Compute est explicite, visible, et jamais automatique sur du contenu marqué sensible.
- Utile pour un seul utilisateur, dès la première session. Aucune fonctionnalité ne dépend d'un effet de réseau, d'un compte, ou d'un serveur qu'on opère.
- Le modèle ne remplace jamais la page. Il l'augmente, la résume, la range. L'utilisateur peut toujours accéder au HTML brut en un geste. Une hallucination ne doit jamais être indiscernable du contenu réel — voir §7.
Ce que Prisme n'est pas
- Pas un chatbot avec une webview collée à côté (Dia, Comet, Atlas occupent déjà ce terrain).
- Pas un agent qui navigue à ta place pendant que tu regardes. L'agent existe (§6) mais il est en arrière-plan, pas au centre.
- Pas un navigateur qui demande de changer ses habitudes avant de donner de la valeur. Arc est mort de ça.
2. Stack & contraintes
| Élément | Choix | Note |
|---|---|---|
| UI | SwiftUI, iOS 27+ | @Observable, pas d'ObservableObject |
| Moteur web | WKWebView |
pas d'alternative viable sur iOS |
| IA locale | FoundationModels |
SystemLanguageModel.default |
| IA lourde | PrivateCloudComputeLanguageModel |
32K contexte, quota par utilisateur |
| Persistance | SwiftData | + fichiers pour les snapshots texte |
| Recherche | Index vectoriel local | voir §5 |
| Tâches fond | BGTaskScheduler |
favoris vivants, veilles |
| Intégrations | App Intents | export vers Notes/Rappels/Calendrier |
Contraintes matérielles à respecter partout
- Foundation Models exige A17 Pro ou plus récent. Sur iPhone 14 et antérieur,
SystemLanguageModel.default.availabilityrenvoie indisponible. Toute fonctionnalité IA doit dégrader proprement : Prisme reste un excellent navigateur sans IA. Jamais d'écran d'erreur, jamais de bouton mort. - Contexte : 4096 tokens (iOS 26) / 8192 (iOS 27, appareils récents). C'est la contrainte structurante du projet. Une page web fait couramment 30 000 tokens. Voir §4 : on ne passe jamais une page brute au modèle.
- L'inférence est sérialisée sur le Neural Engine. Des sessions parallèles sont permises par l'API mais s'exécutent en série. Budgéter en conséquence : jamais plus de 2 requêtes en vol, file d'attente avec priorité.
- Batterie. L'inférence continue tue un téléphone. Toute tâche non déclenchée par l'utilisateur passe par le budget d'énergie (§8).
Vérifier avant de coder
contextSize et tokenCount(for:) existent depuis iOS 26.4. Toujours faire un preflight de tokens avant d'appeler le modèle — l'échec de dépassement est abrupt et tue la session.
let model = SystemLanguageModel.default
let budget = try await model.contextSize
let cost = try await model.tokenCount(for: prompt)
guard cost < budget - reserveForResponse else { /* condenser */ }Réserver systématiquement ~30 % du contexte pour la réponse. Une erreur classique : un prompt à 4092 tokens échoue quand même, parce que le modèle n'a plus la place de répondre.
3. Architecture
Prisme/
├─ App/ point d'entrée, scènes, raccourcis
├─ Browser/
│ ├─ Engine/ WKWebView, pool, delegates, règles de contenu
│ ├─ Tabs/ modèle d'onglets, groupes, sessions
│ └─ Chrome/ barre d'adresse, gestes, navigation
├─ Intelligence/
│ ├─ Router/ choix du modèle (on-device / PCC / aucun)
│ ├─ Distiller/ HTML → structure compacte ⚠️ cœur du projet
│ ├─ Schemas/ tous les types @Generable
│ ├─ Tools/ Tool protocol : accès onglets, historique, page
│ └─ Sessions/ gestion du cycle de vie, reprise après dépassement
├─ Memory/
│ ├─ Index/ index sémantique local
│ ├─ Snapshots/ versions texte des pages visitées
│ └─ Recall/ recherche floue, rappels proactifs
├─ Library/ favoris, extraits, collections
├─ Privacy/ conteneurs d'identité, blocage, détection de patterns
└─ Design/ tokens, typographie, thèmes sémantiques Le routeur (Intelligence/Router)
Chaque tâche déclare son niveau. Le routeur choisit. Aucun appel direct au modèle ailleurs dans le code.
enum Tier {
case none // heuristique pure, zéro IA
case local // on-device, gratuit, illimité, hors ligne
case cloud // PCC : 32K, raisonnement — quota utilisateur, consentement
}Règles de routage :
- Tout ce qui est fréquent va en
local. Classification, extraction, résumé de section, titre d'onglet, tag de favori. C'est gratuit et illimité — c'est là qu'est l'avantage concurrentiel : un concurrent sur API cloud ne peut pas se permettre ce volume. clouduniquement sur action explicite : comparaison multi-onglets, question complexe, synthèse d'un long document. Toujours avec une indication visuelle que ça sort de l'appareil.nonedès qu'une heuristique suffit. Ne pas appeler un LLM pour détecter un mur de cookies : un sélecteur CSS le fait mieux, en 0 ms, avec 0 % d'hallucination. Un LLM n'est pas une réponse à tout — c'est le dernier recours, pas le premier.- Contenu marqué sensible (santé, finance, tout ce qui est dans un conteneur privé) :
cloudinterdit, sans exception.
4. Le Distiller — pièce centrale
Problème : une page fait 30 000 tokens, le modèle en accepte 8 000. Tout le projet tient sur la qualité de cette réduction.
Pipeline, dans l'ordre. Chaque étape est déterministe sauf la dernière.
- Extraction DOM (JS injecté,
WKUserScriptaudocumentEnd) — retire nav, footer, pub, scripts, commentaires. Algorithme type Readability, en dur, pas d'IA. - Structuration — produit un arbre de blocs typés : titre, section, paragraphe, code, tableau, image, formulaire. Conserve les offsets DOM pour pouvoir remonter à l'élément d'origine (indispensable pour §5 et le zoom sémantique).
- Budgétisation — mesure les tokens par bloc, alloue le budget par importance (profondeur de titre, position, densité de liens).
- Condensation hiérarchique — si dépassement : résumer les sections les moins prioritaires en
local, garder intactes les prioritaires. Jamais de troncature brutale au milieu d'une phrase. - Sortie structurée —
@Generable, jamais de texte libre à parser.
@Generable
struct PageDigest {
@Guide(description: "Type de page")
let kind: PageKind // article, doc, forum, boutique, appli, portail, formulaire
@Guide(description: "Réponse à la question implicite de la page, max 2 phrases")
let gist: String
@Guide(description: "Sections dans l'ordre du document", .count(3...12))
let outline: [Section]
@Guide(description: "Affirmations chiffrées ou datées, avec leur offset DOM")
let claims: [Claim]
let hostility: HostilityReport // voir §8
}Règles de fer du Distiller
- Ne jamais envoyer de HTML brut au modèle. Ça brûle le contexte en balises et dégrade la qualité.
- Toujours conserver l'offset DOM de chaque élément produit. Sans ça, impossible de lier un résumé à sa source — et donc impossible de vérifier une hallucination. C'est la contrainte la plus facile à oublier et la plus coûteuse à rattraper.
- Cache agressif. Un digest est indexé par hash du contenu extrait. Une même page ne doit jamais être distillée deux fois. Les mêmes pages reviennent constamment.
- Le digest est un artefact durable, pas un intermédiaire jetable : il alimente l'historique (§5), les favoris (§5) et le diff temporel.
5. Fonctionnalités par domaine
Priorité : P0 = MVP, P1 = v1, P2 = après.
Affichage
P0Zoom sémantique. Le pincement ne change pas la taille du texte : il change le niveau de détail. Écarté au max = page complète. Pincé d'un cran = paragraphes condensés. Deux crans = plan de la page. Trois = une phrase. Le geste le plus familier de l'iPhone, remappé sur la compréhension. C'est la fonctionnalité signature. Si une seule chose doit être parfaite, c'est celle-là. Toutes les transitions sont interpolées, pas des sauts d'écran — l'utilisateur doit voir le texte se contracter.P0Rendu adaptatif par type. Lekinddu digest sélectionne un gabarit SwiftUI natif. Un article devient une vraie page de lecture ; une doc technique garde son code et sa nav ; un forum devient un fil hiérarchisé. Pas un « mode lecture » unique appliqué de force.P1Barre de défilement sémantique. La scrollbar devient une carte de la page : sections nommées, position des tableaux/code/images. On saute à une idée, pas à un pourcentage.P1Thème sémantique. Le mode sombre n'inverse pas les couleurs : le modèle attribue un rôle à chaque bloc (corps, citation, code, avertissement) et applique le thème natif de Prisme. Fin des sites illisibles la nuit.P1Tiroir du bruit. Tout ce qui a été retiré est empilé dans un tiroir consultable. Transparence totale : l'utilisateur voit ce que la page voulait lui faire faire. Aussi une soupape de sécurité quand l'extraction rate.P2Diff temporel. Snapshot texte à chaque visite. Au retour : ce qui a changé est surligné. Puissant sur les pages de prix, les politiques, les docs.P2Lecture d'images. Vision on-device (iOS 27) : un graphique en image, une capture d'écran, un menu en photo deviennent du texte interrogeable.
Onglets
P0Regroupement par intention. Les onglets se rangent par ce que tu es en train de faire (« tu magasines un vélo », « tu débogues du Swift »), pas par domaine. Le regroupement est proposé, jamais imposé — un onglet qui bouge tout seul est une trahison.P0Reprise narrative. À la réouverture : pas 47 vignettes, un paragraphe. « Tu comparais trois assurances. Tu avais retenu X. Il te restait à vérifier les franchises. »P1Onglets périssables. Le modèle estime la durée de vie utile de chaque onglet. Une recette meurt à la fermeture ; une doc de travail survit. Purge proposée, jamais silencieuse.P2Préchargement spéculatif. Les 2-3 liens les plus probables sont préchargés et pré-distillés. Le clic devient instantané. Plafonné par le budget d'énergie (§8) et désactivé sur données cellulaires.
Favoris — à repenser complètement
Le favori est une relique de 1995 : un pointeur vers une URL, qui pourrit. On le remplace par quatre objets.
P0L'extrait. On sélectionne un paragraphe, c'est ça qui est sauvé — avec sa source, sa date, et son contexte de section. La plupart du temps on ne veut pas la page, on veut le passage.P0Le favori structuré. Une recette sauvée devient ingrédients + étapes en données natives (guided generation), pas une page. Une fiche produit devient prix + specs + vendeur. Le contenu est libéré de sa mise en page.P1Le favori vivant. Le favori surveille sa page en tâche de fond et notifie au changement : prix, disponibilité, mise à jour d'une doc, modification d'une politique. La fonctionnalité la plus vendeuse du lot — un favori qui travaille pour toi.P1Le favori-question. On enregistre « combien coûte le renouvellement du passeport » plutôt qu'une URL. Si l'URL meurt, le favori se re-résout tout seul. Immunisé contre le lien mort.P2Collections émergentes. Quand un thème apparaît dans les sauvegardes, Prisme propose une collection. Suggestion, jamais action automatique.P2Purge honnête. « 340 favoris jamais rouverts depuis 2 ans. » Proposition d'archivage groupé.
Mémoire
P0Historique sémantique. Chaque page visitée est distillée et indexée localement. Recherche en langage naturel : « le site avec la recette de ramen vu au printemps ». C'est ici que le modèle local gratuit écrase toute solution cloud : le volume serait impayable en API, et l'intimité de l'historique rend l'envoi hors appareil inacceptable.P1Ligne du temps de sujet. Toutes les visites autour d'un thème, regroupées chronologiquement. On retrace une recherche étalée sur des semaines.P1Rappel proactif. Retour sur une fiche produit : « vu en mars, c'était 899 $ ». Silencieux, une ligne, jamais un popup.
Vie privée
P0Conteneurs d'identité. Travail / perso / magasinage / recherche sensible. Cookies, sessions et empreinte entièrement cloisonnés viaWKWebsiteDataStore(forIdentifier:). Changement d'univers en un geste. Un site ne peut pas relier tes vies.P0Blocage de contenu.WKContentRuleListcompilé, mis à jour, mesurable. Non négociable pour la performance et la vie privée.P1Détecteur de patterns manipulateurs. Faux compte à rebours, désabonnement caché, prix barré mensonger, consentement pré-coché : nommés à l'écran. Éducatif et défensif.P1Traducteur de conditions. Le mur de cookies et les CGU résumés en trois lignes avant d'accepter, avec ce qui est réellement cédé.
Entrée
P0Barre à intention. Un champ unique qui distingue URL, recherche, question, et commande. Il ne devine pas en silence : il propose l'interprétation, l'utilisateur confirme d'un geste.P1Export structuré. Une page → Rappels, Calendrier, Notes, avec les bons champs remplis (App Intents). Une page d'événement devient une entrée d'agenda correcte, pas un lien.P2Écoute. Le digest lu à voix haute. Utile en déplacement, et c'est la seule façon d'« emporter » un long article.
6. Agent (P2 — pas avant que le reste soit excellent)
Portée volontairement étroite. Un agent qui échoue une fois sur cinq est pire qu'aucun agent.
Autorisé : comparer des pages déjà ouvertes ; surveiller des favoris vivants ; extraire et remplir un formulaire avec des données confirmées par l'utilisateur.
Interdit : toute action irréversible sans confirmation explicite — achat, envoi, suppression, publication. Aucune exception, aucun mode « expert » qui la contourne.
Implémentation par Tool :
struct OpenTabsTool: Tool {
let name = "lire_onglets_ouverts"
let description = "Retourne le digest des onglets actuellement ouverts"
@Generable struct Arguments {
@Guide(description: "Filtre optionnel sur le titre ou le domaine")
let filter: String?
}
func call(arguments: Arguments) async throws -> String {
// Retourner les DIGESTS, jamais le HTML.
// Plafonner : 3 onglets max, ~400 tokens chacun.
}
}Règle : un outil retourne toujours du contenu déjà budgété. Un outil qui renvoie une page entière fait exploser le contexte et tue la session.
7. Honnêteté du modèle
Non négociable. Un navigateur qui invente est un navigateur inutilisable.
- Distinction visuelle permanente entre le contenu de la page et le contenu généré. Un traitement typographique et chromatique dédié, cohérent partout. Jamais de texte généré qui ressemble au texte source.
- Tout résumé est cliquable vers sa source dans le DOM. C'est à ça que servent les offsets du §4. Un résumé sans ancre ne s'affiche pas.
- Sur incertitude, on dit qu'on ne sait pas. Pas de comblement. Le modèle 3B ne connaît pas le monde — il traite le texte qu'on lui donne. Toute question dépassant la page doit être routée ou refusée, jamais devinée.
- Un geste, toujours disponible, ramène la page brute. Si l'utilisateur ne fait pas confiance à ce qu'il voit, il doit pouvoir vérifier en une seconde.
- Les garde-fous de Foundation Models peuvent refuser un contenu légitime (faux positifs, améliorés en iOS 26.4 mais présents). Gérer le refus comme un état normal : afficher la page brute, sans message d'erreur alarmant.
8. Performance & énergie
Un navigateur se juge d'abord sur la vitesse d'affichage. L'IA ne doit jamais retarder le rendu.
- La page s'affiche immédiatement. La distillation démarre après
didFinish, en priorité basse, et l'enrichissement arrive progressivement. - Pool de
WKWebViewréutilisées. Ne jamais en instancier une par onglet. - Budget d'énergie global : compteur d'inférences par heure, seuil bas en mode économie, préchargement spéculatif coupé en premier, tâches de fond suspendues sous 20 % de batterie.
- File d'inférence à priorité : geste utilisateur > page active > arrière-plan. Annulation immédiate si l'utilisateur quitte l'onglet.
- Cible : aucune régression perceptible du temps d'affichage contre Safari. Mesurée à chaque build.
9. Conventions de code
- SwiftUI uniquement.
@Observable. Pas de Combine sauf pour les ponts WebKit. - Swift 6, concurrence stricte. Les états d'onglet sont
@MainActor. L'inférence et la distillation sont hors du main actor. WKWebViewenveloppée dansUIViewRepresentable, une seule fois, dansBrowser/Engine. Aucun accès direct ailleurs.- Jamais de
Stringlibre en sortie de modèle. Toujours@Generable. Une réponse à parser au regex est un bug. - Le JS injecté vit dans des fichiers
.jsversionnés, jamais dans des chaînes Swift. - Chaque fonctionnalité IA a un chemin de repli non-IA testé. La suite de tests tourne avec le modèle désactivé.
- Nommage utilisateur en français ; code, commentaires et commits en anglais.
- Chaque fichier de code (Swift, JS, YAML…) commence par un en-tête d'auteur : nom du fichier, projet, puis
Author: Simon-Pierre Boucher <contact@spboucher.ai>. (Exception : JSON, qui n'accepte pas de commentaires.)
10. Ordre de construction
Ne pas dévier. Chaque étape doit être solide avant la suivante.
- Navigateur nu, excellent. Onglets, gestes, blocage de contenu, conteneurs d'identité. Zéro IA. S'il n'est pas déjà agréable ici, l'IA ne le sauvera pas.
- Distiller + cache. Rien de visible pour l'utilisateur, mais tout le reste en dépend.
- Zoom sémantique + rendu adaptatif. La démo. Le moment « je ne peux plus revenir en arrière ».
- Historique sémantique. La valeur qui s'accumule et qui enferme l'utilisateur — au bon sens du terme.
- Favoris repensés, dont le favori vivant.
- Vie privée avancée : patterns manipulateurs, traducteur de conditions.
- Agent, seulement là.
11. Risques connus
| Risque | Réalité |
|---|---|
| 99 % des gens ne changent jamais de navigateur | Le zoom sémantique doit être démontrable en 10 secondes, sans configuration |
| Le contexte de 4-8K | Contrainte permanente. Le Distiller est la réponse ; il ne sera jamais « terminé » |
| Batterie | Un navigateur qui vide la batterie est désinstallé la semaine suivante |
| Éditeurs de sites hostiles au retrait de pub | Prisme réorganise à l'affichage, ne republie rien, ne contourne pas les paywalls durs. Ne pas franchir cette ligne |
| Appareils sans Foundation Models | Base installée réelle limitée. Le mode sans IA doit être un bon produit, pas une version dégradée |
| Revue App Store | Ne pas se présenter comme un moteur alternatif ; c'est une interface au-dessus de WebKit |
| Arc est mort | Sa leçon : la nouveauté d'interface sans bénéfice immédiat ne retient personne. Chaque fonctionnalité doit répondre à « qu'est-ce que ça me donne aujourd'hui » |