designwithai-mcp 0.4.0 → 0.6.0

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.
@@ -0,0 +1,142 @@
1
+ /**
2
+ * L'EXPORT DES CONSTATS — vers les outils que l'équipe emploie déjà.
3
+ *
4
+ * Trois verbes manquaient au produit : assigner, informer, tracer. Ils
5
+ * demandaient jusqu'ici trois intégrations, trois jeux de secrets à stocker et
6
+ * trois API à maintenir — pour un produit dont la promesse est de ne rien
7
+ * garder de ce qui ne lui appartient pas.
8
+ *
9
+ * LA SORTIE PAR LE HAUT : on n'écrit dans aucun outil. On produit un texte
10
+ * parfaitement formaté, et c'est l'assistant de l'utilisateur — chez lui, avec
11
+ * SES serveurs déjà branchés — qui le pose dans Jira, Linear ou Slack. Zéro
12
+ * secret stocké, zéro jeton à révoquer, zéro API à suivre quand elle change.
13
+ *
14
+ * DÉTERMINISTE, ET C'EST LA CONDITION. Aucun appel de modèle ici : le même
15
+ * audit rend exactement le même texte, à la virgule près. Un export qui varie
16
+ * d'une fois sur l'autre produirait deux tickets pour un seul défaut — et
17
+ * personne ne saurait lequel fermer.
18
+ */
19
+ import { severityRank, worstSeverity, deriveVerdict, verdictLisible } from "./severity.js";
20
+ /** Le seuil : ce qui mérite un ticket. Un mineur encombre une file, il ne la sert pas. */
21
+ export const SEUIL_TICKET = 2; // majeur et au-dessus
22
+ /**
23
+ * Ce qu'on lit en tête d'un constat.
24
+ *
25
+ * Les reviews d'avant n'ont pas de titre. Le repli le DIT au lieu d'afficher un
26
+ * vide — un ticket sans intitulé est un ticket que personne n'ouvre.
27
+ */
28
+ export function titre(f) {
29
+ return (f.title || f.summary || "Constat sans intitulé — voir la preuve").trim();
30
+ }
31
+ const ordonner = (findings) => [...findings].sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
32
+ /** Ce qui reste ouvert. Un constat corrigé n'a rien à faire dans une file. */
33
+ const ouverts = (findings) => findings.filter((f) => (f.status ?? "open") === "open");
34
+ /**
35
+ * Le bloc des limites, en pied de chaque sortie.
36
+ *
37
+ * IL NE S'OMET JAMAIS QUAND IL Y A QUELQUE CHOSE À DIRE. Un ticket se lit par
38
+ * quelqu'un qui n'a pas vu l'analyse : sans le périmètre, il prend un constat
39
+ * partiel pour un constat complet, et l'absence d'un défaut pour sa correction.
40
+ */
41
+ function blocLimites(review) {
42
+ const l = review.limites ?? [];
43
+ if (l.length === 0)
44
+ return "";
45
+ return (`\n\n---\nCe que cette lecture n'a pas couvert :\n` +
46
+ l.map((x) => `- ${x}`).join("\n") +
47
+ `\nUn constat absent d'ici ne prouve donc pas qu'il n'existe pas.`);
48
+ }
49
+ /** Le renvoi vers l'analyse. Un ticket sans retour est un ticket orphelin. */
50
+ function retour(review) {
51
+ return `designwithai · ${review.skill} · ${review.artifact_ref} · analyse ${review.id}`;
52
+ }
53
+ /**
54
+ * Le format « tickets » — un bloc par constat, prêt à devenir une issue.
55
+ *
56
+ * Chaque bloc porte les quatre choses qu'une issue exige et qu'on oublie
57
+ * toujours : ce qui cloche en langage courant, la preuve qui permet de le
58
+ * retrouver, la correction proposée, et le critère qui dira que c'est fini.
59
+ * Sans le dernier, un ticket se ferme sur une impression.
60
+ */
61
+ export function enTickets(review) {
62
+ const retenus = ordonner(ouverts(review.findings)).filter((f) => severityRank(f.severity) <= SEUIL_TICKET);
63
+ if (retenus.length === 0) {
64
+ return (`Aucun constat de gravité majeure ou plus sur ${review.artifact_ref}.\n\n` +
65
+ `Rien à mettre en file. Les constats mineurs et les notes restent dans l'analyse — ` +
66
+ `les sortir ici encombrerait la file de ton équipe sans la servir.` +
67
+ blocLimites(review));
68
+ }
69
+ const blocs = retenus.map((f, i) => {
70
+ const lignes = [
71
+ `## ${i + 1}. ${titre(f)}`,
72
+ "",
73
+ `**Gravité** : ${f.severity ?? "non précisée"}${f.location ? ` · **Où** : ${f.location}` : ""}`,
74
+ ];
75
+ if (f.evidence)
76
+ lignes.push("", `**Ce qui le prouve**`, f.evidence);
77
+ if (f.impact)
78
+ lignes.push("", `**Pourquoi ça compte**`, f.impact);
79
+ if (f.correction)
80
+ lignes.push("", `**Correction proposée**`, f.correction);
81
+ lignes.push("", `**C'est fait quand** : ${f.acceptance ?? "à définir avec l'équipe — l'analyse n'a pas fourni de critère de fin"}`);
82
+ if (f.confidence && f.confidence !== "élevée") {
83
+ lignes.push("", `_Confiance ${f.confidence} : à vérifier avant d'engager du travail._`);
84
+ }
85
+ lignes.push("", `_${retour(review)}_`);
86
+ return lignes.join("\n");
87
+ });
88
+ const entete = `${retenus.length} ticket${retenus.length > 1 ? "s" : ""} à créer — ${review.artifact_ref}\n` +
89
+ `Un bloc par constat. Les mineurs et les notes ne sont pas ici : ils encombreraient la file.`;
90
+ return `${entete}\n\n${blocs.join("\n\n---\n\n")}${blocLimites(review)}`;
91
+ }
92
+ /**
93
+ * Le format « message » — ce qu'on poste dans un canal, tel quel.
94
+ *
95
+ * Il tient en quelques lignes parce qu'un message d'équipe se lit en passant.
96
+ * Le verdict traduit d'abord — personne ne doit traduire « PASS WITH FIXES » —
97
+ * puis ce qui a bougé, puis les trois points les plus graves. Pas dix : trois.
98
+ */
99
+ export function enMessage(review, mouvement) {
100
+ const ouvertsTries = ordonner(ouverts(review.findings));
101
+ const lisible = verdictLisible(review.verdict) ?? verdictLisible(deriveVerdict(ouvertsTries)) ?? "Analysé";
102
+ const lignes = [`**${review.artifact_ref} — ${lisible.toLowerCase()}**`];
103
+ if (mouvement) {
104
+ /* Le mouvement se dit en clair, et « rien n'a bougé » est un résultat.
105
+ Annoncer un progrès qui n'a pas eu lieu est la faute qu'on a mis un
106
+ chantier entier à retirer du rapport ; elle ne rentrera pas par ici. */
107
+ const parts = [];
108
+ if (mouvement.regles > 0)
109
+ parts.push(`${mouvement.regles} réglé${mouvement.regles > 1 ? "s" : ""}`);
110
+ if (mouvement.nouveaux > 0)
111
+ parts.push(`${mouvement.nouveaux} nouveau${mouvement.nouveaux > 1 ? "x" : ""}`);
112
+ if (mouvement.restants > 0)
113
+ parts.push(`${mouvement.restants} encore ouvert${mouvement.restants > 1 ? "s" : ""}`);
114
+ lignes.push(parts.length > 0 ? `Depuis la dernière fois : ${parts.join(" · ")}.` : "Rien n'a bougé depuis la dernière fois.");
115
+ }
116
+ if (ouvertsTries.length === 0) {
117
+ lignes.push("", "Rien d'ouvert à signaler.");
118
+ }
119
+ else {
120
+ lignes.push("", `Les ${Math.min(3, ouvertsTries.length)} plus graves :`);
121
+ for (const f of ouvertsTries.slice(0, 3)) {
122
+ lignes.push(`• ${titre(f)}${f.location ? ` — ${f.location}` : ""} _(${f.severity ?? "?"})_`);
123
+ }
124
+ if (ouvertsTries.length > 3) {
125
+ lignes.push(`_et ${ouvertsTries.length - 3} autre${ouvertsTries.length - 3 > 1 ? "s" : ""}, dans l'analyse complète._`);
126
+ }
127
+ }
128
+ lignes.push("", `_${retour(review)}_`);
129
+ return lignes.join("\n") + blocLimites(review);
130
+ }
131
+ /** Le compte de ce qui partirait, pour le dire avant d'exporter. */
132
+ export function compter(review) {
133
+ const o = ouverts(review.findings);
134
+ return {
135
+ total: review.findings.length,
136
+ exportables: o.filter((f) => severityRank(f.severity) <= SEUIL_TICKET).length,
137
+ pire: worstSeverity(o),
138
+ };
139
+ }
140
+ export function exporter(review, format, mouvement) {
141
+ return format === "tickets" ? enTickets(review) : enMessage(review, mouvement);
142
+ }
package/dist/server.js CHANGED
@@ -18,6 +18,8 @@ import { compareReviews, getReview, listReviews, saveReview, updateFindingStatus
18
18
  import { saveDecision } from "./decisions.js";
19
19
  import { clearGate, loadState, reopenGate, saveState } from "./state.js";
20
20
  import { readRepoSnapshot } from "./connectors.js";
21
+ import { SEVERITES } from "./severity.js";
22
+ import { compter, exporter } from "./export-findings.js";
21
23
  import { inventorierCode } from "./ds-inventory.js";
22
24
  import { buildPayload, rendreEnvoi, rendreInventaire } from "./inventory-tool.js";
23
25
  import { trySemanticScores } from "./embeddings.js";
@@ -196,7 +198,7 @@ const TOOLS = [
196
198
  items: {
197
199
  type: "object",
198
200
  properties: {
199
- severity: { type: "string", enum: ["bloquant", "critique", "majeur", "mineur", "note"] },
201
+ severity: { type: "string", enum: [...SEVERITES] },
200
202
  location: { type: "string" },
201
203
  category: { type: "string" },
202
204
  summary: { type: "string" },
@@ -443,6 +445,42 @@ const TOOLS = [
443
445
  },
444
446
  },
445
447
  },
448
+ {
449
+ name: "export_findings",
450
+ description: "Transforme les constats d'une review en TICKETS prêts à créer, ou en MESSAGE prêt à " +
451
+ "poster. Rien n'est envoyé nulle part : tu reçois un texte, et c'est TOI qui le poses " +
452
+ "dans Jira, Linear, GitHub ou Slack avec les serveurs que l'utilisateur a déjà branchés. " +
453
+ "Déterministe — le même audit rend le même texte.",
454
+ inputSchema: {
455
+ type: "object",
456
+ properties: {
457
+ format: {
458
+ type: "string",
459
+ enum: ["tickets", "message"],
460
+ description: "tickets = un bloc par constat majeur ou plus, avec preuve, correction et critère " +
461
+ "de fin. message = le verdict, ce qui a bougé, et les trois plus graves.",
462
+ },
463
+ review_id: { type: "string", description: "La review à exporter. Par défaut, la dernière." },
464
+ regles: { type: "number", description: "Constats réglés depuis la fois d'avant, si tu le sais." },
465
+ nouveaux: { type: "number", description: "Constats apparus depuis." },
466
+ },
467
+ required: ["format"],
468
+ },
469
+ },
470
+ {
471
+ name: "framing_criteria",
472
+ description: "Charge les CRITÈRES D'ACCEPTATION d'un cadrage écrit avant de construire, pour vérifier " +
473
+ "la feature contre eux — un par un, avec verify_fix. C'est la seule façon de vérifier " +
474
+ "contre des critères posés AVANT : reconstruits de mémoire, ils se moulent sur le " +
475
+ "résultat obtenu.",
476
+ inputSchema: {
477
+ type: "object",
478
+ properties: {
479
+ product_ref: { type: "string", description: "Le produit, pour ne lister que ses cadrages." },
480
+ id: { type: "string", description: "Un cadrage précis, si tu le connais déjà." },
481
+ },
482
+ },
483
+ },
446
484
  {
447
485
  name: "inventory_ds",
448
486
  description: "Inventorie le DESIGN SYSTEM d'un dépôt local — valeurs de style, composants, et lesquels " +
@@ -630,7 +668,7 @@ function toolPlan(args) {
630
668
  }
631
669
  // ─── Reviews suivies (étape C) ───────────────────────────────────────────────
632
670
  function sevCounts(findings) {
633
- const order = ["bloquant", "critique", "majeur", "mineur", "note"];
671
+ const order = SEVERITES;
634
672
  const c = {};
635
673
  for (const f of findings) {
636
674
  const s = f.severity ?? "note";
@@ -692,6 +730,8 @@ function toolListReviews(args) {
692
730
  const DEFAULT_INGEST_URL = "https://hbugvurcdmimhvvfdlos.supabase.co/functions/v1/ingest-audit";
693
731
  /** Même compte, même jeton, autre table : les inventaires ont leur point d'entrée. */
694
732
  const DEFAULT_INVENTORY_URL = "https://hbugvurcdmimhvvfdlos.supabase.co/functions/v1/ingest-inventory";
733
+ /** La lecture des cadrages. Le seul point d'entrée en LECTURE de tout le MCP. */
734
+ const DEFAULT_FRAMING_URL = "https://hbugvurcdmimhvvfdlos.supabase.co/functions/v1/read-framing";
695
735
  async function toolPushAudit(args) {
696
736
  const token = process.env.DWAI_TOKEN?.trim();
697
737
  const url = process.env.DWAI_INGEST_URL?.trim() || DEFAULT_INGEST_URL;
@@ -1022,6 +1062,121 @@ function toolNextGate(args) {
1022
1062
  * lecture : la personne a quand même le droit de savoir ce que son dépôt
1023
1063
  * contient, et ce qu'on n'a pas su y lire.
1024
1064
  */
1065
+ /**
1066
+ * `framing_criteria` — la boucle se referme ici.
1067
+ *
1068
+ * Les critères ont été écrits AVANT de construire, et c'est toute leur valeur.
1069
+ * Les relire au moment de vérifier, plutôt que de les reformuler de mémoire,
1070
+ * est la différence entre une vérification et une relecture complaisante : des
1071
+ * critères reconstruits après coup se moulent sur le résultat obtenu.
1072
+ *
1073
+ * L'outil ne juge rien. Il rend les critères, dans leur forme structurée, et
1074
+ * dit explicitement quoi en faire — c'est `verify_fix` qui statue, un critère à
1075
+ * la fois, avec sa preuve.
1076
+ */
1077
+ /**
1078
+ * `export_findings` — les trois verbes, sans une seule intégration.
1079
+ *
1080
+ * Assigner, informer, tracer manquaient au produit. Les câbler aurait demandé
1081
+ * trois API, trois jeux de secrets et leur maintenance — pour un produit dont
1082
+ * la promesse est de ne rien garder qui ne lui appartienne pas.
1083
+ *
1084
+ * On sort par le haut : le texte est produit ici, parfaitement formaté, et
1085
+ * c'est l'assistant de l'utilisateur qui le pose dans SES outils, avec SES
1086
+ * serveurs déjà branchés. Aucun secret ne transite, aucune API à suivre quand
1087
+ * elle change.
1088
+ */
1089
+ function toolExportFindings(args) {
1090
+ const format = String(args.format ?? "");
1091
+ if (format !== "tickets" && format !== "message") {
1092
+ return refusal("export_findings attend un format : « tickets » ou « message ».");
1093
+ }
1094
+ const review = args.review_id ? getReview(String(args.review_id)) : (listReviews()[0] ?? null);
1095
+ if (!review) {
1096
+ return refusal("Aucune review à exporter. Lance d'abord un skill et enregistre le résultat avec save_review.");
1097
+ }
1098
+ const compte = compter(review);
1099
+ /* Le mouvement n'est employé QUE si l'appelant le connaît. On ne le déduit
1100
+ pas d'une comparaison de photos : c'est exactement ce qui avait produit un
1101
+ « ton produit monte » sur un site que personne n'avait touché. */
1102
+ const mouvement = typeof args.regles === "number" || typeof args.nouveaux === "number"
1103
+ ? {
1104
+ regles: Number(args.regles ?? 0),
1105
+ nouveaux: Number(args.nouveaux ?? 0),
1106
+ restants: review.findings.filter((f) => (f.status ?? "open") === "open").length,
1107
+ }
1108
+ : undefined;
1109
+ const corps = exporter(review, format, mouvement);
1110
+ const entete = format === "tickets"
1111
+ ? `${compte.exportables} constat(s) exportable(s) sur ${compte.total} — les mineurs et les notes restent dans l'analyse.\n` +
1112
+ `CE QUE TU EN FAIS : crée une issue par bloc, avec les serveurs que l'utilisateur a déjà branchés ` +
1113
+ `(Jira, Linear, GitHub). Ne réécris pas les titres : ils sont déjà écrits pour être lus par quelqu'un ` +
1114
+ `qui n'a pas vu l'analyse. Si aucun serveur n'est branché, rends-lui le texte tel quel.`
1115
+ : `CE QUE TU EN FAIS : poste ce message tel quel dans le canal qu'il te dira (Slack, Teams). ` +
1116
+ `Ne l'allonge pas : il est court parce qu'un message d'équipe se lit en passant.`;
1117
+ return text(`${entete}\n\n${corps}`);
1118
+ }
1119
+ async function toolFramingCriteria(args) {
1120
+ const token = process.env.DWAI_TOKEN?.trim();
1121
+ if (!token) {
1122
+ return refusal("framing_criteria a besoin de DWAI_TOKEN dans l'environnement du serveur MCP. " +
1123
+ "Génère un jeton dans ton compte (page Compte → « Connecter mon éditeur »).");
1124
+ }
1125
+ const url = process.env.DWAI_FRAMING_URL?.trim() || DEFAULT_FRAMING_URL;
1126
+ let body;
1127
+ try {
1128
+ const res = await fetch(url, {
1129
+ method: "POST",
1130
+ headers: { "content-type": "application/json" },
1131
+ body: JSON.stringify({
1132
+ token,
1133
+ product_ref: args.product_ref ? String(args.product_ref) : undefined,
1134
+ id: args.id ? String(args.id) : undefined,
1135
+ }),
1136
+ });
1137
+ body = (await res.json().catch(() => ({})));
1138
+ if (!res.ok) {
1139
+ return refusal(res.status === 401
1140
+ ? "Le jeton est inconnu ou révoqué : regénères-en un depuis ton compte."
1141
+ : `La lecture a été refusée (${res.status} ${String(body.error ?? "")}).`);
1142
+ }
1143
+ }
1144
+ catch (e) {
1145
+ return refusal(`Impossible de joindre ${url} : ${e instanceof Error ? e.message : String(e)}`);
1146
+ }
1147
+ const cadrages = (Array.isArray(body.framings) ? body.framings : []);
1148
+ if (cadrages.length === 0) {
1149
+ return text("Aucun cadrage enregistré pour ce produit.\n\n" +
1150
+ "Les critères d'acceptation s'écrivent AVANT de construire, sur le site — sans eux, " +
1151
+ "il n'y a rien contre quoi vérifier, et une vérification faite de mémoire se moule " +
1152
+ "sur ce qu'on a construit.");
1153
+ }
1154
+ const lignes = [];
1155
+ for (const c of cadrages) {
1156
+ const criteres = c.criteria ?? [];
1157
+ lignes.push(`CADRAGE « ${c.feature ?? "sans nom"} » — ${c.id ?? "?"}`);
1158
+ if (c.essai)
1159
+ lignes.push(` Ce qui a été tenté : ${c.essai}`);
1160
+ if (c.verdict)
1161
+ lignes.push(` Verdict du cadrage : ${c.verdict}`);
1162
+ if (criteres.length === 0) {
1163
+ lignes.push(" (aucun critère structuré — rien à vérifier contre)");
1164
+ }
1165
+ else {
1166
+ lignes.push(` Critères d'acceptation (${criteres.length}) :`);
1167
+ for (const k of criteres) {
1168
+ lignes.push(` - ${k.quoi ?? "?"} : de ${k.de ?? "?"} à ${k.a ?? "?"}, d'ici ${k.avant ?? "?"}` +
1169
+ (k.mesure ? ` — mesuré par ${k.mesure}` : " — AUCUNE MESURE INDIQUÉE au cadrage"));
1170
+ }
1171
+ }
1172
+ lignes.push("");
1173
+ }
1174
+ lignes.push("CE QUE TU EN FAIS : appelle verify_fix avec ces critères, UN PAR UN, et une preuve " +
1175
+ "par critère prise dans l'état COURANT du produit. Un critère sans mesure indiquée " +
1176
+ "reste vérifiable — mais dis d'où vient ton chiffre, sinon la vérification vaut ce " +
1177
+ "que vaut une impression.");
1178
+ return text(lignes.join("\n"));
1179
+ }
1025
1180
  async function toolInventoryDs(args) {
1026
1181
  const racine = String(args.path ?? "").trim();
1027
1182
  const productRef = String(args.product_ref ?? "").trim();
@@ -1166,6 +1321,10 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1166
1321
  return toolGenerateDecision(args);
1167
1322
  case "next_gate":
1168
1323
  return toolNextGate(args);
1324
+ case "export_findings":
1325
+ return toolExportFindings(args);
1326
+ case "framing_criteria":
1327
+ return toolFramingCriteria(args);
1169
1328
  case "inventory_ds":
1170
1329
  return toolInventoryDs(args);
1171
1330
  case "connect_repo":
@@ -0,0 +1,71 @@
1
+ /**
2
+ * LA SÉVÉRITÉ ET LE VERDICT, CÔTÉ MCP.
3
+ *
4
+ * La définition de référence vit dans `supabase/functions/_shared/severity.ts` :
5
+ * c'est elle que le serveur applique et que le site affiche. Ce paquet-ci ne
6
+ * peut pas l'importer — il est publié seul sur npm, et un import qui remonte
7
+ * hors de son dossier ne survivrait pas à l'emballage. C'est la même frontière
8
+ * que celle de l'inventaire Figma, et elle se paie une fois.
9
+ *
10
+ * Ce fichier est donc une RÉÉCRITURE, pas une copie de confort. Et
11
+ * `scripts/test-severite-frontiere.ts` empêche les deux de diverger : il
12
+ * compare les deux tables terme à terme, depuis le site qui peut lire les deux.
13
+ *
14
+ * CE QU'IL REMPLACE. La même échelle était écrite à trois endroits du paquet —
15
+ * le type dans `reviews.ts`, l'énumération du schéma d'outil, et un tableau
16
+ * d'ordre au milieu du serveur. Trois copies qui n'avaient aucun moyen de se
17
+ * contredire bruyamment.
18
+ */
19
+ /** Du plus grave au moins grave. L'ordre EST la définition. */
20
+ export const SEVERITES = ["bloquant", "critique", "majeur", "mineur", "note"];
21
+ const RANK = Object.fromEntries(SEVERITES.map((s, i) => [s, i]));
22
+ /** Une sévérité inconnue passe en dernier — jamais devant un défaut réel. */
23
+ export function severityRank(s) {
24
+ return RANK[(s ?? "").toLowerCase().trim()] ?? 9;
25
+ }
26
+ /** La plus grave d'un lot, ou `null` s'il n'y a rien. */
27
+ export function worstSeverity(findings) {
28
+ let pire = null;
29
+ for (const f of findings) {
30
+ const s = (f.severity ?? "").toLowerCase().trim();
31
+ if (!RANK[s])
32
+ if (RANK[s] !== 0)
33
+ continue;
34
+ if (pire === null || severityRank(s) < severityRank(pire))
35
+ pire = s;
36
+ }
37
+ return pire;
38
+ }
39
+ /**
40
+ * Le verdict se DÉDUIT des sévérités, il ne se décide pas.
41
+ *
42
+ * Un bloquant, ou deux critiques, et c'est un échec. Un critique isolé ou un
43
+ * majeur laissent passer avec des corrections. La règle est celle du serveur,
44
+ * terme à terme.
45
+ */
46
+ export function deriveVerdict(findings) {
47
+ const compte = (s) => findings.filter((f) => (f.severity ?? "").toLowerCase().trim() === s).length;
48
+ if (compte("bloquant") > 0)
49
+ return "FAIL";
50
+ if (compte("critique") >= 2)
51
+ return "FAIL";
52
+ if (compte("critique") === 1 || compte("majeur") > 0)
53
+ return "PASS WITH FIXES";
54
+ return "PASS";
55
+ }
56
+ /**
57
+ * LES VERDICTS, DITS EN FRANÇAIS.
58
+ *
59
+ * `PASS`, `FAIL`, `BLOCKED` sont du vocabulaire de fabrication. Ils n'ont rien à
60
+ * faire dans un ticket que lira quelqu'un qui n'a jamais ouvert ce produit.
61
+ */
62
+ export const VERDICT_LISIBLE = {
63
+ PASS: "Ça tient",
64
+ "PASS WITH FIXES": "Presque prêt — des choses à corriger",
65
+ FAIL: "À corriger avant d'ouvrir",
66
+ BLOCKED: "Pas assez d'infos pour juger",
67
+ };
68
+ export function verdictLisible(v) {
69
+ const cle = String(v ?? "").trim().toUpperCase();
70
+ return VERDICT_LISIBLE[cle] ?? null;
71
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "designwithai-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.6.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "description": "Le serveur designwithai pour ton éditeur : audite ton produit — interface, navigation, performance, sécurité des données, hygiène Figma — trace les corrections, et fait remonter les résultats dans ton tableau de bord.",
@@ -26,7 +26,7 @@
26
26
  ],
27
27
  "scripts": {
28
28
  "build": "tsc",
29
- "test": "tsx scripts/test-finding-identity.ts && tsx scripts/test-scoring.ts && tsx scripts/test-ds-diff.ts && tsx scripts/test-ds-match.ts && tsx scripts/test-ds-corpus.ts && tsx scripts/test-ds-inventory.ts && tsx scripts/test-inventory-tool.ts && tsx scripts/check-goldens.ts",
29
+ "test": "tsx scripts/test-finding-identity.ts && tsx scripts/test-scoring.ts && tsx scripts/test-ds-diff.ts && tsx scripts/test-ds-match.ts && tsx scripts/test-ds-corpus.ts && tsx scripts/test-export-findings.ts && tsx scripts/test-ds-inventory.ts && tsx scripts/test-inventory-tool.ts && tsx scripts/check-goldens.ts",
30
30
  "start": "node dist/server.js",
31
31
  "dev": "tsx src/server.ts",
32
32
  "embed": "node scripts/embed.mjs",
@@ -39,6 +39,17 @@ Produit construit rapidement, plusieurs contributeurs, routes hétérogènes, mi
39
39
  | **Données** | Formats de date, nombre, devise, unités, labels et ordre de colonnes. |
40
40
  | **Accessibilité** | Focus, labels, noms accessibles et clavier variables selon les implémentations. |
41
41
 
42
+ ## La frontière avec `better-writing`
43
+
44
+ Les deux juges regardent des mots, et la confusion serait facile.
45
+
46
+ - **Ici, les TERMES et les COMPOSANTS** : le même objet nommé « projet » ici et « espace » là, deux boutons pour la même action, deux formats de date. Une question d'identité — deux choses désignées de deux façons.
47
+ - **Là-bas, le TON et la VOIX** : comment le produit s'adresse aux gens, sur quel registre, avec quelle chaleur dans l'échec. Une question de manière — la même chose dite de deux façons.
48
+
49
+ Un même écran peut relever des deux, et ce n'est pas un chevauchement. Le test qui tranche : qu'est-ce qui changerait en corrigeant ? Un mot remplacé partout, c'est ici. Une phrase réécrite, c'est là-bas.
50
+
51
+ **Ce qui n'est PAS de ce skill** : le tutoiement qui devient vouvoiement, un message d'erreur qui culpabilise, un registre qui glisse d'un écran à l'autre. Ces constats appartiennent à `better-writing`, qui les juge contre la charte de voix déclarée par le produit — une charte que ce skill-ci n'a pas à lire.
52
+
42
53
  ## Séquence d'analyse obligatoire
43
54
 
44
55
  1. **Définir l'échantillon.** Routes, écrans, composants, périodes et plateformes comparés.
@@ -133,6 +133,9 @@ Lot 4 - Polish et prévention
133
133
  - Axes non vérifiés, nommés
134
134
  ```
135
135
 
136
+
137
+ Ces constats s'exportent en tickets ou en message d'équipe — `export_findings`. Rien n'est envoyé nulle part : l'outil rend un texte, et c'est l'assistant de la personne qui le pose dans SES outils, avec SES accès.
138
+
136
139
  ## Critères de qualité du skill
137
140
 
138
141
  - Les skills activés sont justifiés par le contenu de l'artefact.
@@ -60,6 +60,66 @@ Labels, CTA, onboarding, empty states, erreurs, notifications, formulaires ou te
60
60
  - **Utiliser le même terme pour la même chose.** Créer un mini-glossaire lorsque plusieurs écrans divergent.
61
61
  - **Ne pas cacher les conséquences.** Les actions irréversibles, paiements, permissions et publications doivent être explicites.
62
62
 
63
+ ## La voix du produit — la dimension qui ne s'active pas toute seule
64
+
65
+ Cette grille juge la copie contre des règles générales : clarté, erreurs utiles, terminologie. Elle gagne ici une seconde dimension, qui juge la copie contre **la voix propre du produit** — et celle-ci ne s'exprime qu'à une condition, non négociable.
66
+
67
+ > **Pas de charte confirmée, pas de jugement de voix.**
68
+
69
+ La charte vit sur la fiche du produit. Elle porte six choses : le tutoiement ou le vouvoiement, le registre, les mots maison, les mots bannis, la façon de dire les erreurs, et les emojis.
70
+
71
+ **Quand elle est absente**, la grille générique s'applique seule et le rapport le dit, une fois, sans insister : *« Déclare ta voix sur ta fiche, et je vérifierai que tes écrans la tiennent. »* Aucun constat de voix, aucune insinuation. Une voix qu'on n'a pas déclarée n'est pas une voix qu'on tient mal.
72
+
73
+ **Quand elle est seulement PROPOSÉE** — déduite d'un échantillon d'écrans, pas encore confirmée — elle ne juge rien non plus. Une proposition se montre, elle ne s'applique pas.
74
+
75
+ ### LE GARDE-FOU, et c'est celui qui compte le plus ici
76
+
77
+ > **La charte est celle de l'utilisateur, jamais la tienne.**
78
+
79
+ Un produit qui choisit le vouvoiement, un registre corporate et des emojis partout **a le droit**. Tu vérifies la TENUE, pas le goût. Un constat qui reprocherait le vouvoiement à un produit dont la charte dit « vous » n'est pas un constat sévère : c'est un contresens, et c'est la faute la plus grave possible sur cette dimension.
80
+
81
+ Corollaire : tu ne proposes jamais de changer la charte. Si elle te paraît étrange, elle appartient quand même à quelqu'un qui l'a écrite.
82
+
83
+ ### Ce qu'un constat de voix doit porter
84
+
85
+ Deux choses ensemble, sans quoi il ne s'écrit pas : **la règle de la charte violée**, et **la phrase exacte** qui la viole, avec sa localisation.
86
+
87
+ | Règle violée | Constat |
88
+ |---|---|
89
+ | Adresse | « Ta charte tutoie ; cet écran vouvoie — *"Veuillez réessayer"*, écran de connexion. » |
90
+ | Mots bannis | « *"Utilisateur"* est dans tes mots bannis ; il apparaît 3 fois — page Compte, page Équipe, message d'erreur du formulaire. » |
91
+ | Mots maison | « Tu appelles ça un *dossier* ; cet écran dit *"projet"* — tableau de bord, en-tête. » |
92
+ | Ton des erreurs | « Ta charte dit rassurer ; ce message culpabilise — *"Vous avez fait une erreur"*, formulaire d'inscription. » |
93
+ | Emojis | « Ta charte les exclut ; il y en a deux — bandeau de bienvenue. » |
94
+
95
+ Une occurrence sans localisation n'est pas citable, donc pas un constat. Un compte sans les endroits qui le produisent non plus.
96
+
97
+ ### La cohérence de voix, même sans règle violée
98
+
99
+ Deux registres qui coexistent dans un même produit sont un constat **même si la charte n'interdit ni l'un ni l'autre**. Un accueil chaleureux et des messages d'erreur techniques ne se contredisent pas ligne à ligne : ils se contredisent d'un écran à l'autre, et c'est ce que personne ne voit en relisant un écran à la fois.
100
+
101
+ Ce constat cite au moins **deux endroits**, un de chaque registre. Sans les deux, il n'y a rien à comparer.
102
+
103
+ ### LA FRONTIÈRE AVEC `better-consistency`, à ne pas franchir
104
+
105
+ Les deux juges regardent des mots, et c'est là que la confusion serait facile.
106
+
107
+ - **`better-consistency` juge les TERMES et les COMPOSANTS** : le même objet nommé « projet » ici et « espace » là, deux boutons pour la même action, deux formats de date. C'est une question d'identité — deux choses désignées de deux façons.
108
+ - **`better-writing` juge le TON et la VOIX** : comment on s'adresse, sur quel registre, avec quelle chaleur dans l'échec. C'est une question de manière — la même chose dite de deux façons.
109
+
110
+ Un même écran peut relever des deux, et ce n'est pas un chevauchement : « projet » vs « espace » est un problème de terme ; « Veuillez patienter » vs « Une seconde ! » est un problème de ton. Si tu hésites, demande-toi ce qui changerait en corrigeant : un mot remplacé partout, ou une phrase réécrite.
111
+
112
+ ### Sévérité sur cette dimension
113
+
114
+ | Sévérité | Sur la voix |
115
+ |---|---|
116
+ | **Critique** | La voix se contredit sur le parcours principal — accueil et paiement ne parlent pas au même public. Un mot banni apparaît dans un message que tout le monde voit. |
117
+ | **Majeur** | L'adresse change d'un écran à l'autre. Le ton des erreurs contredit la charte là où l'échec est fréquent. Un mot maison remplacé par un synonyme dans un écran central. |
118
+ | **Mineur** | Une violation isolée sur un écran secondaire — mentions légales, page d'aide rarement ouverte. |
119
+ | **Note** | Une formulation qui tient la charte mais s'en éloigne, sans que rien ne soit faux. |
120
+
121
+ La règle de la dernière hésitation s'applique : entre deux niveaux, prends le plus bas et écris l'hésitation dans la confiance.
122
+
63
123
  ## Patrons de microcopy
64
124
 
65
125
  | Situation | Structure recommandée | À éviter |
@@ -87,5 +87,120 @@
87
87
  "should_refuse": true,
88
88
  "note": "Sans emplacement ni action attendue, réécrire est deviner. Le skill doit demander le contexte d'affichage."
89
89
  }
90
+ },
91
+ {
92
+ "id": "writing-conformant-voix-corporate-tenue",
93
+ "skill": "better-writing",
94
+ "kind": "conformant",
95
+ "delivery_mode": "classique",
96
+ "artifact": "CHARTE DE VOIX DÉCLARÉE PAR LE PRODUIT (confirmée sur la fiche)\n- Adresse : le produit dit « vous ». Toute phrase qui emploie l'autre forme est une violation.\n- Registre : technique — on nomme les choses précisément. Un écran qui bascule dans un autre registre est une violation, même si la phrase est bonne.\n- Messages d'erreur : on dit ce qui s'est passé, puis la suite, sans détour. Un message qui culpabilise est une violation dans les deux cas.\n- Emojis : autorisés.\n- Mots maison — c'est ainsi que CE produit nomme ses objets : dossier, collaborateur, mandat. Employer un synonyme à la place est une violation.\n- Mots bannis, jamais à l'écran : utilisateur, plateforme. Chaque occurrence est une violation, avec sa localisation.\n\nÉCRANS RELEVÉS (lecture complète du HTML servi, 6 pages)\n\n/connexion\n Titre : « Accès à votre espace de travail »\n Champ : « Adresse e-mail professionnelle »\n Bouton : « Se connecter »\n Erreur : « Ce mot de passe ne correspond pas à cette adresse. Réinitialisez-le ou contactez votre administrateur. »\n\n/dossiers\n Titre : « Vos dossiers »\n Vide : « Aucun dossier pour le moment. Créez-en un pour commencer à travailler. »\n Bouton : « Nouveau dossier »\n\n/dossiers/equipe\n Titre : « Collaborateurs du dossier »\n Sous-titre : « Les personnes autorisées à consulter et modifier ce dossier. »\n Bouton : « Inviter un collaborateur »\n\n/mandats\n Titre : « Mandats en cours »\n Colonne : « Échéance » · « Responsable » · « Statut »\n Vide : « Aucun mandat actif. Les mandats clôturés restent consultables dans les archives. »\n\n/facturation\n Titre : « Facturation du dossier »\n Erreur : « Le paiement a été refusé par votre banque. Vérifiez les informations de votre carte, puis réessayez. »\n\n/aide\n Titre : « Centre d'aide »\n Texte : « Consultez la documentation ou écrivez à notre équipe support. » 📄",
97
+ "golden": {
98
+ "expected_findings": [],
99
+ "forbidden_findings": [
100
+ "le vouvoiement met de la distance avec la personne qui lit",
101
+ "conseiller de tutoyer pour un ton plus proche et plus moderne",
102
+ "le registre technique rend les écrans froids et peu accueillants",
103
+ "un emoji dans le centre d'aide détonne avec le reste du produit",
104
+ "les messages d'erreur manquent de chaleur et de réassurance"
105
+ ],
106
+ "expected_verdict": "PASS",
107
+ "should_refuse": false,
108
+ "note": "LE PIÈGE DU GOÛT, et c'est le cas le plus important de cette extension. Le produit a déclaré une voix corporate — vouvoiement, registre technique, erreurs directes, emojis autorisés — et il la tient partout : six écrans, aucune adresse qui glisse, aucun mot banni, les trois mots maison employés exactement là où ils doivent l'être. L'emoji du centre d'aide est AUTORISÉ par la charte. Un juge qui préférerait le tutoiement et le ferait savoir commettrait un contresens, pas une sévérité : il remplacerait le projet de quelqu'un par le sien. Les cinq interdits sont exactement les cinq façons de céder à ce réflexe. La bonne réponse est PASS et le silence."
109
+ }
110
+ },
111
+ {
112
+ "id": "writing-obvious-voix-adresse-qui-glisse",
113
+ "skill": "better-writing",
114
+ "kind": "obvious-defect",
115
+ "delivery_mode": "ai-builder",
116
+ "artifact": "CHARTE DE VOIX DÉCLARÉE PAR LE PRODUIT (confirmée sur la fiche)\n- Adresse : le produit dit « tu ». Toute phrase qui emploie l'autre forme est une violation.\n- Registre : chaleureux — on est à côté de la personne. Un écran qui bascule dans un autre registre est une violation, même si la phrase est bonne.\n- Messages d'erreur : on rassure avant de dire quoi faire. Un message qui culpabilise est une violation dans les deux cas.\n- Emojis : jamais.\n- Mots maison : carnet, note.\n- Mots bannis, jamais à l'écran : utilisateur, contenu.\n\nÉCRANS RELEVÉS (lecture complète du HTML servi, 5 pages)\n\n/accueil\n Titre : « Bienvenue dans ton carnet »\n Texte : « Retrouve ici toutes tes notes, où que tu sois. »\n Bouton : « Ouvrir mon carnet »\n\n/notes/nouvelle\n Titre : « Veuillez saisir le titre de votre note »\n Champ : « Titre »\n Aide : « Vous pourrez le modifier ultérieurement. »\n Bouton : « Enregistrer »\n\n/parametres\n Titre : « Vos préférences »\n Texte : « Veuillez sélectionner la langue d'affichage de votre interface. »\n Bouton : « Valider »\n\n/partage\n Titre : « Partager ta note »\n Texte : « Choisis avec qui tu partages ce contenu. »\n Bouton : « Créer le lien »\n\n/erreur-500\n Titre : « Une erreur est survenue »\n Texte : « Vous avez effectué une action non autorisée. Veuillez réessayer plus tard. »",
117
+ "golden": {
118
+ "expected_findings": [
119
+ "la charte tutoie mais trois écrans vouvoient",
120
+ "l'écran de nouvelle note dit vous alors que l'accueil dit tu",
121
+ "les préférences vouvoient d'un bout à l'autre",
122
+ "le message d'erreur vouvoie et rejette la faute sur la personne",
123
+ "le mot contenu est banni par la charte et apparaît sur l'écran de partage"
124
+ ],
125
+ "forbidden_findings": [
126
+ "conseiller de choisir le vouvoiement pour l'ensemble du produit",
127
+ "le tutoiement n'est pas adapté à un produit professionnel",
128
+ "les titres des écrans ne suivent pas la même hiérarchie visuelle",
129
+ "revoir la mise en page des écrans de paramètres",
130
+ "le terme note est employé de deux façons différentes"
131
+ ],
132
+ "expected_verdict": "FAIL",
133
+ "should_refuse": false,
134
+ "note": "La violation la plus visible, et la plus fréquente en pratique : un produit généré où la moitié des écrans a gardé le vouvoiement par défaut. Trois écrans sur cinq, dont un message d'erreur qui cumule deux violations — le vouvoiement ET la culpabilisation, que la charte refuse dans les deux tons. « contenu » est dans les mots bannis. Les interdits couvrent le contresens du goût (conseiller l'inverse de la charte), l'avis sur le choix lui-même, et deux dérives hors axe — la mise en page, et un constat de TERME qui appartient à better-consistency."
135
+ }
136
+ },
137
+ {
138
+ "id": "writing-subtle-voix-qui-lache-sous-stress",
139
+ "skill": "better-writing",
140
+ "kind": "subtle-defect",
141
+ "delivery_mode": "hybride",
142
+ "artifact": "CHARTE DE VOIX DÉCLARÉE PAR LE PRODUIT (confirmée sur la fiche)\n- Adresse : le produit dit « tu ». Toute phrase qui emploie l'autre forme est une violation.\n- Registre : chaleureux — on est à côté de la personne. Un écran qui bascule dans un autre registre est une violation, même si la phrase est bonne.\n- Messages d'erreur : on rassure avant de dire quoi faire. Un message qui culpabilise est une violation dans les deux cas.\n- Emojis : jamais.\n- Mots maison : carnet, note.\n- Mots bannis, jamais à l'écran : utilisateur, contenu.\n\nÉCRANS RELEVÉS (lecture complète du HTML servi, 7 pages)\n\n/accueil\n Titre : « Ton carnet t'attend »\n Texte : « Reprends là où tu t'es arrêté. »\n\n/notes\n Titre : « Tes notes »\n Vide : « Rien ici pour l'instant. Crée ta première note, ça prend dix secondes. »\n\n/notes/nouvelle\n Titre : « Nouvelle note »\n Aide : « Donne-lui un titre, tu pourras le changer après. »\n\n/partage\n Titre : « Partage ta note »\n Texte : « Choisis qui peut la lire. Tu peux retirer l'accès quand tu veux. »\n\n/parametres\n Titre : « Tes préférences »\n Texte : « Choisis ta langue et le thème qui te va. »\n\nMESSAGES D'ERREUR (relevés dans le même HTML)\n Formulaire de note : « Échec de la validation du formulaire : le champ titre est requis. »\n Import : « Erreur 422 — le fichier fourni ne satisfait pas au format attendu. »\n Partage : « Opération impossible : quota de liens de partage atteint pour la période courante. »\n Connexion : « Authentification échouée. »",
143
+ "golden": {
144
+ "expected_findings": [
145
+ "la voix tient partout sauf dans les messages d'erreur",
146
+ "les messages d'échec basculent dans un registre technique que la charte n'emploie nulle part ailleurs",
147
+ "aucun message d'erreur ne rassure ni ne dit quoi faire ensuite"
148
+ ],
149
+ "forbidden_findings": [
150
+ "le ton chaleureux du produit manque de sérieux",
151
+ "conseiller un registre plus neutre sur l'ensemble des écrans",
152
+ "les écrans principaux emploient un tutoiement inconstant",
153
+ "revoir la hiérarchie des titres sur les écrans de note",
154
+ "le mot note désigne deux objets différents selon les écrans"
155
+ ],
156
+ "expected_verdict": "PASS WITH FIXES",
157
+ "should_refuse": false,
158
+ "note": "LE REGISTRE QUI GLISSE SOUS STRESS. Cinq écrans impeccables — tutoiement, chaleur, phrases courtes, aucun mot banni. Un juge qui relit écran par écran coche tout et s'arrête. La bascule ne se voit qu'en rassemblant les quatre messages d'erreur : « Échec de la validation », « Erreur 422 », « quota atteint pour la période courante », « Authentification échouée » — un autre produit parle. C'est le cas typique où le code d'erreur remonte tel quel du serveur et où personne ne l'a jamais relu à côté du reste. Aucune règle précise de la charte n'est violée mot à mot sur trois de ces messages : c'est la COHÉRENCE de voix qui l'est, et elle se constate en citant les deux registres. Le quatrième viole en plus le ton d'erreur déclaré."
159
+ }
160
+ },
161
+ {
162
+ "id": "writing-out-of-scope-voix-et-mise-en-page",
163
+ "skill": "better-writing",
164
+ "kind": "out-of-scope",
165
+ "delivery_mode": "classique",
166
+ "artifact": "CHARTE DE VOIX DÉCLARÉE PAR LE PRODUIT (confirmée sur la fiche)\n- Adresse : le produit dit « tu ». Toute phrase qui emploie l'autre forme est une violation.\n- Registre : chaleureux — on est à côté de la personne. Un écran qui bascule dans un autre registre est une violation, même si la phrase est bonne.\n- Messages d'erreur : on rassure avant de dire quoi faire. Un message qui culpabilise est une violation dans les deux cas.\n- Emojis : jamais.\n- Mots maison : carnet, note.\n- Mots bannis, jamais à l'écran : utilisateur, contenu.\n\nÉCRANS RELEVÉS (lecture complète du HTML servi, 3 pages)\n\n/accueil\n Titre : « Ton carnet »\n Texte : « Tout ce que tu écris, au même endroit. »\n Bouton : « Ouvrir »\n\n/notes\n Titre : « Tes notes »\n Vide : « Rien pour l'instant. Crée ta première note quand tu veux. »\n\n/note/123\n Titre : « Réunion du 12 mars »\n Texte : « Modifié il y a deux heures. »\n Bouton : « Enregistrer »\n\nCE QUE LA LECTURE A AUSSI RELEVÉ (hors du texte)\n - Les trois écrans emploient quatre tailles de titre différentes : 32, 28, 26 et 24 pixels.\n - Le bouton principal est aligné à gauche sur /accueil et à droite sur /note/123.\n - Les marges entre les blocs varient de 16 à 40 pixels sans logique apparente.\n - Deux composants de bouton coexistent dans le HTML, avec des classes différentes.",
167
+ "golden": {
168
+ "expected_findings": [],
169
+ "forbidden_findings": [
170
+ "quatre tailles de titre coexistent sans échelle typographique",
171
+ "le bouton principal change de côté d'un écran à l'autre",
172
+ "les marges entre les blocs varient sans logique apparente",
173
+ "deux composants de bouton coexistent dans le produit",
174
+ "conseiller de revoir la mise en page avant de travailler les textes"
175
+ ],
176
+ "expected_verdict": "PASS",
177
+ "should_refuse": false,
178
+ "note": "TOUT CE QUI EST VRAI N'EST PAS DE CE JUGE. Les quatre observations hors texte sont exactes et importantes — et pas une seule ne relève de la copie ni de la voix. Les tailles de titre et les marges sont à better-layout ; les deux composants de bouton sont à better-components, ou à better-consistency si c'est leur libellé qui diverge. La voix, elle, tient : trois écrans, tutoiement partout, chaleur constante, aucun mot banni. La bonne réponse est PASS. Le cinquième interdit est le plus tentant de tous — il ne rapporte pas le défaut, il conseille de s'en occuper d'abord, ce qui revient à sortir de son axe par la porte du conseil."
179
+ }
180
+ },
181
+ {
182
+ "id": "writing-obvious-sans-charte-la-voix-se-tait",
183
+ "skill": "better-writing",
184
+ "kind": "obvious-defect",
185
+ "delivery_mode": "ai-builder",
186
+ "artifact": "AUCUNE CHARTE DE VOIX N'EST DÉCLARÉE POUR CE PRODUIT.\n\nLa fiche ne porte pas de voix confirmée. Une voix a été DÉDUITE d'un échantillon d'écrans et proposée, mais elle n'a pas été validée : elle ne fait donc pas foi.\n\nVOIX DÉDUITE, NON CONFIRMÉE (pour information seulement)\n- Adresse dominante observée : « vous » (7 écrans sur 9)\n- Registre dominant : sobre\n\nÉCRANS RELEVÉS (lecture complète du HTML servi, 4 pages)\n\n/connexion\n Titre : « Connexion »\n Champ : « E-mail »\n Bouton : « Envoyer »\n Erreur : « Erreur »\n\n/inscription\n Titre : « Créer un compte »\n Champ : « E-mail » · « Mot de passe »\n Aide : (aucune)\n Bouton : « Soumettre »\n\n/tableau-de-bord\n Titre : « Tableau de bord »\n Vide : (aucun texte)\n\n/parametres\n Titre : « Paramètres »\n Bouton : « Valider »",
187
+ "golden": {
188
+ "expected_findings": [
189
+ "le message d'erreur de la connexion dit seulement erreur, sans ce qui s'est passé ni quoi faire",
190
+ "le bouton soumettre ne dit pas ce qui va se passer",
191
+ "le champ mot de passe n'indique aucune contrainte de format",
192
+ "le tableau de bord vide n'explique pas ce qui apparaîtra"
193
+ ],
194
+ "forbidden_findings": [
195
+ "le produit vouvoie sur sept écrans et tutoie sur les deux autres",
196
+ "la voix déduite montre un registre sobre mal tenu",
197
+ "l'adresse employée manque de constance dans l'ensemble du produit",
198
+ "conseiller de choisir entre le tutoiement et le vouvoiement",
199
+ "le registre du produit ne correspond pas à son public"
200
+ ],
201
+ "expected_verdict": "PASS WITH FIXES",
202
+ "should_refuse": false,
203
+ "note": "LA DIMENSION VOIX SE TAIT, LA GRILLE GÉNÉRIQUE PARLE. Ce cas n'est PAS un `insufficient` — le contrôleur de goldens me l'a fait remarquer, et il avait raison : le skill ne refuse pas, il répond. C'est UNE DIMENSION qui se tait, pendant que l'autre travaille normalement, et le vocabulaire du banc n'a pas de nom pour ça. Le kind teste donc la détection générique ; ce sont les phrases interdites qui testent le silence de la voix.\n\nUne voix a été déduite et proposée — elle n'a pas été confirmée, donc elle ne juge rien. Le piège est double et les deux moitiés sont tentantes : employer la voix déduite comme si elle faisait foi (les cinq interdits le font, sous cinq formes), ou se taire complètement au motif qu'il n'y a pas de charte. Ni l'un ni l'autre. Les quatre constats attendus relèvent tous de la grille générique — un message d'erreur qui dit « Erreur », un bouton « Soumettre », un champ sans contrainte annoncée, un écran vide muet — et aucun ne suppose de savoir comment ce produit veut parler. Le rapport invite à déclarer la voix ; il ne la devine pas."
204
+ }
90
205
  }
91
206
  ]
@@ -219,6 +219,9 @@ Lot 4 Accélérer
219
219
  - Axes non vérifiés, nommés
220
220
  ```
221
221
 
222
+
223
+ Ces constats s'exportent en tickets ou en message d'équipe — `export_findings`. Rien n'est envoyé nulle part : l'outil rend un texte, et c'est l'assistant de la personne qui le pose dans SES outils, avec SES accès.
224
+
222
225
  ## Critères de qualité du skill
223
226
 
224
227
  - Le verdict sépare clairement prototype, V0, production et industrialisation.
@@ -40,6 +40,8 @@ Sans cette distinction, le skill condamne tout ce qu'on lui montre, et devient i
40
40
  Deux corollaires, tout aussi fermes :
41
41
 
42
42
  - **Il ne juge jamais la valeur de l'idée.** « Cette feature n'apporte pas grand-chose » n'est pas un constat de cadrage : c'est une opinion, et personne ne l'a demandée. Une feature modeste peut être parfaitement cadrée.
43
+
44
+ La formule qui tient cette ligne, et qu'il faut employer telle quelle : le skill ne dit pas *« cette feature est bonne »* ou *« mauvaise »*. Il dit **« ton dossier est solide »**, ou **« ton dossier repose sur N croyances non vérifiées, les voici »** — et il cite chacune, mot pour mot, en preuve. Une croyance nommée est discutable ; un jugement de valeur ne l'est pas.
43
45
  - **Il ne propose pas la feature à la place de celui qui l'écrit.** Un constat dit ce qui manque pour décider. Il ne réécrit pas l'objectif.
44
46
 
45
47
  ## Déclenchement
@@ -55,6 +57,9 @@ Avant d'engager du travail sur une feature, quand une décision se prend « au f
55
57
  | Invariant | Ce qu'il faut prouver | Ce qui le prouve |
56
58
  |---|---|---|
57
59
  | **Problème testable** | Le problème est formulé de façon **falsifiable** : on peut imaginer une observation qui le contredirait. | Une phrase qui dit qui bute sur quoi, où, et à quelle fréquence. Pas une intention (« améliorer l'onboarding »), pas une solution déguisée en problème (« il manque un tutoriel »). |
60
+ | **Preuve du problème** | Le problème s'appuie sur quelque chose d'**observé**, pas sur une conviction. | Des retours réels, des données, des tickets, des entretiens — avec leur nombre et leur période. « Je pense que », « on sent que », « c'est évident » ne sont pas des preuves, et le skill le dit en citant la phrase. |
61
+ | **Pour qui, précisément** | La population concernée est **nommable et bornée**. | « Les comptes créés depuis moins de sept jours », « les équipes de plus de dix personnes ». « Les utilisateurs » n'est pas une réponse recevable : elle rend le problème invérifiable et le critère de succès incalculable. |
62
+ | **Hypothèse la plus risquée** | Celle qui **tue la feature si elle est fausse** est identifiée. | Une croyance nommée, et ce qui la vérifierait. Un dossier qui n'en a aucune n'est pas sans risque : il n'a pas cherché. |
58
63
  | **Critère de succès observable** | Ce qui compterait comme une réussite est **écrit, chiffré et daté**, avant de commencer. | « X passe de A à B d'ici telle date ». Un critère posé après coup n'en est pas un : il se moule sur le résultat obtenu. |
59
64
  | **Parcours complet** | Le chemin va du **déclencheur** à la **post-condition**, erreurs comprises. | Ce qui déclenche, ce que la personne fait, ce qu'elle voit, ce qui se passe quand ça échoue, et dans quel état on la laisse. |
60
65
  | **On saura après coup** | Les **événements à mesurer** existent, et ils mesurent le critère de succès — pas la présence. | Le nom des événements, ce qu'ils portent, et le lien explicite avec le critère. Compter les ouvertures d'un écran ne dit pas si la tâche a réussi. |
@@ -65,11 +70,12 @@ Avant d'engager du travail sur une feature, quand une décision se prend « au f
65
70
 
66
71
  1. **Lire le dossier en entier** avant de juger quoi que ce soit. Un critère de succès se trouve parfois trois paragraphes plus loin, formulé autrement.
67
72
  2. **Repérer le problème** et le tester : *quelle observation le contredirait ?* Si aucune, c'est une intention.
68
- 3. **Chercher le critère de succès**, puis vérifier qu'il est **antérieur** à la décision et non calqué sur ce qu'on espère.
69
- 4. **Dérouler le parcours** de bout en bout, et s'arrêter à chaque endroit où le dossier ne dit pas ce qui se passe.
70
- 5. **Confronter la mesure au critère** : chaque critère a-t-il de quoi être observé ? Un critère sans mesure est le défaut le plus fréquent, et le plus discret.
71
- 6. **Chercher l'irréversible**, y compris ce qui n'est pas technique : une promesse publique ne se retire pas plus qu'une migration.
72
- 7. **Écrire le périmètre** : ce qui a été lu, et ce qui n'a pas été fourni. Sans cette ligne, aucune conclusion négative n'est recevable.
73
+ 3. **Chercher sur quoi il s'appuie** : combien de retours, sur quelle période, auprès de qui. Une conviction se cite mot pour mot et devient une croyance à vérifier, pas un constat sur l'idée.
74
+ 4. **Chercher le critère de succès**, puis vérifier qu'il est **antérieur** à la décision et non calqué sur ce qu'on espère.
75
+ 5. **Dérouler le parcours** de bout en bout, et s'arrêter à chaque endroit où le dossier ne dit pas ce qui se passe.
76
+ 6. **Confronter la mesure au critère** : chaque critère a-t-il de quoi être observé ? Un critère sans mesure est le défaut le plus fréquent, et le plus discret.
77
+ 7. **Chercher l'irréversible**, y compris ce qui n'est pas technique : une promesse publique ne se retire pas plus qu'une migration.
78
+ 8. **Écrire le périmètre** : ce qui a été lu, et ce qui n'a pas été fourni. Sans cette ligne, aucune conclusion négative n'est recevable.
73
79
 
74
80
  ## La règle de l'absence
75
81
 
@@ -91,7 +97,8 @@ Le verdict se **déduit** des sévérités, il ne se décide pas.
91
97
  |---|---|
92
98
  | **Bloquant** | Un risque irréversible non nommé — migration destructive, promesse publique, changement de prix. On engage quelque chose qu'on ne pourra pas défaire, sans que personne l'ait écrit. |
93
99
  | **Critique** | Aucun critère de succès, ou un critère inobservable. On ne saura jamais si on a eu raison, et la décision suivante se prendra sur la même absence. |
94
- | **Majeur** | Un critère existe mais rien ne le mesure. Le problème est formulé comme une intention. Le parcours s'arrête avant les erreurs. |
100
+ | **Majeur** | Un critère existe mais rien ne le mesure. Le problème est formulé comme une intention. Le parcours s'arrête avant les erreurs. Le problème ne s'appuie sur aucune observation — seulement sur une conviction. La population visée n'est pas bornée : « les utilisateurs ». |
101
+ | **Majeur (bis)** | Aucune hypothèse risquée n'est identifiée. Ce n'est pas l'absence de risque, c'est l'absence de recherche — et elle se corrige en une conversation, d'où majeur et non critique. |
95
102
  | **Mineur** | Le coût d'annulation est évoqué sans être chiffré. Un événement de mesure est nommé sans ce qu'il porte. |
96
103
  | **Note** | Une inconnue assumée, avec sa condition de réévaluation. |
97
104
 
@@ -137,6 +144,10 @@ Dans ce cas, la sortie contient :
137
144
  - Confondre un problème et l'absence d'une solution.
138
145
  - Réécrire l'objectif de la feature à la place de celui qui l'a écrit.
139
146
  - Condamner une feature modeste pour sa modestie.
147
+ - Dire qu'une feature est bonne ou mauvaise, au lieu de compter les croyances sur lesquelles elle repose.
148
+ - Accepter « je pense que » ou « c'est évident » comme preuve d'un problème.
149
+ - Accepter « les utilisateurs » comme réponse à « pour qui ».
150
+ - Nommer une hypothèse risquée à la place de celui qui écrit le dossier.
140
151
  - Cartographier ce que la feature touche dans le code : ce n'est pas cet axe.
141
152
 
142
153
  ## Contrat de sortie exact
@@ -149,9 +160,10 @@ La sortie contient, dans cet ordre :
149
160
 
150
161
  1. **Le périmètre lu** — ce qui a été reçu, ce qui ne l'a pas été.
151
162
  2. **La grille**, invariant par invariant : établi, manquant, ou hors périmètre.
152
- 3. **Les constats**, au schéma commun, triés par sévérité.
153
- 4. **Le verdict**, déduit des sévérités.
154
- 5. **Les questions ouvertes** — ce qui reste à savoir, formulé comme des questions qu'on peut poser à quelqu'un.
163
+ 3. **Les croyances non vérifiées**, s'il y en a — chacune citée mot pour mot depuis le dossier, avec ce qui la vérifierait. C'est la seule forme sous laquelle ce skill parle de la valeur d'une idée : il compte ce sur quoi elle repose sans preuve, et laisse quelqu'un d'autre décider.
164
+ 4. **Les constats**, au schéma commun, triés par sévérité.
165
+ 5. **Le verdict**, déduit des sévérités.
166
+ 6. **Les questions ouvertes** — ce qui reste à savoir, formulé comme des questions qu'on peut poser à quelqu'un.
155
167
 
156
168
  ## Critères de qualité du skill
157
169
 
@@ -87,6 +87,31 @@
87
87
  "note": "LE DÉFAUT NE SE VOIT QU'EN CROISANT DEUX SECTIONS. Le dossier est excellent partout : problème chiffré et instruit, critère antérieur, daté, observable, parcours complet jusqu'aux échecs, rien d'irréversible, retour arrière établi. Et le plan de mesure est riche — cinq événements, ce qui donne l'impression d'un dossier instrumenté. Aucun ne permet de calculer un ABANDON : il faudrait savoir qui quitte l'étape sans la terminer, et rien ne marque la sortie. Un juge qui vérifie « y a-t-il un critère ? » puis « y a-t-il une mesure ? » séparément coche les deux cases et passe à côté. C'est pourquoi la séquence impose de les confronter. Le constat est unique, majeur, et ne remet pas en cause la décision : PASS WITH FIXES."
88
88
  }
89
89
  },
90
+ {
91
+ "id": "cadre-subtle-dossier-de-croyances",
92
+ "skill": "review-feature-framing",
93
+ "kind": "subtle-defect",
94
+ "delivery_mode": "classique",
95
+ "artifact": "DOSSIER REÇU EN ENTIER — docs/features/mode-focus.md\n\nPROBLÈME\nLes utilisateurs sont distraits par l'interface quand ils rédigent un rapport.\nC'est évident dès qu'on regarde l'écran : il y a le rail à gauche, le fil\nd'activité à droite, et les notifications qui arrivent par-dessus. Je pense\nqu'on perd beaucoup de monde à cette étape.\n\nPOUR QUI\nLes utilisateurs qui rédigent des rapports.\n\nCE QU'ON ATTEND\nD'ici le 31 mars 2027 : le taux de rapports terminés une fois commencés passe\nde 54 % à 70 %.\n\nPARCOURS\nDéclencheur : la personne ouvre l'éditeur de rapport et active « Mode focus ».\nLe rail, le fil d'activité et les notifications disparaissent.\nElle rédige. Elle ressort par Échap ou par le bouton en haut à droite.\nSi elle quitte l'onglet, le mode reste actif à son retour.\nÉchec d'enregistrement : bandeau qui le dit, le texte reste à l'écran, elle peut\nréessayer sans rien perdre.\nPost-condition : rapport enregistré, mode désactivé.\n\nMESURE\nÉvénements déjà en place et suffisants :\n rapport_commence { rapport_id }\n rapport_termine { rapport_id }\n mode_focus_active { rapport_id }\nLe taux du critère se calcule sur les deux premiers, qui existent depuis un an.\n\nCE QU'ON NE POURRA PAS DÉFAIRE\nRien. Aucune migration, aucun engagement public, aucun changement de prix.\n\nSI ON REVIENT EN ARRIÈRE\nOn retire le bouton. Les personnes qui l'avaient activé retrouvent l'écran\nnormal. Rien à prévenir, rien à migrer.",
96
+ "golden": {
97
+ "expected_findings": [
98
+ "le problème repose sur une conviction et non sur quelque chose d'observé",
99
+ "la population concernée n'est pas bornée, on ne saura pas sur qui mesurer",
100
+ "aucune hypothèse risquée n'est identifiée dans le dossier"
101
+ ],
102
+ "forbidden_findings": [
103
+ "le mode focus ne réduira pas les abandons de rédaction",
104
+ "cacher le fil d'activité dégradera l'expérience des équipes",
105
+ "cette fonctionnalité n'a pas assez de valeur pour être construite",
106
+ "aucun critère de succès chiffré n'est posé dans ce dossier",
107
+ "les événements de mesure sont absents du plan",
108
+ "conseiller de mener des entretiens avant de continuer"
109
+ ],
110
+ "expected_verdict": "PASS WITH FIXES",
111
+ "should_refuse": false,
112
+ "note": "LE DOSSIER QUI A L'AIR SOLIDE ET REPOSE SUR DES CROYANCES. Quatre invariants sur sept sont impeccables : critère chiffré, daté, antérieur ; parcours complet jusqu'aux échecs ; mesure déjà en place et qui porte le critère ; rien d'irréversible et retour arrière établi. Un juge qui déroule la grille dans l'ordre coche tout et s'arrête. Ce qui manque est en tête du dossier et se lit sans effort une fois qu'on le cherche : « c'est évident », « je pense qu'on perd beaucoup de monde » — deux convictions, zéro observation ; « les utilisateurs qui rédigent des rapports » — une population non bornée, sur laquelle le taux de 54 % ne peut pas se calculer honnêtement ; et aucune hypothèse risquée, alors que la feature entière repose sur celle-ci : que la distraction soit bien la cause des abandons. Le skill NOMME les croyances en les citant, il ne dit pas que l'idée est mauvaise. Trois majeurs, aucun bloquant : PASS WITH FIXES. Les six interdits couvrent les deux dérives symétriques — prédire l'effet, juger la valeur — et les trois faux constats qu'un juge pressé produirait en confondant « problème mal étayé » avec « critère absent »."
113
+ }
114
+ },
90
115
  {
91
116
  "id": "cadre-out-of-scope-dette-et-code",
92
117
  "skill": "review-feature-framing",