Aller au contenu

Règles du framework

Les règles vivent dans _ai/rules/ (chargées selon les fichiers touchés, frontmatter paths:) et _ai/rules-ondemand/ (lues quand le sujet arrive). Le noyau _ai/CLAUDE.core.md les résume et reste toujours chargé.

Règle Vérifiée par
Routes minces (app/** = réexports) ESLint, hook daroms-check-rules
Couleurs : tokens seulement ESLint, hook, typage Tamagui (somewhat-strict)
Pas de nombre magique de style, StyleSheet, style inline ESLint, hook
Supabase seulement dans features/x/api.ts ESLint, hook
any interdit ESLint, hook
Textes via t(), sans emoji ni tiret long, clés FR = EN ESLint, hook, daroms doctor
Pas de librairie UI ni d’icônes tierces ESLint, hook
Imports absolus @/, ~/ ESLint
Fichiers <= 200 lignes ESLint, hook
RLS sur chaque table, pas de service_role dans l’app générateur g table, agent daroms-reviewer
4 états par écran générateur g screen, QueryView / List, agent daroms-reviewer
.env jamais lu permissions deny, hook daroms-deny-env
Règle Résumé Chargée pour
daroms-ai-files Les fichiers daroms-* de .claude/ sont écrasés à chaque synchro ; ce qui est propre à l’app porte le préfixe app-. .claude/**, CLAUDE.md, app.spec.md
daroms-data Supabase seulement dans features/x/api.ts via db() ; entrées et sorties validées zod ; hooks TanStack Query. src/features/**/api.ts, src/features/**/schema.ts, src/features/**/hooks.ts
daroms-database Structure uniquement par migration SQL ; RLS sur chaque table ; jamais de clé service_role côté app. supabase/**
daroms-i18n Tout texte affiché passe par t(‘cle’) ; clés identiques dans chaque langue ; ni emoji, ni tiret long. locales/**, src/**/*.tsx
daroms-naming PascalCase composants, useX hooks, kebab-case routes et dossiers, fichiers ≤ 200 lignes, imports absolus @/ et ~/. src/**, app/**
daroms-performance FlashList pour toute liste, expo-image, items mémoïsés, animations Reanimated ou transition Tamagui. src/**/*.tsx
daroms-routes app/** ne contient que des réexportations d’écrans ; aucune logique, aucun JSX. app/**/*.tsx
daroms-screens Chaque écran gère 4 états (chargement, vide, erreur + réessayer, données) et reste sous 200 lignes. src/features/**/*Screen.tsx, src/features/**/components/**
daroms-state Serveur = TanStack Query ; local persistant = @daroms/lib/storage ; global UI = Zustand, un store par feature, rare. src/**/*.ts, src/**/*.tsx
daroms-styles Composants @daroms/ui et tokens Tamagui v3 uniquement ; ni hex, ni nombre magique, ni StyleSheet, ni style inline. src/**/*.tsx, app/**/*.tsx
daroms-tests Tests unitaires jest-expo à côté du code ; flows Maestro dans e2e/ ; pas de réseau dans les tests. **/*.test.ts, **/*.test.tsx, e2e/**
daroms-animations transition Tamagui pour les états simples ; Reanimated 4 pour le geste et le continu. à la demande
daroms-core-dev Travailler dans le dépôt daroms-speed-app (et non dans core/ d’une app), tester, publier une version. à la demande
daroms-eas Profils development, preview, production ; canaux OTA ; version runtime = version de l’app. à la demande
daroms-store-compliance Points de refus fréquents à vérifier avant chaque soumission. à la demande
daroms-supabase-advanced Tables partagées, lecture publique, storage, realtime, fonctions SQL et edge functions. à la demande

Les fichiers daroms-* de .claude/ sont écrasés à chaque synchro ; ce qui est propre à l’app porte le préfixe app-.

Chargement : quand Claude lit ou écrit .claude/**, CLAUDE.md, app.spec.md.

  • Règle propre à l’app : .claude/rules/app-<sujet>.md (frontmatter paths:, une ligne > résumé).
  • Skill propre à l’app : /daroms-skill-new ; zones apprises : .claude/context/app-zones.json (/daroms-learn).
  • CLAUDE.md de l’app reste court : importe le noyau et le digest, ajoute seulement les spécificités.
  • app.spec.md est la source de vérité produit : le mettre à jour quand une décision change.

Supabase seulement dans features/x/api.ts via db() ; entrées et sorties validées zod ; hooks TanStack Query.

Chargement : quand Claude lit ou écrit src/features/**/api.ts, src/features/**/schema.ts, src/features/**/hooks.ts.

  • Générer : bun daroms g table <table> --fields "titre:text, fait:bool=false, echeance:timestamptz?" -> migration RLS, schema.ts, api.ts, types.
  • Données sur l’appareil seulement : --local -> localTable('t', Schema) (list, get, insert, update, remove), mêmes hooks ; jamais de storage.getJSON à la main pour une collection.
  • Lecture : useQuery({ queryKey: keys.list(), queryFn: async () => parseOrThrow(Schema.array(), unwrap(await db().from('t').select('*'))) }).
  • Écriture : useMutation + Input.parse(values) + invalidateQueries({ queryKey: keys.all }). Optimiste seulement si l’UX l’exige.
  • Erreurs : unwrap() et toAppError() (@daroms/data) ; l’UI affiche ErrorState error={q.error} (message traduit).
  • Clés : queryKeys('feature') -> all, list(params), detail(id).
  • Utilisateur courant : useUser() ; ne jamais filtrer par user_id côté client à la place de la RLS.
  • hooks.ts combine requêtes et état local de l’écran (useXScreen) ; l’écran ne parle qu’à ce hook.
  • Types : src/types/db.ts (généré : bun daroms db types) ; ne pas l’éditer.

Structure uniquement par migration SQL ; RLS sur chaque table ; jamais de clé service_role côté app.

Chargement : quand Claude lit ou écrit supabase/**.

  • Nouvelle table : bun daroms g table (préféré) ou bun daroms db new <nom> puis SQL à la main.
  • Chaque table : enable row level security + politiques select/insert/update/delete to authenticated avec (select auth.uid()) = user_id.
  • Index sur toute colonne de filtre (user_id, clés étrangères). updated_at via public.daroms_set_updated_at().
  • Ne jamais modifier une migration déjà poussée : en créer une nouvelle.
  • Appliquer : bun daroms db push (puis types régénérés). Edge functions : supabase/functions/<nom>/index.ts (Deno).
  • Secrets serveur : bunx supabase secrets set NOM=... ; jamais dans .env de l’app (seules les clés publiques EXPO_PUBLIC_*).
  • Détails avancés (tables partagées, storage, realtime) : .claude/rules-ondemand/daroms-supabase-advanced.md.

Tout texte affiché passe par t(‘cle’) ; clés identiques dans chaque langue ; ni emoji, ni tiret long.

Chargement : quand Claude lit ou écrit locales/**, src/**/*.tsx.

  • const t = useT() (@daroms/i18n) ; clés typées depuis locales/fr.json (namespace app).
  • Organisation : <feature>.<clé> (todos.emptyTitle), onglets tabs.<nom>. Pluriels : _one / _other.
  • Ajouter chaque clé en FR et EN dans le même changement (bun daroms doctor vérifie la parité).
  • Textes courts, ton du cahier (app.spec.md). Interpolation : t('x.count', { count }).
  • Textes du cœur (Réessayer, Annuler…) : déjà fournis par les composants ; messages zod = clés daroms:validation.*.

PascalCase composants, useX hooks, kebab-case routes et dossiers, fichiers ≤ 200 lignes, imports absolus @/ et ~/.

Chargement : quand Claude lit ou écrit src/**, app/**.

  • Feature : src/features/<kebab>/ avec <Pascal>Screen.tsx, hooks.ts, api.ts, schema.ts, components/.
  • Hooks : use<Nom> ; composants : <Nom>.tsx exportant <Nom> ; tests à côté : <Nom>.test.tsx.
  • Imports : @/ pour src/, ~/ pour la racine (~/brand, ~/locales/fr.json) ; jamais ../.
  • Fonctionnalité optionnelle réutilisable : module du cœur (/daroms-module), pas dans l’app.

FlashList pour toute liste, expo-image, items mémoïsés, animations Reanimated ou transition Tamagui.

Chargement : quand Claude lit ou écrit src/**/*.tsx.

  • Liste : List (FlashList v2) ; renderItem stable (useCallback) ; item extrait et memo. Jamais map sur plus de ~20 éléments dans un ScrollView.
  • Images : Image de @daroms/ui (cache, placeholder blurhash, ratio).
  • Animations : prop transition + valeurs enter:/exit: ; cas avancés Reanimated (voir rules-ondemand/daroms-animations.md).
  • React Compiler actif : pas de useMemo/useCallback défensifs partout, seulement pour les props de listes.

app/** ne contient que des réexportations d’écrans ; aucune logique, aucun JSX.

Chargement : quand Claude lit ou écrit app/**/*.tsx.

  • Une route = une ligne : export { default } from '@/features/<slug>/<Name>Screen'.
  • Layouts : export { default } from '@/features/root/RootLayout' ; écrans de module : export { XScreen as default } from '@daroms/module-x'.
  • Créer une route : bun daroms g screen <slug> [--tab --icon Nom] (route + feature + i18n + test + manifest).
  • Onglets : déclarés dans app.manifest.ts > tabs (nom, icône Lucide, clé de titre), pas dans le layout.
  • Paramètres : app/todo/[id].tsx -> l’écran lit useLocalSearchParams<{ id: string }>().
  • Navigation : useRouter() ; liens typés (router.push('/todo/42')).

Chaque écran gère 4 états (chargement, vide, erreur + réessayer, données) et reste sous 200 lignes.

Chargement : quand Claude lit ou écrit src/features/**/*Screen.tsx, src/features/**/components/**.

  • Squelette : <Screen header={<Header title={t('x.title')} back|large />} onRefresh refreshing> puis contenu.
  • 4 états : QueryView query={q} empty={{...}} (ou List data loading error onRetry empty) ; jamais d’état oublié.
  • Formulaire : useForm({ resolver: zodResolver(schema) }) + FormField control name label ; soumission onPress={() => void form.handleSubmit(onValid)()} ; bouton loading={mutation.isPending}.
  • Pied fixe (action principale) : Screen footer={<Button fullWidth>...}. Clavier : Screen keyboard.
  • Retour utilisateur : notify.success|error(t('...')) ; confirmation destructive : Dialog destructive.
  • Feuilles : Sheet open onOpenChange title pour créer/éditer sans changer d’écran.
  • Découper dès ~150 lignes : components/XItem.tsx (mémoïsé), logique dans hooks.ts.
  • Vérifier : bun daroms shot /route (puis --dark).

Serveur = TanStack Query ; local persistant = @daroms/lib/storage ; global UI = Zustand, un store par feature, rare.

Chargement : quand Claude lit ou écrit src/**/*.ts, src/**/*.tsx.

  • Pas de useEffect pour charger des données : useQuery. Pas de copie de données serveur dans un state.
  • Persistant simple : storage.getBoolean/setBoolean, getJSON(key, schema) ; store persistant : persist + zustandStorage.
  • Fonctions pures dans src/lib/<domaine>/ avec un test *.test.ts ; formats : formatPrice, formatDate, formatRelative ; jours : dayKey, addDays, daysBetween, lastDays, streak, formatWeekday (@daroms/lib, calcul calendaire sûr à l’heure d’été).
  • Haptique : intégrée aux composants ; ailleurs haptic('success') (@daroms/lib/haptics).
  • any interdit ; unknown + zod pour les données externes.

Composants @daroms/ui et tokens Tamagui v3 uniquement ; ni hex, ni nombre magique, ni StyleSheet, ni style inline.

Chargement : quand Claude lit ou écrit src/**/*.tsx, app/**/*.tsx.

  • Lire core/packages/ui/CATALOG.md avant d’écrire une UI. Composer : Screen, Header, Card, List, ListItem, Button…
  • Mise en page : YStack, XStack (exportés par @daroms/ui) avec gap="stack", p="gutter", items="center", justify="space-between", flex={1}.
  • Tokens v3 sans $ : espaces gutter|section|stack|inline|tight ou 0..96 ; rayons button|input|card|sheet|chip|pill ; couleurs background, card, separator, color, color-1..11, brand-500, accent-500, red-500.
  • États dans la valeur : bg="card press:background-press", opacity="1 disabled:0.5", scale="1 press:0.97" + transition="fast". Jamais hoverStyle, pressStyle, $4, $color10 (syntaxe v2 morte).
  • Sous-thème local : <YStack theme="soft" bg="background"> ; brand pour un plein, danger|success|warning teintés.
  • Texte : Title, Heading level={1|2|3}, Body, Caption avec tone="muted|brand|danger|success" ; tailles size="1".."10".
  • Icônes : import { Plus } from '@daroms/ui/icons' ; <Plus size="5" color="color-9" />. Icône absente : bun daroms icons add Nom.
  • Composant d’app réutilisé : src/features/<x>/components/ (ou src/components/ si partagé), styled(YStack, {…}) avec tokens.
  • Mode sombre automatique : n’utiliser que des clés de thème ; vérifier avec bun daroms shot /route --dark.

Tests unitaires jest-expo à côté du code ; flows Maestro dans e2e/ ; pas de réseau dans les tests.

Chargement : quand Claude lit ou écrit **/*.test.ts, **/*.test.tsx, e2e/**.

  • Écran : renderApp(<XScreen />, { config, locales: { fr } }) (@daroms/app/testing) ; await sur render et fireEvent.
  • Données simulées : jest.mock('./api', ...) avec une variable préfixée mock.
  • Fonctions pures src/lib/ : test systématique.
  • Maestro : e2e/<flow>.yaml (appId: ${APP_ID}), textes 'Texte FR|Text EN', champs par id: 'field-<nom>' (testID automatique de FormField) ; bun daroms e2e (build installé) ou bun daroms e2e --go (Expo Go, Metro lancé : bun daroms dev --go).
  • Données de démo pour shot : bun daroms shot /route --storage-file e2e/fixtures/demo.json.
  • Lancer : bun daroms test [motif].

transition Tamagui pour les états simples ; Reanimated 4 pour le geste et le continu.

Chargement : lue quand le sujet arrive (le digest la liste).

  • Simple : <YStack transition="medium" opacity="1 enter:0 exit:0" y="0 enter:10"> dans un AnimatePresence (exporté par tamagui).
  • Pression : scale="1 press:0.97" transition="fast" (déjà sur Button et Card pressable).
  • Reanimated : useSharedValue, useAnimatedStyle, withSpring ; lire et écrire avec .get() / .set() (compatible React Compiler).
  • Listes : entrées entering={FadeIn} avec parcimonie ; jamais d’animation par item dans de longues listes.
  • Respecter « réduire les animations » : useReducedMotion() de Reanimated.
  • Durées et ressorts : presets de marque fast, medium, slow, bouncy (réglés par le ton).

Travailler dans le dépôt daroms-speed-app (et non dans core/ d’une app), tester, publier une version.

Chargement : lue quand le sujet arrive (le digest la liste).

  • Dépôt : packages/* (theme, ui, data, lib, i18n, app, config), modules/*, template/, generators/, cli/, _ai/.
  • Vérifier : bun run check (tsc, eslint, jest, tests CLI, tests hooks Python).
  • Tester sur une vraie app : bun cli/daroms.ts new essai --link --dir ../ (cœur lié) puis bun daroms dev.
  • Composant UI : bloc @catalog en JSDoc, puis bun daroms catalog ; icône : bunx tamagui icons add Nom --out packages/ui/src/icons/lucide.
  • Module : bun daroms g module <slug> ; module.json décrit routes, dépendances natives, migrations, fonctions, textes d’app.
  • Règle ou skill du cœur : fichier daroms-* dans _ai/ ; digest régénéré par bun daroms catalog.
  • Publier : bun daroms release-core patch|minor|major (version, changelog, tag), puis mettre à jour les apps.

Profils development, preview, production ; canaux OTA ; version runtime = version de l’app.

Chargement : lue quand le sujet arrive (le digest la liste).

  • bun daroms build development : client de dev (nécessaire dès qu’un module natif hors Expo Go est présent : MMKV, RevenueCat, push).
  • bun daroms build preview : APK Android et build iOS ad hoc, installation par QR (distribution interne).
  • bun daroms build production puis bun daroms submit : TestFlight et piste interne Google Play.
  • OTA : bun daroms update production --message "..." ; uniquement JS et assets. Changement natif (module, plugin, permission) = nouveau build + version incrémentée dans le manifest.
  • Identifiants : gérés par EAS (bunx eas-cli credentials). Projet EAS : manifest.eas.projectId (créé au premier build).
  • Sous-module core/ : EAS envoie l’arbre de travail ; vérifier que core/ est bien présent (git submodule update –init).

Points de refus fréquents à vérifier avant chaque soumission.

Chargement : lue quand le sujet arrive (le digest la liste).

  • Compte : si inscription possible, suppression du compte dans l’app (module auth : Réglages > Supprimer mon compte).
  • Connexion sociale sur iOS : Sign in with Apple obligatoire si Google est proposé (module auth le gère).
  • Achats numériques : uniquement via achats intégrés (RevenueCat) ; bouton Restaurer visible ; prix, période et conditions affichés ; liens CGU et confidentialité sur le paywall.
  • Confidentialité : URL publique (manifest.store.privacyUrl) ; étiquettes de confidentialité App Store et section Sécurité des données Google Play cohérentes avec les SDK (analytics, auth, push).
  • Permissions : demander au moment de l’usage, texte d’explication clair (plugins Expo) ; aucune permission inutilisée.
  • Contenu : pas d’écran vide ni de « bientôt » ; données de démo pour la revue (compte de test dans les notes de revue).
  • Support : manifest.store.supportEmail ; captures d’écran à jour (/daroms-store).

Tables partagées, lecture publique, storage, realtime, fonctions SQL et edge functions.

Chargement : lue quand le sujet arrive (le digest la liste).

  • Lecture publique, écriture propriétaire : politique select to anon, authenticated using (true) + écritures with check ((select auth.uid()) = user_id).
  • Partage (équipes, conversations) : table de liaison members(group_id, user_id) ; politique using (exists (select 1 from members m where m.group_id = t.group_id and m.user_id = (select auth.uid()))). Indexer members(user_id, group_id).
  • Fonctions security definer : toujours set search_path = '' et noms qualifiés (public.t).
  • Storage : bucket privé + politiques sur storage.objects par préfixe auth.uid()::text ; upload depuis api.ts (db().storage.from('b').upload(path, file)), URL signée pour l’affichage.
  • Realtime : db().channel('x').on('postgres_changes', { event: '*', schema: 'public', table: 't', filter: 'user_id=eq.' + id }, cb) dans un hook ; invalider la requête TanStack plutôt que muter le cache à la main ; activer la table dans la publication supabase_realtime.
  • Edge function : Deno, npm: imports, client avec l’en-tête Authorization de l’utilisateur (RLS appliquée) ; service_role seulement pour l’administration.
  • Pagination : .range(from, to) + useInfiniteQuery ; tri stable (created_at, id).