@nodefony/devkit 10.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/LICENSE +544 -0
  2. package/README.md +318 -0
  3. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorate.js +9 -0
  4. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateMetadata.js +6 -0
  5. package/dist/_virtual/_@oxc-project_runtime@0.142.0/helpers/esm/decorateParam.js +8 -0
  6. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorate.js +9 -0
  7. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateMetadata.js +6 -0
  8. package/dist/_virtual/_@oxc-project_runtime@0.143.0/helpers/esm/decorateParam.js +8 -0
  9. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorate.js +9 -0
  10. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateMetadata.js +6 -0
  11. package/dist/_virtual/_@oxc-project_runtime@0.146.0/helpers/esm/decorateParam.js +8 -0
  12. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorate.js +9 -0
  13. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateMetadata.js +6 -0
  14. package/dist/_virtual/_@oxc-project_runtime@0.147.0/helpers/esm/decorateParam.js +8 -0
  15. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorate.js +9 -0
  16. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateMetadata.js +6 -0
  17. package/dist/_virtual/_@oxc-project_runtime@0.148.0/helpers/esm/decorateParam.js +8 -0
  18. package/dist/index.js +47 -0
  19. package/dist/nodefony/command/CardCommand.js +70 -0
  20. package/dist/nodefony/config/config.js +200 -0
  21. package/dist/nodefony/config/defineModuleConfig.js +36 -0
  22. package/dist/nodefony/controllers/DevkitController.js +60 -0
  23. package/dist/nodefony/controllers/McpController.js +223 -0
  24. package/dist/nodefony/controllers/OAuthMetadataController.js +89 -0
  25. package/dist/nodefony/interfaces/IDevkitService.js +1 -0
  26. package/dist/nodefony/interfaces/index.js +1 -0
  27. package/dist/nodefony/service/DevkitService.js +198 -0
  28. package/dist/nodefony/src/card.js +2 -0
  29. package/dist/nodefony/src/errors/DevkitError.js +21 -0
  30. package/dist/nodefony/src/mcp/guard.js +51 -0
  31. package/dist/nodefony/src/mcp/protocol.js +127 -0
  32. package/dist/nodefony/src/mcp/server.js +133 -0
  33. package/dist/nodefony/src/mcp/tools.js +163 -0
  34. package/dist/types/index.d.ts +52 -0
  35. package/dist/types/nodefony/command/CardCommand.d.ts +33 -0
  36. package/dist/types/nodefony/config/config.d.ts +25 -0
  37. package/dist/types/nodefony/config/defineModuleConfig.d.ts +27 -0
  38. package/dist/types/nodefony/controllers/DevkitController.d.ts +37 -0
  39. package/dist/types/nodefony/controllers/McpController.d.ts +70 -0
  40. package/dist/types/nodefony/controllers/OAuthMetadataController.d.ts +39 -0
  41. package/dist/types/nodefony/interfaces/IDevkitService.d.ts +69 -0
  42. package/dist/types/nodefony/interfaces/index.d.ts +1 -0
  43. package/dist/types/nodefony/service/DevkitService.d.ts +143 -0
  44. package/dist/types/nodefony/src/card.d.ts +18 -0
  45. package/dist/types/nodefony/src/errors/DevkitError.d.ts +14 -0
  46. package/dist/types/nodefony/src/mcp/guard.d.ts +66 -0
  47. package/dist/types/nodefony/src/mcp/protocol.d.ts +139 -0
  48. package/dist/types/nodefony/src/mcp/server.d.ts +48 -0
  49. package/dist/types/nodefony/src/mcp/tools.d.ts +81 -0
  50. package/docs/index.md +358 -0
  51. package/package.json +77 -0
  52. package/skills/nodefony-add-crud/SKILL.md +199 -0
  53. package/skills/nodefony-add-realtime-channel/SKILL.md +95 -0
  54. package/skills/nodefony-add-service/SKILL.md +90 -0
  55. package/skills/nodefony-browser/SKILL.md +416 -0
  56. package/skills/nodefony-browser/references/socket.md +115 -0
  57. package/skills/nodefony-browser/references/sondes.md +175 -0
  58. package/skills/nodefony-browser/scripts/audit.mjs +169 -0
  59. package/skills/nodefony-browser/scripts/inspect.mjs +903 -0
  60. package/skills/nodefony-browser/scripts/lib/browser.mjs +357 -0
  61. package/skills/nodefony-browser/scripts/lib/probes.mjs +501 -0
  62. package/skills/nodefony-browser/scripts/lib/wcag.mjs +153 -0
  63. package/skills/nodefony-browser/scripts/socket.mjs +354 -0
  64. package/skills/nodefony-browser/scripts/watch.mjs +125 -0
  65. package/skills/nodefony-migrate-schema/SKILL.md +359 -0
  66. package/skills/nodefony-migrate-schema/references/verdicts.md +139 -0
  67. package/skills/nodefony-protect-route/SKILL.md +195 -0
@@ -0,0 +1,95 @@
1
+ ---
2
+ name: nodefony-add-realtime-channel
3
+ description: >
4
+ Ajoute un flux temps réel à une application Nodefony par la bonne couche — un
5
+ `RealtimeController` et ses décorateurs de canal — au lieu de recomposer un WebSocket à la main.
6
+ Porte la façon de fermer un canal à certains rôles, le piège du canal public par défaut, celui
7
+ du canal dont le nom est calculé, et la façade cliente à employer côté navigateur. À charger
8
+ AVANT d'écrire du code WebSocket, un canal, ou un abonnement client.
9
+ Déclencheurs : "flux temps réel", "websocket", "canal", "push", "notifications en direct",
10
+ "abonnement", "RealtimeController", "@RealtimeChannel", "socket client", "temps réel privé",
11
+ "réserver un canal à un rôle", "mon canal est public", "diffuser à plusieurs onglets".
12
+ ---
13
+
14
+ # add-realtime-channel — le temps réel par sa couche
15
+
16
+ > ⚖️ **La confiance n'exclut pas le contrôle.** Un canal sans politique est **public par
17
+ > construction** — c'est le comportement voulu du framework, pas un oubli. Ce qui n'est pas
18
+ > déclaré fermé est ouvert.
19
+
20
+ ## Le geste
21
+
22
+ ```bash
23
+ npx nodefony create controller Ops --kind realtime
24
+ ```
25
+
26
+ Produit une sous-classe de `RealtimeController` avec son canal décoré et une action, plus le
27
+ câblage. **N'écris pas de `new WebSocket(...)` ni de `ws.on(...)` côté serveur** : le bas niveau
28
+ existe, il est employé par le framework, et le reprendre à la main te prive du routage par canal,
29
+ de l'authentification partagée avec HTTP, et de la contre-pression.
30
+
31
+ ## Fermer un canal
32
+
33
+ ```ts
34
+ @RealtimeChannel("ops:alerts", { roles: ["ROLE_ADMIN"] })
35
+ export class OpsController extends RealtimeController {
36
+ @RealtimeAction("ops:snapshot", { roles: ["ROLE_ADMIN"] })
37
+ async snapshot() { … }
38
+ }
39
+ ```
40
+
41
+ Deux niveaux, et ils sont indépendants : la **politique du canal** décide qui peut s'abonner, la
42
+ politique d'une **action** décide qui peut la déclencher. `{ authenticated: true }` suffit quand
43
+ aucun rôle particulier n'est requis.
44
+
45
+ ## 🔴 Le piège du canal dont le nom est calculé
46
+
47
+ Une politique est indexée par le nom **EXACT** du canal. Un canal dont le nom se construit à
48
+ l'exécution (`` `room:${id}` ``) **n'est couvert par aucune politique déclarée** — il naît public,
49
+ et rien ne le signale.
50
+
51
+ ```ts
52
+ @RealtimeChannel("room:lobby", { authenticated: true }) // ✅ couvert
53
+ // `room:42` construit à la volée // 🔴 PAS couvert
54
+ ```
55
+
56
+ Tant qu'un canal à membres n'existe pas dans le framework, le geste montrable est le canal
57
+ **gardé** par rôle ou authentification, pas le canal dynamique.
58
+
59
+ ## Côté client — la façade, pas le socket
60
+
61
+ ```ts
62
+ import { RealtimeClient } from "nodefony/client";
63
+
64
+ const client = RealtimeClient.shared();
65
+ client.subscribe("ops:alerts", (payload) => { … });
66
+ ```
67
+
68
+ En React, les hooks du paquet client font la même chose avec le cycle de vie du composant.
69
+ **Importe depuis `nodefony/client`**, jamais depuis la racine `nodefony` : côté navigateur, elle
70
+ n'expose pas ces symboles et le compilateur refuse.
71
+
72
+ ## Ne pas ouvrir plus que le canal demandé
73
+
74
+ Fermer « toute la zone `^/api` » pour protéger un canal emporte le reste de l'application avec
75
+ lui — y compris les démonstrations publiques posées par le scaffold. La politique se pose **sur le
76
+ canal**, pas sur l'espace HTTP qui l'entoure.
77
+
78
+ ## Prouver
79
+
80
+ ```bash
81
+ npm test
82
+ npx nodefony inspect routes --json # les canaux montés, tels que l'application les voit
83
+ ```
84
+
85
+ Puis, avec trois identités comme pour une route : un anonyme ne reçoit pas le flux réservé, un
86
+ compte sans le rôle non plus, l'administrateur oui — **et le canal public du scaffold répond
87
+ toujours** (s'il s'est fermé, une politique a débordé).
88
+
89
+ ## Voisins
90
+
91
+ | Besoin | Skill |
92
+ | ------------------------------ | ------------------------ |
93
+ | Réserver une route HTTP | `nodefony-protect-route` |
94
+ | Une ressource complète stockée | `nodefony-add-crud` |
95
+ | Un service métier injectable | `nodefony-add-service` |
@@ -0,0 +1,90 @@
1
+ ---
2
+ name: nodefony-add-service
3
+ description: >
4
+ Crée un service injectable dans une application Nodefony par `nodefony create service`, et le
5
+ fait entrer dans le conteneur — la moitié qu'on oublie. Porte la distinction entre le nom de la
6
+ CLASSE et le nom de l'INSTANCE, les deux façons d'obtenir un service depuis un autre
7
+ (`@inject` au constructeur ou `container.get` à l'usage), et le défaut mesuré qu'un service
8
+ écrit à la main produit : une classe qui compile, dont les tests passent, et que le conteneur
9
+ ignore. À charger AVANT d'écrire une classe de service ou d'appeler un service depuis un autre.
10
+ Déclencheurs : "crée un service", "un service métier", "logique métier partagée", "injecter une
11
+ dépendance", "container.get", "@injectable", "@services", "appeler un service depuis un autre",
12
+ "mon service est undefined", "le conteneur ne trouve pas mon service".
13
+ ---
14
+
15
+ # add-service — un service que le conteneur connaît
16
+
17
+ > ⚖️ **La confiance n'exclut pas le contrôle.** Un service qui compile n'est pas un service
18
+ > enregistré. Le seul juge est l'application en marche.
19
+
20
+ ## Le geste
21
+
22
+ ```bash
23
+ npx nodefony create service Billing
24
+ ```
25
+
26
+ Produit la classe `@injectable()` `extends Service`, son interface, **et** l'inscrit dans le
27
+ `@services([…])` de la cible — en le créant s'il n'existe pas.
28
+
29
+ Pour qu'il en appelle un autre :
30
+
31
+ ```bash
32
+ npx nodefony create service Invoice --inject Billing
33
+ ```
34
+
35
+ La commande **refuse avant d'écrire** si le service visé n'existe pas, et liste alors ceux de la
36
+ cible. Elle refuse aussi de s'auto-injecter.
37
+
38
+ ## Pourquoi ne pas l'écrire à la main — c'est mesuré
39
+
40
+ Lâché dans une application fraîche sans accès aux sources du framework, un agent produit une
41
+ classe à méthodes `static`. **Elle compile, elle marche, et elle reste invisible au conteneur.**
42
+ Le vérificateur le dit (`orphan-service`), mais seulement si on le lance :
43
+
44
+ ```
45
+ Billing porte @injectable mais n'est déclaré nulle part — sans @services([Billing])
46
+ sur le module, il n'entre pas dans l'ordre de démarrage, échappe au rapport de boot
47
+ et à l'introspection, et n'est construit qu'à la première requête qui le réclame
48
+ ```
49
+
50
+ ## Deux noms, et ils ne servent pas à la même chose
51
+
52
+ C'est le piège n°1, et il ne produit aucune erreur — juste un `undefined` :
53
+
54
+ ```ts
55
+ @injectable() // ← nomme la CLASSE : @inject("BillingService")
56
+ export class BillingService extends Service {
57
+ constructor() {
58
+ super("billing", ...); // ← nomme l'INSTANCE : container.get("billing")
59
+ }
60
+ }
61
+ ```
62
+
63
+ ## Obtenir un service depuis un autre — deux voies, un choix
64
+
65
+ | Voie | Quand |
66
+ | ------------------------------ | ----------------------------------------------------------------------------- |
67
+ | `@inject("X")` au constructeur | **par défaut** — la dépendance est déclarée, l'ordre de démarrage la respecte |
68
+ | `container.get("x")` à l'usage | quand la dépendance est facultative, tardive, ou choisie à l'exécution |
69
+
70
+ `create service --inject` pose la première. Le second est visible partout dans les exemples, et
71
+ c'est pour cela qu'il est sur-employé : **déclarer vaut mieux que chercher.**
72
+
73
+ ## Prouver
74
+
75
+ ```bash
76
+ npx nodefony doctor # « porte @injectable mais n'est déclaré nulle part »
77
+ npx nodefony inspect services # ce que le conteneur porte VRAIMENT au démarrage
78
+ npm test
79
+ ```
80
+
81
+ `inspect services` est le seul juge : ni la compilation ni les tests ne voient un service absent
82
+ du conteneur — les tests l'instancient eux-mêmes.
83
+
84
+ ## Voisins
85
+
86
+ | Besoin | Skill |
87
+ | ------------------------------ | ------------------------------- |
88
+ | Une ressource complète stockée | `nodefony-add-crud` |
89
+ | Réserver une route | `nodefony-protect-route` |
90
+ | Un flux temps réel | `nodefony-add-realtime-channel` |
@@ -0,0 +1,416 @@
1
+ ---
2
+ name: nodefony-browser
3
+ description: >
4
+ Ouvre un écran de ton application dans un navigateur piloté pour le VOIR et surtout le MESURER —
5
+ contrastes et tailles réellement calculés par le moteur de rendu, audit d'accessibilité par
6
+ axe-core, erreurs de console, requêtes HTTP, frames WebSocket. Fonctionne sur ta machine
7
+ (Playwright) ou dans un conteneur jetable, au choix. Porte les sondes prêtes à l'emploi, le choix
8
+ du thème clair ou sombre — un défaut d'affichage n'existe souvent que dans l'un des deux —, les
9
+ contraintes de réseau qui font répondre `421` ou `401` à une application pourtant saine, et les
10
+ pièges qui font conclure FAUX : mesurer avant que l'écran soit peuplé, observer un bundle qui
11
+ n'est plus celui du code, prendre une condition d'arrêt qui réussit toujours. Sait aussi piloter
12
+ un socket temps réel de bout en bout : accueil, abonnement à un canal, action, latence médiane,
13
+ pont API, reconnexion. À charger AVANT de conclure quoi que ce soit sur un écran.
14
+ Déclencheurs : "regarde l'écran", "vérifie l'affichage", "est-ce que ça s'affiche ?",
15
+ "montre-moi la page", "lis la console", "y a-t-il des erreurs JS ?", "mesure le contraste",
16
+ "cette couleur est-elle lisible ?", "capture d'écran", "vérifie l'accessibilité",
17
+ "audit accessibilité", "audit WCAG", "en mode clair", "en mode sombre", "le thème sombre",
18
+ "la page est-elle rapide ?", "temps de chargement", "responsive ?",
19
+ "quelles requêtes fait la page ?", "le temps réel arrive-t-il jusqu'à l'écran ?",
20
+ "teste le socket", "mesure la latence du websocket", "le canal pousse-t-il ?",
21
+ "l'application démarre-t-elle vraiment ?".
22
+ ---
23
+
24
+ # see-screen — voir et MESURER un écran
25
+
26
+ > ⚖️ **La confiance n'exclut pas le contrôle.** Un `curl` prouve qu'une route répond ; il ne dit
27
+ > pas si l'écran se monte, s'alimente et ne crie pas dans la console.
28
+
29
+ ## Le geste — sur ta machine
30
+
31
+ ```bash
32
+ npm i -D playwright
33
+ npx playwright install chromium
34
+ node node_modules/@nodefony/devkit/skills/nodefony-browser/scripts/inspect.mjs /
35
+ ```
36
+
37
+ C'est tout. Les sondes **constatent** où elles s'exécutent : sur ta machine elles visent
38
+ `https://127.0.0.1:5152` et déposent leurs captures dans `tmp/browser/`. Rien à configurer tant que
39
+ tu ne changes pas de port — et `NF_BROWSER_BASE` est là si tu le changes.
40
+
41
+ Playwright est un **pair optionnel** — quelques mégaoctets, pas cent : il serait déraisonnable
42
+ d'imposer un navigateur complet à qui n'a pas besoin de regarder un écran. Tant qu'il manque, les
43
+ sondes s'arrêtent en le disant, avec la commande exacte à taper — jamais sur un « module
44
+ introuvable » nu.
45
+
46
+ ### Le navigateur : celui que tu as déjà, avant d'en télécharger un
47
+
48
+ Les sondes essaient, dans l'ordre : le **`chromium`** du pilote, puis **`chrome`**, puis
49
+ **`msedge`** — les deux derniers étant ceux DÉJÀ installés sur la machine. Sous Windows, Edge est
50
+ préinstallé : rien à télécharger. Le champ **`browserName`** de la sortie dit lequel a servi, parce
51
+ que deux mesures faites par des navigateurs différents ne se comparent pas.
52
+
53
+ Si aucun ne répond, la sonde s'arrête (code 69) en donnant la commande — le téléchargement se fait
54
+ **une fois par machine**, dans un cache utilisateur partagé par tous tes projets, jamais dans
55
+ `node_modules` :
56
+
57
+ ```bash
58
+ npx playwright install chromium
59
+ ```
60
+
61
+ `NF_BROWSER_ENGINE=chrome` (ou `msedge`, `chromium`) impose un navigateur précis. Un choix explicite
62
+ n'est **jamais** complété par un repli : se rabattre en silence attribuerait la mesure au mauvais
63
+ moteur. ⚠️ À ne pas confondre avec `NF_BROWSER_CHANNEL`, qui désigne le canal d'un socket
64
+ applicatif — le mot « canal » sert aux deux dans des mondes différents.
65
+
66
+ ## L'autre voie — en conteneur, et QUAND s'en servir
67
+
68
+ 🔴 **Le conteneur est un DERNIER RECOURS, jamais le réflexe.** Playwright pilote un navigateur déjà
69
+ présent sur ta machine — il n'y a le plus souvent rien à installer ni à démarrer. Le conteneur ne se
70
+ justifie que par les trois lignes « conteneur » du tableau ci-dessous ; ailleurs, il rallonge tout
71
+ (copie des sondes à chaque modification, nom d'hôte particulier, ports publiés) pour un résultat
72
+ identique.
73
+
74
+ Le conteneur n'est pas « la bonne façon » : c'est un compromis, et il se choisit sur ce que tu es en
75
+ train de faire.
76
+
77
+ <!-- prettier-ignore -->
78
+ | Ce que tu fais | La voie | Pourquoi |
79
+ | --- | --- | --- |
80
+ | Corriger un écran et vérifier ta correction | **locale** | la boucle est bien plus courte — pas de copie vers un conteneur entre deux essais |
81
+ | Regarder une page vite fait, lire la console | **locale** | une commande, rien à démarrer |
82
+ | **Comparer une mesure** dans le temps ou entre machines | **conteneur** | l'image est épinglée par empreinte : le même navigateur aujourd'hui et dans six mois. Un contraste ne bouge pas d'une version de navigateur à l'autre — un temps de rendu ou un Web Vital, si |
83
+ | **Intégration continue** | **conteneur** | un exécuteur sans interface graphique a déjà tout ; même décor qu'en local |
84
+ | Piloter une session authentifiée avec des identifiants **sensibles** | **conteneur** | le navigateur n'y voit ni ton disque ni ton réseau local |
85
+ | Tu ne veux **rien** installer, ou ta machine n'a pas les bibliothèques | **conteneur** | l'image porte navigateur, pilote et dépendances système |
86
+
87
+ Ce que le conteneur coûte, en revanche : le démarrer, recopier les sondes à chaque modification,
88
+ joindre ton application par un nom particulier (ci-dessous), et publier les ports. Sur une boucle de
89
+ correction, cela se paie à chaque tour.
90
+
91
+ ```bash
92
+ docker compose --profile browser up -d
93
+ docker cp node_modules/@nodefony/devkit/skills/nodefony-browser/scripts/. mon-app-browser:/app/see-screen
94
+ docker cp node_modules/axe-core/axe.min.js mon-app-browser:/app/see-screen/axe.min.js
95
+ docker exec mon-app-browser node /app/see-screen/inspect.mjs /
96
+ ```
97
+
98
+ Le conteneur s'appelle **`<nom-de-ton-app>-browser`** — fixé par le `compose.yaml`, rien à chercher.
99
+
100
+ > Le **`/.`** de la copie n'est pas décoratif : il copie le CONTENU du dossier. Sans lui, une
101
+ > seconde copie **imbrique** un dossier de plus au lieu de remplacer, et tu relances alors une
102
+ > version périmée des sondes en croyant les avoir mises à jour — sans le moindre message.
103
+
104
+ La troisième ligne emporte `axe-core`, qui vit dans les dépendances de ton projet et n'est donc pas
105
+ dans le dossier des sondes. Sans elle, la famille `axe` s'annonce **indisponible** et te donne cette
106
+ commande — elle ne rend jamais un verdict qu'elle n'a pas mesuré.
107
+
108
+ Ces commandes tiennent chacune sur **une ligne** et n'emploient ni substitution, ni tube, ni
109
+ continuation : elles passent telles quelles dans un terminal Linux, macOS, PowerShell ou `cmd.exe`.
110
+
111
+ > **Ne demande jamais à l'utilisateur de jouer la sonde** (« recharge et dis-moi ce que dit la
112
+ > console »). C'est le travail de cet outil, quelle que soit la voie choisie.
113
+
114
+ ## Ce que la sonde rend, et qu'une capture ne dit pas
115
+
116
+ ```bash
117
+ NF_BROWSER_LOGIN=/login NF_BROWSER_USER=admin NF_BROWSER_PASSWORD=secret NF_BROWSER_PROBES="bouton principal=button[type=submit],titre=h1" node node_modules/@nodefony/devkit/skills/nodefony-browser/scripts/inspect.mjs /tableau-de-bord "Chiffre d affaires"
118
+ ```
119
+
120
+ <details><summary>La même chose en conteneur (dernier recours)</summary>
121
+
122
+ ```bash
123
+ docker exec -e NF_BROWSER_LOGIN=/login -e NF_BROWSER_USER=admin -e NF_BROWSER_PASSWORD=secret -e "NF_BROWSER_PROBES=bouton principal=button[type=submit],titre=h1" mon-app-browser node /app/see-screen/inspect.mjs /tableau-de-bord "Chiffre d affaires"
124
+ ```
125
+
126
+ </details>
127
+
128
+ Le troisième argument est un **texte discriminant** attendu avant toute mesure (voir les pièges).
129
+
130
+ ```json
131
+ {
132
+ "url": "https://host.docker.internal:5152/tableau-de-bord",
133
+ "theme": "light",
134
+ "lang": "fr",
135
+ "title": "Mon application",
136
+ "scripts": ["/static/index-B7fK2p.js"],
137
+ "probes": [
138
+ {
139
+ "label": "bouton principal",
140
+ "text": "Enregistrer",
141
+ "color": "rgb(255, 255, 255)",
142
+ "background": "rgb(0, 87, 156)",
143
+ "contrast": 7.39,
144
+ "font": "16px",
145
+ "wcag": "AAA",
146
+ "size": "243×41"
147
+ }
148
+ ],
149
+ "consoleErrors": [],
150
+ "capture": "tmp/browser/tableau-de-bord-….png"
151
+ }
152
+ ```
153
+
154
+ **Le contraste est CALCULÉ, pas estimé** — luminances WCAG sur les couleurs que le moteur de rendu
155
+ applique vraiment, fond effectif obtenu en EMPILANT toutes les couches translucides jusqu'au premier
156
+ ancêtre opaque, puis en les composant. C'est ce qui sépare
157
+ « ça me paraît lisible » de « 7,39:1, donc AAA », et ce qui permet de valider une correction de
158
+ palette sans attendre un audit complet.
159
+
160
+ Le champ `wcag` tranche pour toi, parce que le seuil dépend de la **police** et non de la taille du
161
+ bloc : WCAG appelle « large » un texte d'au moins 24 px (ou 18,66 px en gras) et lui applique 3:1 au
162
+ lieu de 4,5:1. Un contraste rendu sans sa police laisse le lecteur choisir son seuil au hasard —
163
+ c'est-à-dire ne rien conclure.
164
+
165
+ **Un sélecteur par élément qui t'intéresse** : un contraste n'existe pas « pour une page », il existe
166
+ pour un élément contre son fond. La sonde ne connaît donc aucun sélecteur — les tiens viennent de
167
+ `NF_BROWSER_PROBES`, et c'est ce qui la garde utilisable quelle que soit ta bibliothèque de
168
+ composants.
169
+
170
+ Réglages par variables d'environnement : `NF_BROWSER_BASE`, `NF_BROWSER_PAGE`, `NF_BROWSER_EXPECT`,
171
+ `NF_BROWSER_LOGIN`, `NF_BROWSER_USER`, `NF_BROWSER_PASSWORD`, `NF_BROWSER_PROBES`
172
+ (`libellé=sélecteur`, séparés par des virgules), `NF_BROWSER_FAMILIES`, `NF_BROWSER_WIDTHS`,
173
+ `NF_BROWSER_SEUIL_LOURD`, `NF_BROWSER_SEUIL_LENT`, `NF_BROWSER_ACTIONS`, `NF_BROWSER_FULLPAGE`. Le détail vit dans l'en-tête de chaque script.
174
+
175
+ ### Un écran qui n'existe qu'après un GESTE — `NF_BROWSER_ACTIONS`
176
+
177
+ Certaines pages ne sont pas un état mais un **parcours** : un formulaire qui ne déplie ses questions
178
+ qu'après un choix, un menu qui s'ouvre au survol, un panneau qui demande un second clic.
179
+ Photographier sans agir fait conclure « le champ n'y est pas » alors qu'on ne l'a jamais ouvert.
180
+
181
+ ```bash
182
+ NF_BROWSER_ACTIONS="Nouvelle commande|voir:Moyen de paiement" node node_modules/@nodefony/devkit/skills/nodefony-browser/scripts/inspect.mjs /commandes "Commandes"
183
+ ```
184
+
185
+ Grammaire : `verbe:cible[=valeur]`, séquence séparée par `|`, verbe facultatif (`clic` par défaut),
186
+ cible cherchée d'abord comme **texte visible** puis comme sélecteur CSS. Verbes : `clic` · `double`
187
+ · `droit` (clic droit) · `survol` · `saisir:Nom=valeur` · `touche:Enter` · `voir` · `defiler:600`
188
+ · `attendre`.
189
+
190
+ - 🔴 **`voir` plutôt qu'un clic pour amener dans la vue** : sur un formulaire, le texte d'une
191
+ question est un `label` — cliquer dessus coche la case qu'il décrit, et tu observes alors un écran
192
+ que l'observation a modifié.
193
+ - ⚠️ **`defiler` n'est pas `NF_BROWSER_FULLPAGE=1`** : une application dont le contenu défile dans
194
+ un conteneur interne (toute interface à barre latérale fixe) ne grandit pas — la capture « page
195
+ entière » y rend exactement la fenêtre, et l'on croit que ce qui est plus bas n'existe pas.
196
+ - Une action dont la cible est introuvable **arrête la sonde** (code 65) en la nommant : une mesure
197
+ faite sur un écran qu'on n'a pas ouvert est pire qu'aucune mesure.
198
+
199
+ **`NF_BROWSER_LOGIN` n'a pas de défaut** : c'est le chemin du formulaire de connexion de **ton**
200
+ application. Il n'en existe pas d'universel, et deviner enverrait la sonde sur une page inexistante,
201
+ où elle mesurerait un écran d'erreur en croyant s'être authentifiée. Sans lui, un identifiant posé
202
+ fait s'arrêter la sonde avec un message — jamais une mesure fausse.
203
+
204
+ Les sondes de couleur cherchent tes sélecteurs, pas ceux d'une bibliothèque : le thème est lu sur le
205
+ `color-scheme` **calculé** (ce que le moteur applique) et sur `data-theme`. Si ton application marque
206
+ son thème autrement, sonde-le comme n'importe quel autre élément.
207
+
208
+ ### Mesurer dans le thème que tu veux — pas seulement celui par défaut
209
+
210
+ **Un défaut d'affichage n'existe souvent que dans UN des deux thèmes.** Vécu : un libellé de menu
211
+ actif à **1,62:1** en clair — illisible — et impeccable en sombre, où la même variable de couleur
212
+ rend une nuance opposée. Tant qu'on ne regarde qu'un seul thème, la palette paraît saine.
213
+
214
+ ```bash
215
+ NF_BROWSER_COLOR_SCHEME=light NF_BROWSER_FAMILIES=axe node .../scripts/inspect.mjs /tableau-de-bord "Chiffre d affaires"
216
+ ```
217
+
218
+ `NF_BROWSER_COLOR_SCHEME` émule `prefers-color-scheme` — la média query standard, comprise quelle
219
+ que soit ta trousse d'interface. Valeurs : `light`, `dark`, `no-preference` ; toute autre est
220
+ **refusée** (code 64), car l'accepter ferait mesurer le thème par défaut en croyant tenir l'autre.
221
+
222
+ Si ton application **mémorise** le choix de l'utilisateur, elle n'obéit plus à cette média query :
223
+ donne alors la clé de stockage, qui t'appartient — le code ne la devine pas.
224
+
225
+ ```bash
226
+ NF_BROWSER_STORAGE="ma-cle-de-theme=light" node .../scripts/inspect.mjs /
227
+ ```
228
+
229
+ Le champ `theme` de la sortie dit ce qui a été RÉELLEMENT appliqué. Vérifie-le : c'est ainsi qu'on
230
+ sait qu'on a mesuré le bon écran.
231
+
232
+ ## Les familles de sondes — activables, jamais un mur de JSON
233
+
234
+ Le socle ci-dessus sort toujours. Le reste s'active par famille, chacune rendant un **verdict**
235
+ (`OK`/`ALERTE`) et des données bornées — comptes et 3 exemples, jamais l'inventaire :
236
+
237
+ ```bash
238
+ NF_BROWSER_FAMILIES=axe,perf,reseau node .../scripts/inspect.mjs /tableau-de-bord "Chiffre d affaires"
239
+ ```
240
+
241
+ | Famille | Question à laquelle elle répond |
242
+ | ------------ | -------------------------------------------------------------------------------------------------- |
243
+ | **`axe`** | **Audit WCAG complet par `axe-core` — une centaine de règles, dont le contraste de TOUT le texte** |
244
+ | `a11y` | Étiquettes, noms accessibles, hiérarchie des titres, cibles < 24 px, arbre d'accessibilité |
245
+ | `rendu` | Débordement horizontal, éléments hors viewport, polices RÉELLEMENT chargées |
246
+ | `reseau` | Requêtes, échecs, ressources lourdes et lentes, octets réellement transférés |
247
+ | `perf` | TTFB, FCP, LCP, CLS, tâches longues — verdict sur les seuils Web Vitals |
248
+ | `stockage` | Attributs des cookies et inventaire du Web Storage — **jamais les valeurs** |
249
+ | `responsive` | Le débordement horizontal rejoué à plusieurs largeurs (`NF_BROWSER_WIDTHS`) |
250
+
251
+ `NF_BROWSER_FAMILIES=toutes` active tout ; un nom inconnu est **refusé** (code 64), jamais ignoré.
252
+ Ce que chaque champ veut dire, comment lire un verdict, et **quand chaque famille se trompe** :
253
+ [`references/sondes.md`](references/sondes.md) — à lire avant de conclure sur un `ALERTE`.
254
+
255
+ > 🔴 **Pour l'accessibilité, prends `axe` — pas `a11y` seule, et n'écris JAMAIS ton propre calcul.**
256
+ > Les règles WCAG sont pleines de cas particuliers qu'on ne devine pas : canaux en 0–1 des
257
+ > couleurs CSS modernes, fonds semi-transparents à composer sur ce qu'il y a dessous, texte peint
258
+ > par une police en couleurs, éléments masqués aux seules techniques d'assistance. Une sonde écrite
259
+ > à la main les rate et produit des échecs inventés **qui noient les vrais** — mesuré : quarante et
260
+ > un faux positifs contre sept défauts réels, dont celui qu'on cherchait, invisible au milieu.
261
+ > `axe-core` est le moteur qu'embarque Lighthouse pour ce volet ; `a11y` reste utile pour ce qu'il
262
+ > ne fait pas — l'arbre d'accessibilité brut et les cibles trop petites.
263
+ >
264
+ > `axe` distingue trois choses, et la nuance compte : les **manquements** (avérés, ils font
265
+ > l'alerte), les cas **à vérifier** (le moteur refuse de trancher — un fond en image, par exemple —
266
+ > et ce n'est PAS un défaut), et les règles **conformes**. Chaque manquement rend jusqu'à cinq
267
+ > cibles distinctes plus le compte des autres : une même règle couvre des défauts à des endroits
268
+ > différents, qui ne se corrigent pas d'un seul geste.
269
+
270
+ ## Auditer la page comme un moteur de recherche et un agent la voient — `audit.mjs`
271
+
272
+ Lighthouse complet, y compris **derrière une authentification** — ce que l'extension du navigateur
273
+ ne sait pas faire sur une application protégée.
274
+
275
+ ```bash
276
+ npm i -D lighthouse
277
+ NF_BROWSER_LOGIN=/login NF_BROWSER_USER=admin NF_BROWSER_PASSWORD=secret node .../scripts/audit.mjs /tableau-de-bord
278
+ ```
279
+
280
+ Il rend les scores des cinq catégories — dont **`agentic-browsing`**, qui note ce qu'un agent
281
+ d'intelligence artificielle trouve en arrivant sur ta page : arbre d'accessibilité bien formé,
282
+ stabilité visuelle, annotations **WebMCP** de tes formulaires, outils déclarés, et présence d'un
283
+ `llms.txt`. Puis les audits ratés, **classés par poids** — ce qui coûte le plus à ta note, en
284
+ premier.
285
+
286
+ ```json
287
+ {
288
+ "verdict": "ALERTE",
289
+ "decor": { "appareil": "desktop", "bridage": "simulate" },
290
+ "scores": {
291
+ "performance": 30,
292
+ "accessibility": 93,
293
+ "best-practices": 100,
294
+ "seo": 91,
295
+ "agentic-browsing": 96
296
+ },
297
+ "failedAudits": { "total": 22, "examples": [] },
298
+ "fullReport": "tmp/browser/lighthouse-….json"
299
+ }
300
+ ```
301
+
302
+ Le rapport COMPLET est déposé à côté : le résumé sert à décider, l'original à vérifier et à
303
+ comparer dans le temps. Tu peux l'ouvrir tel quel dans une visionneuse Lighthouse.
304
+
305
+ **Trois choses à savoir, sans quoi les chiffres trompent :**
306
+
307
+ - **Le `decor` fait partie de la mesure.** Un score de performance sans son appareil ne veut rien
308
+ dire. Le défaut est `desktop` ; `NF_BROWSER_FORMFACTOR=mobile` simule un téléphone bridé, et les
309
+ chiffres n'ont alors plus rien à voir.
310
+ - **Ne juge pas la performance d'un serveur de DÉVELOPPEMENT.** Modules servis un par un, sources
311
+ non minifiées, rechargement à chaud : la note s'effondre pour des raisons qui n'existent pas en
312
+ production. Cette catégorie ne se mesure que sur une version bâtie.
313
+ - **Un audit sans score n'a pas échoué** — il ne s'applique pas. Les audits WebMCP et `llms.txt`
314
+ sortent ainsi tant que tu ne les as pas mis en place ; c'est une indication, pas un reproche.
315
+
316
+ > **Comment l'authentification survit** alors que Lighthouse ouvre son propre onglet : le navigateur
317
+ > est lancé avec un profil PERSISTANT et un port de débogage ; on s'y connecte, puis Lighthouse s'y
318
+ > branche et hérite du profil. Et `disableStorageReset` est posé — sans lui, Lighthouse **vide le
319
+ > stockage** avant de mesurer, donc les témoins de session, et audite l'écran de connexion sans le
320
+ > dire.
321
+
322
+ ## Observer ce qui se PASSE — `watch.mjs`
323
+
324
+ `inspect.mjs` photographie un instant ; celui-ci regarde le temps qui coule. C'est la seule façon de
325
+ voir une frame qui n'arrive pas, un canal qui pousse trop, une reconnexion en boucle — rien de tout
326
+ cela n'apparaît sur une capture.
327
+
328
+ ```bash
329
+ docker exec mon-app-browser node /app/see-screen/watch.mjs /tableau-de-bord 7000
330
+ docker exec -e "NF_BROWSER_UNTIL=() => document.querySelectorAll('tbody tr').length >= 3" mon-app-browser node /app/see-screen/watch.mjs /articles 8000
331
+ ```
332
+
333
+ La seconde s'arrête sur une **condition applicative** plutôt que sur une durée. Note les guillemets :
334
+ doubles à l'extérieur, simples à l'intérieur de l'expression — l'inverse ne survit pas à `cmd.exe`.
335
+
336
+ Il rend les **sockets et leurs frames horodatées dans les deux sens**, les réponses HTTP ≥ 400, les
337
+ erreurs de console et le verdict de la condition. Un controller temps réel se vérifie ainsi de bout
338
+ en bout : le message part-il, revient-il, et l'écran le reçoit-il ?
339
+
340
+ > 🔴 **Une condition d'arrêt se vérifie avec une condition IMPOSSIBLE.** Une chaîne passée à
341
+ > `waitForFunction` est évaluée comme une **expression** : `() => x` y **définit** une fonction sans
342
+ > jamais l'appeler, l'objet fonction est truthy, et l'attente réussit **toujours** — même sur une
343
+ > condition qui ne peut pas être vraie. La sonde invoque désormais les formes fonction ; le principe,
344
+ > lui, vaut pour toute attente que tu écriras : tant qu'elle n'a pas échoué une fois, elle ne
345
+ > discrimine rien.
346
+
347
+ ## Piloter le socket de bout en bout — `socket.mjs`
348
+
349
+ `watch.mjs` regarde le trafic d'une page ; celui-ci **conduit** : il ouvre un socket temps réel
350
+ depuis la page (cookies et `Origin` réels), attend l'accueil, s'abonne à un canal, appelle une
351
+ action, mesure la latence médiane, rejoue une route par le pont API, ferme et se reconnecte — un
352
+ verdict par étape.
353
+
354
+ ```bash
355
+ docker exec -e NF_BROWSER_API=/api/sante mon-app-browser node /app/see-screen/socket.mjs /chat/realtime
356
+ ```
357
+
358
+ Le chemin du endpoint est **requis** (1er argument ou `NF_BROWSER_SOCKET`) : c'est une route de ton
359
+ application, rien n'est deviné. `NF_BROWSER_CHANNEL` choisit le canal (défaut : le premier annoncé
360
+ par l'accueil) ; `NF_BROWSER_ACTION` une action RPC ; sans méthode corrélée la latence est
361
+ `NON MESURÉE` — jamais un zéro inventé, car la notification `ping` n'a pas de pong.
362
+
363
+ Le protocole du fil (les quatre formes de frame), la lecture de chaque verdict — dont
364
+ `SILENCIEUX`, qui n'est **pas** « cassé » — et les pièges :
365
+ [`references/socket.md`](references/socket.md).
366
+
367
+ ## Trois contraintes de réseau — chacune imite un bug applicatif
368
+
369
+ <!-- prettier-ignore -->
370
+ | Contrainte | Ce qui arrive sinon |
371
+ | --- | --- |
372
+ | Joindre l'app par **`host.docker.internal`** | `localhost` désigne le CONTENEUR, pas ta machine. Et si tu as activé `domainCheck`, ajoute ce nom aux `trustedHosts` en développement : sinon la barrière répond **`421`** alors que le réseau passe. |
373
+ | Passer par **HTTPS** | Le cookie de session est `secure` : sur une origine `http://` non-`localhost`, le navigateur le **jette**, et tout revient en **`401`** — ce qui se lit à tort comme un login qui rate. |
374
+ | **Rien à poser** pour rendre Vite joignable | L'origine des assets se dérive du `Host` de ta requête : arriver par `host.docker.internal` suffit — l'allowlist Vite et le WebSocket du rechargement à chaud suivent le même nom, et ton poste reste servi sur `127.0.0.1` en même temps. |
375
+
376
+ Si la page annonce quand même `127.0.0.1` depuis le conteneur, c'est que le nom ne franchit pas
377
+ `trustedHosts`, ou qu'une `publicOrigin` explicite est configurée dans `nodefony.config.ts` — un
378
+ réglage durable gagne toujours sur une déduction.
379
+
380
+ ## Pièges — chacun a déjà fait conclure faux
381
+
382
+ - **🔴 Mesurer trop tôt.** Une application se monte, PUIS demande ses données. Attendre un « réseau
383
+ calme » te fait mesurer un écran vide, avec des sondes absentes et des `401` encore en vol.
384
+ Attends un **texte discriminant de la page visée** — pas un texte présent aussi sur l'écran de
385
+ connexion (le nom de l'application aboutit dans les deux cas : il ne prouve rien).
386
+ - **🔴 Le bundle servi n'est pas toujours celui que tu as bâti.** À contrôler AVANT d'accuser ton
387
+ code, sinon tu débogues une génération précédente. Le champ **`scripts`** rendu par `inspect.mjs`
388
+ donne les fichiers réellement servis à la page : compare-les à ceux que désigne l'`index.html`
389
+ produit dans `dist/frontend/` de ton module. Deux valeurs différentes ⇒ rebâtis, **redémarre le
390
+ serveur** (le service d'assets lit son `index.html` au démarrage), puis redémarre le conteneur —
391
+ son cache HTTP survit à un simple rechargement.
392
+ - **Les erreurs de console d'un parcours de connexion ne sont pas des défauts.** Se connecter
393
+ produit des `401` sur la vérification d'identité ; ils disparaissent dès que l'état
394
+ d'authentification est réutilisé.
395
+ - **Une capture ne s'écrase pas.** Réutiliser un nom laisse l'ancienne image en place pendant que
396
+ l'appel répond « OK » : tu lis un écran périmé. Les sondes horodatent — ne le contourne pas.
397
+ - **Un état d'authentification sauvegardé peut être périmé** (session expirée, serveur redémarré).
398
+ Les sondes le constatent et refont le parcours plutôt que de mesurer l'écran de connexion.
399
+ - **Chaque compte a SON état sauvegardé** (le fichier porte l'identifiant). C'est ce qui permet
400
+ d'enchaîner deux sondes sous deux comptes — comparer ce que voit un administrateur et ce que voit
401
+ un utilisateur ordinaire — sans que la seconde reprenne la session de la première. Sans cela on
402
+ demande une mesure sous un compte et l'on obtient celle de l'autre, sans aucun message.
403
+
404
+ ## L'autre voie : le serveur MCP du conteneur
405
+
406
+ La même image expose un serveur MCP (`http://127.0.0.1:3001/mcp`) auquel un agent se branche pour
407
+ **explorer** une page interactivement. Prends-le pour cela — et le pilotage direct ci-dessus pour
408
+ tout le reste : une commande, un JSON, un code de retour, quelques secondes. Le protocole
409
+ intermédiaire coûte plusieurs fois ce temps, ne rend pas de valeur exploitable par un script, et
410
+ sa session peut tomber sous toi.
411
+
412
+ ## Ce que ce navigateur ne remplace pas
413
+
414
+ Le rechargement à chaud, l'animation et le rendu fin (polices, sous-pixel) se jugent dans un vrai
415
+ navigateur, sur ton poste. Celui-ci répond à « l'écran se monte-t-il, s'alimente-t-il, crie-t-il ? »
416
+ — et en tire des nombres.