designwithai-mcp 0.5.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,28 @@ 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
+ },
446
470
  {
447
471
  name: "framing_criteria",
448
472
  description: "Charge les CRITÈRES D'ACCEPTATION d'un cadrage écrit avant de construire, pour vérifier " +
@@ -644,7 +668,7 @@ function toolPlan(args) {
644
668
  }
645
669
  // ─── Reviews suivies (étape C) ───────────────────────────────────────────────
646
670
  function sevCounts(findings) {
647
- const order = ["bloquant", "critique", "majeur", "mineur", "note"];
671
+ const order = SEVERITES;
648
672
  const c = {};
649
673
  for (const f of findings) {
650
674
  const s = f.severity ?? "note";
@@ -1050,6 +1074,48 @@ function toolNextGate(args) {
1050
1074
  * dit explicitement quoi en faire — c'est `verify_fix` qui statue, un critère à
1051
1075
  * la fois, avec sa preuve.
1052
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
+ }
1053
1119
  async function toolFramingCriteria(args) {
1054
1120
  const token = process.env.DWAI_TOKEN?.trim();
1055
1121
  if (!token) {
@@ -1255,6 +1321,8 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
1255
1321
  return toolGenerateDecision(args);
1256
1322
  case "next_gate":
1257
1323
  return toolNextGate(args);
1324
+ case "export_findings":
1325
+ return toolExportFindings(args);
1258
1326
  case "framing_criteria":
1259
1327
  return toolFramingCriteria(args);
1260
1328
  case "inventory_ds":
@@ -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.5.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",
@@ -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.
@@ -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.