uxskill
Star on GitHub
Sortie · v3.0.0 · 2026-05-28

ux-skill v3.0 — on a livré The Brain. Les brand specs sont des données d'entraînement, plus des templates.

v2 piochait dans le catalogue. v3 le distille. Les mêmes 160 brand specs, un rôle complètement nouveau. Le recommandeur notait 1 182 entrées et renvoyait la plus similaire au brief. Le synthétiseur les lit désormais comme un vocabulaire et compile depuis un brief un langage de design inédit. Chaque appel renvoie un système qui n'existait pas avant l'appel.

Les brand specs sont des données d'entraînement,
plus des templates.

Le basculement en un paragraphe

v2 était un recommandeur. On lui donnait un brief, il renvoyait la correspondance la plus proche dans un catalogue curé — 84 styles, 176 palettes, 160 brand specs, 70 paires typographiques. La sortie était toujours une ligne de la base. v3 conserve l'intégralité du catalogue mais inverse son rôle : ces 1 182 entrées sont désormais le vocabulaire que distille le moteur. Un brief ne tire plus une ligne — il est compilé en sept valeurs d'axe, et ces valeurs synthétisent des tokens neufs. Le catalogue apprend au moteur à quoi ressemble « aéré et corporate » ; le moteur produit un nouveau système « aéré et corporate » qui n'a jamais été livré. Mêmes données. Rôle complètement différent.

Ce qui était cassé en v2 et que v3 corrige

Trois vrais bugs et un trou de design. Les bugs n'étaient pas bruyants — c'étaient ceux du type silencieux où le moteur renvoyait des sorties plausibles et nous continuions à livrer. On les a vus seulement quand on s'est assis pour rédiger la spec de mise à jour et qu'une revue externe nous a forcés à regarder honnêtement.

1. Pseudo-déterminisme via des tie-breaks à l'ordre du système de fichiers

Plusieurs tris à l'intérieur du pipeline du recommandeur notaient les candidats par similarité et départageaient les égalités par l'ordre que renvoyait os.listdir de Python. Sur macOS, c'était alphabétique. Sur Linux, c'était l'ordre d'insertion dans le journal du système de fichiers. Entre un clone neuf et un clone âgé, le tie-break pouvait basculer. On se racontait que le moteur était déterministe. Il l'était jusqu'à l'égalité. Après, il était ce dont l'OS se souvenait par hasard.

v3 verrouille chaque tri avec un tie-break explicite par ordre alphabétique du brand_id. Même brief, mêmes axes, même sortie d'une machine à l'autre, d'un système de fichiers à l'autre, d'une version de Python à l'autre. Il y a un test qui lance le synthétiseur 200 fois dans trois répertoires temporaires et qui exige une sortie identique octet pour octet.

2. Des collisions d'axes résolues par accident

En v2, quand un brief avait deux tags qui tiraient en sens contraires — dense et corporate, par exemple — le scorer du recommandeur pesait l'un plus fort via un coefficient caché, et la sortie penchait vers le tag favorisé par ce coefficient. Pas de règle documentée. On savait expliquer le résultat après coup. On ne savait pas le prédire avant.

v3 hisse cela au rang d'une matrice d'interaction d'axes. Chaque conflit documenté possède une résolution explicite et une justification. Densité et formalité se disputent l'espacement — la densité gagne pour les briefs denses (école Bloomberg), la formalité gagne pour les briefs aérés (luxe). La matrice est testée. Elle est lisible. Elle est commitée.

3. L'évaluateur notait sa propre copie

C'était le plus embarrassant, et on l'a vu parce que ChatGPT l'a signalé dans une revue externe de la spec v2.1 — ce qu'il a appelé le problème du self-referential drift. Le flux paraissait propre : le synthétiseur compilait un système depuis le brief, puis score_tone_match évaluait à quel point le système collait au ton du brief. Le hic, c'est que score_tone_match dérivait les valeurs d'axe depuis le brief avec la même logique que le synthétiseur, puis comparait les axes du synthétiseur à ses propres axes dérivés. Les deux côtés de la comparaison passaient par la même fonction. Évidemment qu'il notait bien. Le synthétiseur s'évaluait lui-même avec sa propre grille.

v3 les découple. score_tone_match compare désormais la sortie du synthétiseur aux tone tags bruts du brief — les chaînes warm, bold, minimal, etc. — via un mapping indépendant qui ne touche jamais à la logique d'axes du synthétiseur. Les deux chemins ne peuvent plus copier l'un sur l'autre. On doit cette observation à la revue externe et on le dit ici pour mémoire.

4. Le journal de décisions était en écriture seule — jamais consommé

v2 écrivait une entrée dans .ux/decisions.jsonl après chaque recommandation, mais rien ne la relisait. C'était un log pour notre propre debug, pas un signal de feedback. Le recommandeur tournait à l'identique à l'appel 1 et à l'appel 1 000. Aucun apprentissage. Le dépôt avait une mémoire et le moteur l'ignorait.

v3 boucle cette boucle. Le journal a un schéma verrouillé (_v: 1), le recommandeur re-classe les candidats selon les succès passés dans le même bucket (industry, ui_type), et seules les décisions ayant bien noté et que l'utilisateur a effectivement livrées comptent dans le re-classement. Le cerveau a enfin des yeux sur son propre historique. La section 6 fait la visite guidée.

Le synthétiseur à 7 axes

Au cœur de v3 se trouve une fonction déterministe qui mappe un brief vers sept axes numériques, puis ces sept axes vers un système de design complet. Les axes ne tiennent pas de la magie — chacun est un scalaire normalisé avec une plage documentée, et chacun pointe vers un bundle de tokens précis. L'intérêt de tourner sur des axes (plutôt que de prendre une ligne du catalogue) est que n'importe quelle combinaison devient atteignable, y compris des combinaisons qu'aucune marque réelle du catalogue n'utilise. Le catalogue définit la forme de l'espace ; le synthétiseur peut atterrir n'importe où à l'intérieur.

Axe Ce qu'il mesure Mappe vers
warmth Température chromatique de la palette — du charbon froid à la sienne chaude famille de teinte de la palette, courbe de saturation de l'accent
contrast Volume visuel — quiet/balanced/loud ratio d'échelle typographique (1.200 / 1.250 / 1.333), profondeur d'ombre, intensité d'accent
density Densité d'information par viewport — aéré à dense base de l'échelle d'espacement (4/8/12/16px), multiplicateurs de line-height
geometry Personnalité des arêtes — sharp/balanced/soft échelle de radius (2/8/18px), topologie d'angle, épaisseurs de bordure
formality Registre de ton — playful/balanced/corporate courbes de poids typographique, tracking, amplitude du caption au hero
motion Budget d'animation — restrained/balanced/cinematic rampes de durée (120/240/420ms), famille d'easing, comportement du scroll
type_personality Voix typographique — humanist/neutral/geometric/expressive famille de la display face, échelle de graisses, stratégie d'italique

Le point d'entrée Python est assez petit pour tenir dans un seul exemple. On passe un brief et on récupère un système synthétisé, avec valeurs d'axes, palette choisie, configuration typographique, échelle d'espacement, échelle de radius et presets de motion attachés.

# d'un brief à un système, en un appel
from engine.synthesizer import synthesize

brief = Brief(
  industry="fintech-payments",
  tone=["bold", "serious"],
  audience=["merchants", "developers"],
)

sys = synthesize(brief)
# sys.mode == "pure_synthesis"
# sys.axes == {warmth: 0.32, contrast: 0.72, density: 0.58, ...}
# sys.palette, sys.type, sys.spacing, sys.radius, sys.motion

Même brief en entrée, mêmes axes en sortie, mêmes tokens en sortie. Toujours. Le synthétiseur n'a pas de seed aléatoire parce qu'il n'y a aucune aléa à seeder.

Trois modes, une seule logique

Le synthétiseur répartit automatiquement entre trois modes selon ce que contient le brief. Pour la plupart des usages, aucun flag pour choisir le mode à la main — la forme du brief décide. La logique est volontairement simple : la présence d'une marque de référence et un flag strict déterminent le chemin.

strict_brand · le plus rapide

100% la marque nommée

reference_brands=[stripe] avec strict=True renvoie la spec Stripe telle quelle. Pas d'interprétation, pas de mélange, pas de synthèse. Utile quand la marque existe, que la spec fait autorité et que le brief veut juste les tokens. C'est le chemin quand on a un Figma et un système documenté, et qu'on a besoin d'un code qui n'improvise pas.

brand_anchor · 70 / 30

Ancré à la marque, ajusté par axes pour le brief

reference_brands=[stripe] sans strict=True renvoie 70% de tokens Stripe fusionnés à 30% de deltas dérivés des axes de quatre marques voisines choisies par proximité d'axes. La sortie reste indéniablement Stripe, mais s'incline vers le ton du brief — un Stripe plus joueur pour une feature grand public, un Stripe plus formel pour un tableau de bord entreprise. La marque d'ancrage survit à tous les conflits.

pure_synthesis · l'espace infini

Aucune marque nommée — un langage neuf à chaque appel

Pas de reference_brands dans le brief. Le synthétiseur note les sept axes depuis le brief, trouve les huit exemplaires les plus proches du catalogue par distance d'axes, distille leur structure partagée, et émet un bundle frais palette + type + espacement + radius + motion. Des briefs différents atterrissent dans des régions différentes du même espace ; chaque sortie est cohérente en interne et identifiable comme étant elle-même.

Côté CLI, les trois chemins ressemblent à ceci :

# strict — 100% cette marque
$ uxskill synthesize --brand stripe --strict

# anchor — 70/30
$ uxskill synthesize --brand stripe

# pure synthesis — sans marque
$ uxskill synthesize --industry fintech-payments --tone bold

Un seul synthétiseur, une seule logique, trois personnalités de sortie. L'utilisateur ne change pas d'implémentation — le brief se route lui-même.

La matrice d'interaction des axes

Les axes tirent sur les mêmes tokens. La densité veut un espacement serré ; la formalité veut un espacement généreux pour un registre de luxe. La géométrie veut des angles vifs pour un ton éditorial ; la formalité veut des angles doux pour un tableau de bord financier. En v2, ces conflits se réglaient selon le coefficient qui sortait gagnant dans le scorer. En v3, chaque conflit documenté a un résultat déclaré et une école de design vers laquelle il pointe — nommée, pour que vous puissiez la lire et ne pas être d'accord.

Quatre cas représentatifs, tous commitées comme fixtures de tests :

dense + corporate
Base d'échelle d'espacement : 4px

La densité gagne. La réponse de l'école Bloomberg. La densité d'information est la vertu suprême quand le brief demande des tableaux de bord denses en registre corporate — la formalité s'incline devant la lisibilité à haute densité.

airy + corporate
Base d'échelle d'espacement : 12px

La formalité gagne. La réponse luxe-finance. Aéré + corporate, c'est le brief de la banque privée, de la gestion de patrimoine, des tableaux de bord cadres — l'air autour de chaque élément est le message de sérieux.

sharp + corporate
Échelle de radius : 2px

La géométrie gagne. La réponse école NYT. Sharp + corporate, c'est le brief éditorial — angles droits et micro-radius de 2px se lisent comme institutionnels, réfléchis, format grand journal.

soft + playful
Échelle de radius : 18px

Les deux axes s'alignent. La réponse école Glossier. Géométrie douce et registre joueur s'effondrent sur un arrondi généreux — 18px est le seuil où les rectangles commencent à se lire comme des galets.

Pourquoi c'est important : en v2, le même brief pouvait atterrir sur 4px ou 12px selon le coefficient qui avait gagné le scorer ce mois-là. En v3, dense + corporate vaut toujours 4px. airy + corporate vaut toujours 12px. Les règles sont lisibles, testables, discutables. Si une résolution vous déplaît, vous pouvez ouvrir une issue contre la matrice et la discussion porte sur le goût, pas sur une constante cachée.

Le cerveau apprend

C'est la partie de v3 qui est véritablement nouvelle pour le projet : le moteur apprend désormais de vos décisions locales. Le mécanisme est petit, déterministe, et entièrement hors ligne. Pas de télémétrie, pas de compte, pas de sync cloud. Votre installation apprend de votre installation. Des dépôts différents font grandir des cerveaux différents.

Le journal

Chaque appel à /ux-recommend et /ux-synthesize écrit une ligne dans .ux/decisions.jsonl. Le schéma est verrouillé à _v: 1 :

{
  "_v": 1,
  "ts": "2026-05-28T14:22:09Z",
  "frame": { "industry": "fintech-payments", "ui_type": "dashboard", ... },
  "system": { "style_id": "editorial-calm-dark", "palette_id": "charcoal-amber", ... },
  "axes": { "warmth": 0.31, "contrast": 0.72, ... },
  "lint_score": 88,
  "user_accepted": true
}

Le re-classement

À l'appel suivant, le recommandeur regroupe les entrées passées par (industry, ui_type). Chaque candidat considéré reçoit un bonus de +5 s'il colle à un précédent dans le même bucket. Seuls les précédents avec lint_score >= 80 ET user_accepted = true comptent — on n'apprend pas des sorties refusées. Le bucket a besoin d'au moins trois précédents valides pour qu'un re-classement s'enclenche. En dessous, le moteur démarre à froid et se comporte comme une installation fraîche.

Les garanties

Les garanties qu'on donne là-dessus sont étroites et valent la peine d'être dites à voix haute. Le déterminisme est préservé. Même brief plus même journal produisent toujours la même sortie. Le +5 s'applique avant tout tie-break, et la règle de l'ordre alphabétique par brand_id continue de gagner les égalités en dessous. Le démarrage à froid est sûr. Un nouveau dépôt se comporte comme tous les autres nouveaux dépôts. L'apprentissage est local. Vos décisions ne quittent jamais votre machine.

Vous pouvez inspecter à tout moment ce que votre installation a appris :

$ uxskill stats --html
[OK] Wrote .ux/stats.html
[OK] Open in browser to see your install's learned priors

Le tableau de bord HTML montre le nombre de décisions par bucket, le score moyen du linter, les palettes les plus récurrentes, les distributions d'axes. C'est la preuve visible de l'auto-apprentissage — vous voyez le profil de goût de votre installation s'épaissir au fil du temps à mesure que vous livrez. Rien de tout cela n'est sur un serveur. C'est dans votre dépôt, en JSONL brut et une vue HTML générée.

La boucle automatique /ux-evolve

La nouvelle commande de v3, c'est /ux-evolve. Jusqu'ici, la boucle de polish était manuelle : lancer le linter, lire les findings, corriger, relinter, recommencer. Evolve ferme la boucle. On lui donne un artefact et un score cible ; il lance le linter, applique six passes de polish idempotentes, relinte, puis s'arrête à la cible, sur un plateau, ou au plafond des cinq tours.

La forme d'un tour :

  1. Lint — 145 règles regex déterministes en A11y, contenu, qualité, typographie. Renvoie un score de 0 à 100 et une liste de findings.
  2. Polish — six passes qui traitent d'abord les findings les plus sévères. Idempotent : relancer une passe sur une sortie déjà polie est un no-op.
  3. Re-lint — note l'artefact poli.
  4. Décider — si le nouveau score est ≥ 90, on arrête. Si le delta depuis le tour précédent est inférieur à 5 (plateau), on arrête. Si on est au tour 5, on arrête. Sinon on boucle.

Le seuil qualité est ferme à 65. Si le score final est sous 65, le moteur refuse de livrer l'artefact à moins qu'on passe --force. Le raisonnement : une sortie à 65 ou en dessous est du slop IA reconnaissable, et on ne laisse pas notre propre moteur cracher du slop en silence. L'utilisateur peut outrepasser le seuil, mais l'override est explicite et figure dans le journal de décisions.

Une exécution concrète : un squelette de tableau de bord fintech sortait à 72 au premier lint — surtout des focus states manquants, deux labels à faible contraste, un finding sur Inter en taille display. Le tour 1 a poli les focus states jusqu'à 81. Le tour 2 a corrigé le contraste à 86. Le tour 3 a remplacé Inter par la display face réelle du brief et a rééquilibré la taille des captions — 91, cible atteinte, boucle terminée. Trois tours, aucun rejet, livré.

Ce qu'on n'a pas construit (et pourquoi)

La spec maximaliste v2.1 proposait plusieurs choses qui ne sont pas passées en v3. Dire lesquelles, et pourquoi, est la seule façon honnête de parler de scope.

Pas de LLM dans la boucle

La proposition maximaliste contenait un axe esthétique subjectif jugé par LLM — « demander à un modèle si ça sent l'éditorial ». On l'a rejeté par principe. L'intérêt même du moteur est le déterminisme : même brief, même sortie, pas de surprise demain matin. Un LLM dans le synthétiseur rendrait chaque appel non reproductible. v3 appelle zéro LLM. Le synthétiseur est du Python pur sur du JSON ; le linter est du regex ; l'évaluateur est à base de règles.

Pas de mutation génétique multi-candidats

La proposition avait aussi une étape d'algorithme génétique — émettre N candidats par appel, les muter, les noter, renvoyer le plus apte. On a essayé dans une branche. La sortie devenait pire, pas meilleure, parce que la mutation introduisait du bruit que la boucle de polish devait ensuite défaire. v3 livre une synthèse à artefact unique avec boucle de polish. Un candidat. Six passes idempotentes. De meilleurs résultats, moins de surface de code.

Pas de renommage des commandes

La proposition renommait /ux-recommend en /generate:ui et /ux-polish en /mutate:ui. On a gardé les 22 commandes existantes sous leurs noms existants et ajouté /ux-evolve en 23e. On ne voulait pas qu'une migration v2 vers v3 casse la mémoire musculaire ni les alias shell de qui que ce soit.

Pas de « brûler le catalogue pour l'espace infini »

La version la plus maximaliste de la proposition disait que le catalogue était un poids mort — que le synthétiseur pouvait atteindre tout l'espace de design tout seul et qu'il fallait jeter les 1 182 entrées. On a tout gardé. Le catalogue est ce qui apprend au synthétiseur la forme de l'espace — sans exemplaires, les axes n'ont pas d'ancres et la sortie se met à dériver. v3 lit le catalogue comme vocabulaire ; v3 a besoin du vocabulaire.

Ce sont des décisions de goût. Des gens avec d'autres priorités ne seront pas d'accord, et c'est très bien. On doit au projet de dire ce qu'on n'a pas fait, et pourquoi, pour mémoire.

Les chiffres de v3

23
Commandes
18
Outils MCP
1 182
Entrées
145
Anti-patterns
160
Brand specs
17
IDEs
17
Langues
223
Tests OK

Les 22 slash commands existantes gardent leurs noms, plus /ux-evolve en 23e. Les 15 outils MCP sont conservés, plus trois nouveaux — ux_synthesize, ux_decisions_query, ux_decisions_stats — pour les agents qui veulent piloter le synthétiseur ou interroger ce que le cerveau a appris. Le catalogue et le linter sont intacts. L'installeur 17-IDE est intact. Les 17 pages d'accueil localisées et les READMEs sont inchangés. v3 est additif en surface et réécrit au cœur.

v2 était un recommandeur qui piochait dans un catalogue. v3 est un compilateur qui distille depuis un catalogue. Les mêmes données font le travail inverse.

Installer en 60 secondes

v3 est livré par les trois mêmes chemins que v2 — pip, npm et les marketplaces de plugins IDE. L'étape init détecte automatiquement votre IDE et écrit le bon fichier de configuration au bon endroit. L'étape synthesize prend un brief ou un couple industry + tone et renvoie un système de design complet dans .ux/last-system.json au sein du dépôt courant.

$ pip install uxskill
$ uxskill init                     # détecte votre IDE
$ uxskill synthesize --industry fintech-payments --tone bold

[OK] Mode: pure_synthesis
[OK] Axes: warmth=0.31, contrast=0.74, density=0.58, geometry=0.42, ...
[OK] Wrote .ux/last-system.json (palette, type, spacing, radius, motion)
[OK] Wrote .ux/decisions.jsonl (1 entry, schema _v: 1)

Même chemin d'installation pour chaque IDE supporté : Claude Code, Cursor, Windsurf, GitHub Copilot, Gemini CLI, Codex, Kiro, Cline, Continue, Aider, Zed, JetBrains AI, Pieces, Tabby, Tabnine, CodeWhisperer, Roo Cline. Le serveur MCP stdio est la source de vérité unique pour tous.

Scope honnête

v3 est une fondation, pas une destination finie.

Le synthétiseur à 7 axes est le travail pour lequel on a publié des preuves. Les axes 8 et 9 (saturation_strategy, surface_depth) sont esquissés mais pas livrés — ils approfondiraient le partage dark / light et la stratégie de gradient. Le journal de décisions n'est consommé aujourd'hui que par le re-classement du recommandeur ; on s'attend à ce que l'évaluateur et le scorer du linter commencent à le lire en v3.1. La proposition maximaliste v2.1 qu'on a rejetée n'est pas morte — certaines de ces idées atterriront plus tard, quand on pourra prouver qu'elles aident.

Si vous trouvez un brief qui produit des sorties insatisfaisantes, ou une résolution de la matrice qui se heurte à votre goût, ouvrez une issue. La matrice est commitée, les tests sont commités, le cerveau est dans votre dépôt. Les discussions se passent désormais sur un substrat que vous pouvez lire.

Ce qui change pour vous

Si vous étiez en v2 : rien ne casse. Les 22 commandes que vous utilisez déjà se comportent pareil. Le linter tourne à la même vitesse. Le serveur MCP expose 15 des 18 outils sous leurs anciens noms, plus trois nouveaux. Si vous mettez à jour et que vous n'appelez jamais /ux-synthesize ni /ux-evolve, votre flux quotidien ne bouge pas.

Si vous attendiez que le moteur génère au lieu de recommander : c'est v3. Le synthétiseur est le nouveau centre ; /ux-recommend fonctionne toujours et re-classe désormais depuis votre journal ; /ux-evolve ferme la boucle de polish. Les brand specs sont des données d'entraînement maintenant, plus des templates. Les mêmes 1 182 entrées font un travail complètement différent.

On a nommé v3 The Brain parce que c'est ce qu'on a construit — un compilateur de design génératif sous contraintes, avec boucle de feedback fermée. Entièrement hors ligne. Zéro appel LLM. Même brief, mêmes axes, même sortie. Briefs différents, régions différentes, sorties infinies. Votre installation apprend de votre dépôt. Des dépôts différents font des cerveaux différents. Le tout sous MIT, sub-300ms sur une machine normale, installable via pip.

Lancez-le sur un brief qui vous tient à cœur. Regardez ce qu'il renvoie. Ouvrez une issue si la sortie heurte votre goût — le conflit rejoint la matrice, la règle reçoit un nom, et la prochaine installation se comporte mieux. C'est comme ça que le cerveau capitalise.

Lancez-le sur votre projet

Un moteur. Trois chemins d'installation.

Le synthétiseur qui anime v3 est livré dans le même paquet que le linter et le recommandeur. 145 anti-patterns. 160 brand specs. 1 182 entrées. 17 IDEs. MIT, sans télémétrie, sans compte.

$ /plugin marketplace add Laith0003/ux-skill
$ /plugin install ux@ux-skill
— ou —
$ pip install uxskill
— ou —
$ npx uxskill@latest init

Sources sur GitHub : Laith0003/ux-skill · README en français : README.fr.md · npm : uxskill · PyPI : uxskill · Version anglaise : English version · Lectures liées : Brand specs comme training data, Anti-patterns, Brands