designwithai-mcp 0.1.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.
- package/README.md +153 -0
- package/dist/connectors.js +76 -0
- package/dist/decisions.js +67 -0
- package/dist/embeddings.js +98 -0
- package/dist/evals.js +172 -0
- package/dist/finding-identity.js +229 -0
- package/dist/handbook.js +98 -0
- package/dist/paths.js +62 -0
- package/dist/profile.js +79 -0
- package/dist/reviews.js +99 -0
- package/dist/server.js +1136 -0
- package/dist/skills.js +57 -0
- package/dist/state.js +48 -0
- package/handbook-embeddings.json +1 -0
- package/package.json +46 -0
- package/skills/EVALS.md +122 -0
- package/skills/README.md +59 -0
- package/skills/_shared/delivery-modes.md +12 -0
- package/skills/_shared/execution-contract.md +43 -0
- package/skills/_shared/issue-schema.md +25 -0
- package/skills/_shared/severity-model.md +16 -0
- package/skills/analyze-figma/evals/cases.json +140 -0
- package/skills/analyze-url/evals/cases.json +136 -0
- package/skills/better-accessibility/SKILL.md +141 -0
- package/skills/better-accessibility/evals/cases.json +83 -0
- package/skills/better-components/SKILL.md +125 -0
- package/skills/better-components/evals/cases.json +77 -0
- package/skills/better-consistency/SKILL.md +125 -0
- package/skills/better-consistency/evals/cases.json +77 -0
- package/skills/better-forms/SKILL.md +135 -0
- package/skills/better-forms/evals/cases.json +78 -0
- package/skills/better-interface/SKILL.md +143 -0
- package/skills/better-interface/evals/cases.json +80 -0
- package/skills/better-layout/SKILL.md +132 -0
- package/skills/better-layout/evals/cases.json +81 -0
- package/skills/better-navigation/SKILL.md +170 -0
- package/skills/better-navigation/evals/cases.json +131 -0
- package/skills/better-performance/SKILL.md +195 -0
- package/skills/better-performance/evals/cases.json +114 -0
- package/skills/better-responsive/SKILL.md +123 -0
- package/skills/better-responsive/evals/cases.json +77 -0
- package/skills/better-states/SKILL.md +129 -0
- package/skills/better-states/evals/cases.json +78 -0
- package/skills/better-writing/SKILL.md +138 -0
- package/skills/better-writing/evals/cases.json +77 -0
- package/skills/review-ai-product/SKILL.md +231 -0
- package/skills/review-ai-product/evals/cases.json +120 -0
- package/skills/review-data-security/SKILL.md +210 -0
- package/skills/review-data-security/evals/cases.json +138 -0
- package/skills/review-figma-hygiene/SKILL.md +186 -0
- package/skills/review-figma-hygiene/evals/cases.json +158 -0
package/dist/server.js
ADDED
|
@@ -0,0 +1,1136 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Serveur MCP du Handbook Design With AI.
|
|
4
|
+
*
|
|
5
|
+
* Expose la source de vérité éditoriale à Claude / un IDE, en RESOURCES (pour
|
|
6
|
+
* lire) et en TOOLS (pour agir). Principe directeur : les outils imposent la
|
|
7
|
+
* méthode du Handbook — ils ne distribuent pas des réponses. `review_artifact`
|
|
8
|
+
* exige une référence d'artefact ; `recommend_workflow` et `build_action_plan`
|
|
9
|
+
* renvoient des GATES à franchir, pas des solutions clé en main.
|
|
10
|
+
*/
|
|
11
|
+
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
import { CallToolRequestSchema, GetPromptRequestSchema, ListPromptsRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
|
|
14
|
+
import { chapterMatchesMode, excerpt, loadHandbook, scoreUnit, tokenize, } from "./handbook.js";
|
|
15
|
+
import { loadSkills } from "./skills.js";
|
|
16
|
+
import { loadProfile, profileSummary, resolveDeliveryMode, saveProfile, } from "./profile.js";
|
|
17
|
+
import { compareReviews, getReview, listReviews, saveReview, updateFindingStatus, } from "./reviews.js";
|
|
18
|
+
import { saveDecision } from "./decisions.js";
|
|
19
|
+
import { clearGate, loadState, reopenGate, saveState } from "./state.js";
|
|
20
|
+
import { readRepoSnapshot } from "./connectors.js";
|
|
21
|
+
import { trySemanticScores } from "./embeddings.js";
|
|
22
|
+
import { getCase, loadCases, saveScore, scoreCase, scorecard, } from "./evals.js";
|
|
23
|
+
const { handbook, origin } = await loadHandbook();
|
|
24
|
+
process.stderr.write(`[designwithai-mcp] ${handbook.units.length} unités chargées depuis ${origin}\n`);
|
|
25
|
+
const { skills, sharedContract, origin: skillsOrigin } = loadSkills();
|
|
26
|
+
process.stderr.write(`[designwithai-mcp] ${skills.length} skills chargés depuis ${skillsOrigin}\n`);
|
|
27
|
+
const skillByName = new Map(skills.map((s) => [s.name, s]));
|
|
28
|
+
let currentProfile = loadProfile().profile;
|
|
29
|
+
process.stderr.write(`[designwithai-mcp] profil : ${currentProfile ? profileSummary(currentProfile) || "(chargé)" : "aucun"}\n`);
|
|
30
|
+
/**
|
|
31
|
+
* Le mode effectif : l'argument explicite gagne, sinon on retombe sur le mode
|
|
32
|
+
* déclaré dans le profil. C'est ce qui contextualise les recos sans redemander.
|
|
33
|
+
*/
|
|
34
|
+
function effectiveMode(args) {
|
|
35
|
+
const explicit = args.delivery_mode ? resolveDeliveryMode(String(args.delivery_mode)) : undefined;
|
|
36
|
+
return explicit ?? resolveDeliveryMode(currentProfile?.deliveryMode);
|
|
37
|
+
}
|
|
38
|
+
const byUri = new Map(handbook.units.map((u) => [u.uri, u]));
|
|
39
|
+
const orderOf = new Map(handbook.units.map((u, i) => [u.uri, i]));
|
|
40
|
+
const chapterUnits = handbook.units.filter((u) => u.kind === "chapter");
|
|
41
|
+
const chapterById = new Map(chapterUnits.map((u) => [u.id, u]));
|
|
42
|
+
const REQUIRES = handbook.graph?.requires ?? {};
|
|
43
|
+
let currentState = loadState();
|
|
44
|
+
process.stderr.write(`[designwithai-mcp] gates franchis : ${Object.keys(currentState.gates).length}/${chapterUnits.length}\n`);
|
|
45
|
+
/** Résout un chapitre depuis un id (`volume/slug`), un slug, ou des mots-clés. */
|
|
46
|
+
function resolveChapter(query) {
|
|
47
|
+
const q = query.trim();
|
|
48
|
+
if (!q)
|
|
49
|
+
return null;
|
|
50
|
+
const exact = chapterUnits.find((u) => u.id === q || u.chapter === q || u.uri.endsWith(`/${q}`));
|
|
51
|
+
if (exact)
|
|
52
|
+
return exact;
|
|
53
|
+
const terms = tokenize(q);
|
|
54
|
+
const ranked = chapterUnits
|
|
55
|
+
.map((u) => ({ u, s: scoreUnit(u, terms) }))
|
|
56
|
+
.filter((x) => x.s > 0)
|
|
57
|
+
.sort((a, b) => b.s - a.s);
|
|
58
|
+
return ranked[0]?.u ?? null;
|
|
59
|
+
}
|
|
60
|
+
const DELIVERY_MODES = ["ai-builder", "classique", "hybride"];
|
|
61
|
+
const KINDS = [
|
|
62
|
+
"chapter",
|
|
63
|
+
"rule",
|
|
64
|
+
"anti-pattern",
|
|
65
|
+
"prompt",
|
|
66
|
+
"checklist",
|
|
67
|
+
"scorecard",
|
|
68
|
+
];
|
|
69
|
+
function cite(u) {
|
|
70
|
+
return `${u.source_url} (v${u.version})`;
|
|
71
|
+
}
|
|
72
|
+
function atomsOf(chapterId, kinds) {
|
|
73
|
+
return handbook.units.filter((u) => u.parent === chapterId && kinds.includes(u.kind));
|
|
74
|
+
}
|
|
75
|
+
/** Chapitres pertinents pour une requête, filtrés par mode, rendus en ordre curriculaire. */
|
|
76
|
+
function rankChapters(query, mode, limit) {
|
|
77
|
+
const terms = tokenize(query);
|
|
78
|
+
return handbook.units
|
|
79
|
+
.filter((u) => u.kind === "chapter" && chapterMatchesMode(u, mode))
|
|
80
|
+
.map((u) => ({ u, s: scoreUnit(u, terms) }))
|
|
81
|
+
.filter((x) => x.s > 0)
|
|
82
|
+
.sort((a, b) => b.s - a.s)
|
|
83
|
+
.slice(0, limit)
|
|
84
|
+
.map((x) => x.u)
|
|
85
|
+
.sort((a, b) => (orderOf.get(a.uri) ?? 0) - (orderOf.get(b.uri) ?? 0));
|
|
86
|
+
}
|
|
87
|
+
function text(s) {
|
|
88
|
+
return { content: [{ type: "text", text: s }] };
|
|
89
|
+
}
|
|
90
|
+
function refusal(s) {
|
|
91
|
+
return { content: [{ type: "text", text: s }], isError: true };
|
|
92
|
+
}
|
|
93
|
+
// ─── Définitions des outils ──────────────────────────────────────────────────
|
|
94
|
+
const deliveryModeProp = {
|
|
95
|
+
type: "string",
|
|
96
|
+
enum: DELIVERY_MODES,
|
|
97
|
+
description: "Mode de construction déclaré. Filtre comme le fait le site.",
|
|
98
|
+
};
|
|
99
|
+
const TOOLS = [
|
|
100
|
+
{
|
|
101
|
+
name: "search_handbook",
|
|
102
|
+
description: "Cherche dans le Handbook (chapitres, règles, prompts, checklists, scorecards). " +
|
|
103
|
+
"Chaque résultat cite sa source et sa version — à réutiliser dans la réponse.",
|
|
104
|
+
inputSchema: {
|
|
105
|
+
type: "object",
|
|
106
|
+
properties: {
|
|
107
|
+
query: { type: "string", description: "Ce que tu cherches." },
|
|
108
|
+
kind: { type: "string", enum: KINDS, description: "Restreindre à un type d'unité." },
|
|
109
|
+
book: { type: "integer", enum: [1, 2], description: "1 = Product Design, 2 = Design System." },
|
|
110
|
+
delivery_mode: deliveryModeProp,
|
|
111
|
+
limit: { type: "integer", description: "Nombre de résultats (défaut 8).", default: 8 },
|
|
112
|
+
},
|
|
113
|
+
required: ["query"],
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
name: "recommend_workflow",
|
|
118
|
+
description: "Pour un objectif, renvoie une SÉQUENCE DE GATES à franchir dans l'ordre — pas une " +
|
|
119
|
+
"solution. Chaque gate exige un artefact réel et un critère de sortie observable.",
|
|
120
|
+
inputSchema: {
|
|
121
|
+
type: "object",
|
|
122
|
+
properties: {
|
|
123
|
+
objective: { type: "string", description: "Ce que l'utilisateur veut accomplir." },
|
|
124
|
+
delivery_mode: deliveryModeProp,
|
|
125
|
+
},
|
|
126
|
+
required: ["objective"],
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
name: "review_artifact",
|
|
131
|
+
description: "Renvoie la GRILLE du Handbook (critères de gate, règles) à confronter à un artefact. " +
|
|
132
|
+
"Exige une référence d'artefact localisable — sans elle, refuse. Ne juge pas à ta place.",
|
|
133
|
+
inputSchema: {
|
|
134
|
+
type: "object",
|
|
135
|
+
properties: {
|
|
136
|
+
artifact_ref: {
|
|
137
|
+
type: "string",
|
|
138
|
+
description: "Où se trouve l'artefact : lien Figma, chemin de fichier, URL.",
|
|
139
|
+
},
|
|
140
|
+
topic: { type: "string", description: "Le sujet à cibler (ex. « design system », « prompt »)." },
|
|
141
|
+
delivery_mode: deliveryModeProp,
|
|
142
|
+
},
|
|
143
|
+
required: ["artifact_ref"],
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
name: "build_action_plan",
|
|
148
|
+
description: "Pour une situation ou une difficulté, renvoie un plan de GATES priorisés, chacun avec " +
|
|
149
|
+
"son artefact et son critère observable. Ce qu'il faut PRODUIRE, pas des conseils.",
|
|
150
|
+
inputSchema: {
|
|
151
|
+
type: "object",
|
|
152
|
+
properties: {
|
|
153
|
+
situation: { type: "string", description: "La situation, le blocage ou la difficulté." },
|
|
154
|
+
delivery_mode: deliveryModeProp,
|
|
155
|
+
},
|
|
156
|
+
required: ["situation"],
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
name: "set_profile",
|
|
161
|
+
description: "Déclare ou met à jour le profil projet de l'utilisateur pour la session (nom, type, " +
|
|
162
|
+
"mode de delivery, état du design system, difficulté). Les autres outils s'y adaptent " +
|
|
163
|
+
"ensuite sans qu'on le redonne. Persisté localement.",
|
|
164
|
+
inputSchema: {
|
|
165
|
+
type: "object",
|
|
166
|
+
properties: {
|
|
167
|
+
name: { type: "string" },
|
|
168
|
+
product_type: { type: "string" },
|
|
169
|
+
audience: { type: "string" },
|
|
170
|
+
stage: { type: "string" },
|
|
171
|
+
delivery_mode: { type: "string", description: "ai-builder | classique | hybride (ou un libellé)." },
|
|
172
|
+
ds_stage: { type: "string" },
|
|
173
|
+
difficulty: { type: "string" },
|
|
174
|
+
use_case: { type: "string" },
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: "save_review",
|
|
180
|
+
description: "Enregistre le résultat d'un skill (issue register) comme ARTEFACT suivi et horodaté, " +
|
|
181
|
+
"attaché à une référence d'artefact. Permet de re-jouer et de comparer en régression. " +
|
|
182
|
+
"Exige artifact_ref — sans elle, refuse.",
|
|
183
|
+
inputSchema: {
|
|
184
|
+
type: "object",
|
|
185
|
+
properties: {
|
|
186
|
+
skill: { type: "string", description: "Le skill qui a produit la review (ex. better-layout)." },
|
|
187
|
+
artifact_ref: { type: "string", description: "L'artefact audité : lien, chemin, URL." },
|
|
188
|
+
verdict: { type: "string", enum: ["PASS", "PASS WITH FIXES", "FAIL", "BLOCKED"] },
|
|
189
|
+
delivery_mode: deliveryModeProp,
|
|
190
|
+
notes: { type: "string" },
|
|
191
|
+
findings: {
|
|
192
|
+
type: "array",
|
|
193
|
+
description: "Les problèmes trouvés, au schéma issue register.",
|
|
194
|
+
items: {
|
|
195
|
+
type: "object",
|
|
196
|
+
properties: {
|
|
197
|
+
severity: { type: "string", enum: ["bloquant", "critique", "majeur", "mineur", "note"] },
|
|
198
|
+
location: { type: "string" },
|
|
199
|
+
category: { type: "string" },
|
|
200
|
+
summary: { type: "string" },
|
|
201
|
+
evidence: { type: "string" },
|
|
202
|
+
impact: { type: "string" },
|
|
203
|
+
correction: { type: "string" },
|
|
204
|
+
acceptance: { type: "string" },
|
|
205
|
+
confidence: { type: "string", enum: ["élevée", "moyenne", "faible"] },
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
required: ["skill", "artifact_ref"],
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
name: "list_reviews",
|
|
215
|
+
description: "Liste les reviews enregistrées, filtrables par artefact ou par skill.",
|
|
216
|
+
inputSchema: {
|
|
217
|
+
type: "object",
|
|
218
|
+
properties: {
|
|
219
|
+
artifact_ref: { type: "string" },
|
|
220
|
+
skill: { type: "string" },
|
|
221
|
+
limit: { type: "integer", default: 20 },
|
|
222
|
+
},
|
|
223
|
+
},
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
name: "push_audit",
|
|
227
|
+
description: "Envoie une review déjà enregistrée vers le compte designwithai de l'utilisateur, " +
|
|
228
|
+
"pour qu'elle apparaisse dans son cockpit sur n'importe quel appareil. " +
|
|
229
|
+
"Exige DWAI_TOKEN (jeton d'appareil, révocable) et DWAI_INGEST_URL dans l'environnement. " +
|
|
230
|
+
"Aucun secret fort ne transite : le jeton ne donne accès qu'à l'écriture d'un audit.",
|
|
231
|
+
inputSchema: {
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: {
|
|
234
|
+
review_id: {
|
|
235
|
+
type: "string",
|
|
236
|
+
description: "L'id d'une review enregistrée. À défaut, la plus récente est prise.",
|
|
237
|
+
},
|
|
238
|
+
scope: {
|
|
239
|
+
type: "string",
|
|
240
|
+
description: "Ce que l'audit a couvert, en clair (ex. « interface », « sécurité + données »).",
|
|
241
|
+
},
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
name: "compare_reviews",
|
|
247
|
+
description: "Compare deux reviews (régression) : problèmes résolus, nouveaux, persistants. " +
|
|
248
|
+
"Passe deux ids, ou un artifact_ref pour comparer ses deux dernières passes.",
|
|
249
|
+
inputSchema: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
review_base: { type: "string", description: "Id de la review ANCIENNE." },
|
|
253
|
+
review_head: { type: "string", description: "Id de la review RÉCENTE." },
|
|
254
|
+
artifact_ref: { type: "string", description: "À défaut d'ids : compare les 2 dernières de cet artefact." },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
name: "clear_gate",
|
|
260
|
+
description: "Ferme un gate en y attachant sa preuve. Comme sur le site : lire ne débloque rien — " +
|
|
261
|
+
"un gate se ferme sur un artefact localisable. Exige artifact_ref.",
|
|
262
|
+
inputSchema: {
|
|
263
|
+
type: "object",
|
|
264
|
+
properties: {
|
|
265
|
+
chapter: {
|
|
266
|
+
type: "string",
|
|
267
|
+
description: "Le chapitre du gate : son id (volume/slug), son slug, ou des mots-clés.",
|
|
268
|
+
},
|
|
269
|
+
artifact_ref: {
|
|
270
|
+
type: "string",
|
|
271
|
+
description: "Où est l'artefact produit : lien Figma, chemin de fichier, URL.",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
required: ["chapter", "artifact_ref"],
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
name: "reopen_gate",
|
|
279
|
+
description: "Rouvre un gate franchi (retire sa preuve).",
|
|
280
|
+
inputSchema: {
|
|
281
|
+
type: "object",
|
|
282
|
+
properties: {
|
|
283
|
+
chapter: { type: "string", description: "Le chapitre du gate à rouvrir." },
|
|
284
|
+
},
|
|
285
|
+
required: ["chapter"],
|
|
286
|
+
},
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: "gate_status",
|
|
290
|
+
description: "Où en est l'utilisateur : gates franchis vs total, les derniers franchis (avec preuve), " +
|
|
291
|
+
"et les prochains suggérés (non franchis).",
|
|
292
|
+
inputSchema: {
|
|
293
|
+
type: "object",
|
|
294
|
+
properties: {
|
|
295
|
+
objective: { type: "string", description: "Optionnel : biaise les suggestions." },
|
|
296
|
+
delivery_mode: deliveryModeProp,
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
},
|
|
300
|
+
{
|
|
301
|
+
name: "audit_project",
|
|
302
|
+
description: "Flow de bout en bout : audite un VRAI artefact (Figma, dépôt, URL) avec l'orchestrateur, " +
|
|
303
|
+
"ancré dans le Handbook et le profil, puis trace le résultat (review + gates). Compose " +
|
|
304
|
+
"l'orchestrateur, le profil et l'état. Exige artifact_ref.",
|
|
305
|
+
inputSchema: {
|
|
306
|
+
type: "object",
|
|
307
|
+
properties: {
|
|
308
|
+
artifact_ref: { type: "string", description: "L'artefact réel : lien Figma, dépôt, URL." },
|
|
309
|
+
objective: { type: "string", description: "Ce que l'audit doit décider ou améliorer." },
|
|
310
|
+
scope: { type: "string", description: "Écrans, parcours ou composants à couvrir." },
|
|
311
|
+
delivery_mode: deliveryModeProp,
|
|
312
|
+
},
|
|
313
|
+
required: ["artifact_ref"],
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
name: "list_eval_cases",
|
|
318
|
+
description: "Liste les cas du banc d'éval d'un skill (id + type). Ne révèle pas la golden review.",
|
|
319
|
+
inputSchema: {
|
|
320
|
+
type: "object",
|
|
321
|
+
properties: { skill: { type: "string" } },
|
|
322
|
+
required: ["skill"],
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
name: "get_eval_case",
|
|
327
|
+
description: "Renvoie l'artefact d'un cas d'éval, À L'AVEUGLE (sans la golden). Applique le skill dessus, " +
|
|
328
|
+
"puis appelle score_review avec tes findings.",
|
|
329
|
+
inputSchema: {
|
|
330
|
+
type: "object",
|
|
331
|
+
properties: { case_id: { type: "string" } },
|
|
332
|
+
required: ["case_id"],
|
|
333
|
+
},
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: "score_review",
|
|
337
|
+
description: "Score une review candidate contre la golden d'un cas : détection, périmètre, verdict, refus. " +
|
|
338
|
+
"Enregistre le résultat dans la scorecard du skill.",
|
|
339
|
+
inputSchema: {
|
|
340
|
+
type: "object",
|
|
341
|
+
properties: {
|
|
342
|
+
case_id: { type: "string" },
|
|
343
|
+
verdict: { type: "string", enum: ["PASS", "PASS WITH FIXES", "FAIL", "BLOCKED"] },
|
|
344
|
+
refused: { type: "boolean", description: "true si le skill a refusé (artefact insuffisant)." },
|
|
345
|
+
findings: {
|
|
346
|
+
type: "array",
|
|
347
|
+
items: {
|
|
348
|
+
type: "object",
|
|
349
|
+
properties: {
|
|
350
|
+
severity: { type: "string" },
|
|
351
|
+
location: { type: "string" },
|
|
352
|
+
category: { type: "string" },
|
|
353
|
+
summary: { type: "string" },
|
|
354
|
+
correction: { type: "string" },
|
|
355
|
+
},
|
|
356
|
+
},
|
|
357
|
+
},
|
|
358
|
+
},
|
|
359
|
+
required: ["case_id"],
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
{
|
|
363
|
+
name: "skill_scorecard",
|
|
364
|
+
description: "La scorecard agrégée d'un skill : détection, faux positifs, respect du périmètre, refus.",
|
|
365
|
+
inputSchema: {
|
|
366
|
+
type: "object",
|
|
367
|
+
properties: { skill: { type: "string" } },
|
|
368
|
+
required: ["skill"],
|
|
369
|
+
},
|
|
370
|
+
},
|
|
371
|
+
{
|
|
372
|
+
name: "verify_fix",
|
|
373
|
+
description: "Vérifie qu'une recommandation a RÉELLEMENT été appliquée, critère par critère, sur " +
|
|
374
|
+
"l'artefact courant. Exige artifact_ref et des critères. Peut mettre à jour le statut " +
|
|
375
|
+
"d'un finding stocké (open → fixed/partial).",
|
|
376
|
+
inputSchema: {
|
|
377
|
+
type: "object",
|
|
378
|
+
properties: {
|
|
379
|
+
artifact_ref: { type: "string", description: "L'état COURANT vérifié : lien, chemin, URL." },
|
|
380
|
+
expected_fix: { type: "string", description: "La recommandation censée avoir été appliquée." },
|
|
381
|
+
results: {
|
|
382
|
+
type: "array",
|
|
383
|
+
description: "Ton évaluation par critère d'acceptation.",
|
|
384
|
+
items: {
|
|
385
|
+
type: "object",
|
|
386
|
+
properties: {
|
|
387
|
+
criterion: { type: "string" },
|
|
388
|
+
met: { type: "boolean" },
|
|
389
|
+
evidence: { type: "string" },
|
|
390
|
+
},
|
|
391
|
+
required: ["criterion", "met"],
|
|
392
|
+
},
|
|
393
|
+
},
|
|
394
|
+
review_id: { type: "string", description: "Optionnel : la review d'origine à mettre à jour." },
|
|
395
|
+
finding_index: { type: "integer", description: "Optionnel : index du finding (0-based)." },
|
|
396
|
+
},
|
|
397
|
+
required: ["artifact_ref", "results"],
|
|
398
|
+
},
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
name: "generate_decision_record",
|
|
402
|
+
description: "Transforme une analyse en décision traçable (ADR) : contexte, preuves, options, décision, " +
|
|
403
|
+
"risques acceptés, critères de réévaluation, règles du Handbook utilisées. Persistée.",
|
|
404
|
+
inputSchema: {
|
|
405
|
+
type: "object",
|
|
406
|
+
properties: {
|
|
407
|
+
title: { type: "string" },
|
|
408
|
+
context: { type: "string" },
|
|
409
|
+
decision: { type: "string" },
|
|
410
|
+
evidence: { type: "array", items: { type: "string" } },
|
|
411
|
+
options: { type: "array", items: { type: "string" } },
|
|
412
|
+
accepted_risks: { type: "array", items: { type: "string" } },
|
|
413
|
+
review_criteria: { type: "array", items: { type: "string" } },
|
|
414
|
+
sources: {
|
|
415
|
+
type: "array",
|
|
416
|
+
items: { type: "string" },
|
|
417
|
+
description: "Les unités du Handbook citées (uri, ou titre + version).",
|
|
418
|
+
},
|
|
419
|
+
artifact_ref: { type: "string" },
|
|
420
|
+
},
|
|
421
|
+
required: ["title", "decision"],
|
|
422
|
+
},
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
name: "next_gate",
|
|
426
|
+
description: "Le prochain gate à franchir, CONSCIENT DES PRÉRÉQUIS : ne propose que des gates débloqués " +
|
|
427
|
+
"(leurs prérequis sont franchis) et pas encore faits. Explique pourquoi et l'artefact attendu, " +
|
|
428
|
+
"et liste les gates bloqués avec ce qui manque.",
|
|
429
|
+
inputSchema: {
|
|
430
|
+
type: "object",
|
|
431
|
+
properties: {
|
|
432
|
+
objective: { type: "string", description: "Optionnel : biaise vers un objectif." },
|
|
433
|
+
delivery_mode: deliveryModeProp,
|
|
434
|
+
},
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: "connect_repo",
|
|
439
|
+
description: "Lit un DÉPÔT LOCAL pour ancrer l'analyse dans le vrai code : stack, composants, tokens, " +
|
|
440
|
+
"sources présentes/absentes. Les sources distantes (Figma, Supabase live, GitHub) passent " +
|
|
441
|
+
"par leurs propres serveurs MCP.",
|
|
442
|
+
inputSchema: {
|
|
443
|
+
type: "object",
|
|
444
|
+
properties: { path: { type: "string", description: "Chemin du dépôt local à lire." } },
|
|
445
|
+
required: ["path"],
|
|
446
|
+
},
|
|
447
|
+
},
|
|
448
|
+
];
|
|
449
|
+
// ─── Implémentations ─────────────────────────────────────────────────────────
|
|
450
|
+
function toolSetProfile(args) {
|
|
451
|
+
const fields = {
|
|
452
|
+
name: "name",
|
|
453
|
+
product_type: "productType",
|
|
454
|
+
audience: "audience",
|
|
455
|
+
stage: "stage",
|
|
456
|
+
delivery_mode: "deliveryMode",
|
|
457
|
+
ds_stage: "dsStage",
|
|
458
|
+
difficulty: "difficulty",
|
|
459
|
+
use_case: "useCase",
|
|
460
|
+
};
|
|
461
|
+
const merged = { ...(currentProfile ?? {}) };
|
|
462
|
+
for (const [arg, field] of Object.entries(fields)) {
|
|
463
|
+
const v = args[arg];
|
|
464
|
+
if (v != null && String(v).trim())
|
|
465
|
+
merged[field] = String(v).trim();
|
|
466
|
+
}
|
|
467
|
+
currentProfile = merged;
|
|
468
|
+
let saved = "(non persisté)";
|
|
469
|
+
try {
|
|
470
|
+
saved = saveProfile(merged);
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
/* stockage bloqué : le profil vaut pour la session, sans persistance */
|
|
474
|
+
}
|
|
475
|
+
return text(`Profil enregistré : ${profileSummary(merged) || "(vide)"}\n` +
|
|
476
|
+
`Fichier : ${saved}\n` +
|
|
477
|
+
`Mode retenu : ${resolveDeliveryMode(merged.deliveryMode) ?? "non déterminé"}. ` +
|
|
478
|
+
`Les prochaines recos s'y adaptent.`);
|
|
479
|
+
}
|
|
480
|
+
// Poids du sémantique dans le score combiné, et seuil sous lequel une similarité
|
|
481
|
+
// est trop faible pour compter (sinon tout le corpus obtient un petit score).
|
|
482
|
+
const SEM_WEIGHT = 12;
|
|
483
|
+
const SEM_FLOOR = 0.25;
|
|
484
|
+
async function toolSearch(args) {
|
|
485
|
+
const query = String(args.query ?? "");
|
|
486
|
+
const kind = args.kind;
|
|
487
|
+
const book = args.book;
|
|
488
|
+
const mode = effectiveMode(args);
|
|
489
|
+
const limit = Math.max(1, Math.min(25, Number(args.limit ?? 8)));
|
|
490
|
+
const terms = tokenize(query);
|
|
491
|
+
const rawTerms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
492
|
+
const pool = handbook.units
|
|
493
|
+
.filter((u) => (kind ? u.kind === kind : true))
|
|
494
|
+
.filter((u) => (book ? u.book === book : true))
|
|
495
|
+
.filter((u) => chapterMatchesMode(u, mode));
|
|
496
|
+
// Sémantique seulement si des embeddings existent — sinon on retombe sur les
|
|
497
|
+
// mots-clés, à l'identique.
|
|
498
|
+
const sem = query ? await trySemanticScores(query) : null;
|
|
499
|
+
const hits = pool
|
|
500
|
+
.map((u) => {
|
|
501
|
+
const kw = scoreUnit(u, terms);
|
|
502
|
+
const cs = sem ? (sem.get(u.uri) ?? 0) : 0;
|
|
503
|
+
return { u, score: kw + (cs >= SEM_FLOOR ? SEM_WEIGHT * cs : 0) };
|
|
504
|
+
})
|
|
505
|
+
.filter((x) => x.score > 0)
|
|
506
|
+
.sort((a, b) => b.score - a.score)
|
|
507
|
+
.slice(0, limit);
|
|
508
|
+
if (hits.length === 0)
|
|
509
|
+
return text(`Aucun résultat pour « ${query} ».`);
|
|
510
|
+
const kindLabel = sem ? "hybride : mots-clés + sémantique" : "mots-clés";
|
|
511
|
+
const body = hits
|
|
512
|
+
.map(({ u }, i) => `${i + 1}. [${u.kind}] ${u.title}\n ${cite(u)}\n ${excerpt(u.text, rawTerms)}`)
|
|
513
|
+
.join("\n\n");
|
|
514
|
+
return text(`${hits.length} résultat(s) pour « ${query} » (${kindLabel}). Cite la source de chacun.\n\n${body}`);
|
|
515
|
+
}
|
|
516
|
+
function toolRecommend(args) {
|
|
517
|
+
const objective = String(args.objective ?? "");
|
|
518
|
+
const mode = effectiveMode(args);
|
|
519
|
+
const cleared = new Set(Object.keys(currentState.gates));
|
|
520
|
+
const pool = rankChapters(objective, mode, 12);
|
|
521
|
+
const fresh = pool.filter((u) => !cleared.has(u.id));
|
|
522
|
+
const chapters = fresh.slice(0, 6);
|
|
523
|
+
const skipped = pool.length - fresh.length;
|
|
524
|
+
if (chapters.length === 0) {
|
|
525
|
+
return text(pool.length === 0
|
|
526
|
+
? `Rien de pertinent trouvé pour « ${objective} ».`
|
|
527
|
+
: `Tout ce qui ressort pour « ${objective} » est déjà franchi (${pool.length} gate(s)). Rien à proposer — c'est bon signe.`);
|
|
528
|
+
}
|
|
529
|
+
const steps = chapters
|
|
530
|
+
.map((u, i) => {
|
|
531
|
+
const g = u.gate;
|
|
532
|
+
return (`${i + 1}. ${u.title}\n` +
|
|
533
|
+
(g ? ` Artefact : ${g.artifact}\n Réussi quand : ${g.criterion}\n` : "") +
|
|
534
|
+
` Source : ${cite(u)}`);
|
|
535
|
+
})
|
|
536
|
+
.join("\n\n");
|
|
537
|
+
return text(`Ces ${chapters.length} étapes sont des GATES à franchir dans l'ordre pour « ${objective} » — ` +
|
|
538
|
+
`pas une solution clé en main. Chacune exige un artefact réel ; lire ne débloque rien.` +
|
|
539
|
+
(skipped ? ` (${skipped} déjà franchi(s), écarté(s).)` : "") +
|
|
540
|
+
`\n\n${steps}\n\nQuand un gate est produit, ferme-le avec clear_gate + son artefact.`);
|
|
541
|
+
}
|
|
542
|
+
function toolReview(args) {
|
|
543
|
+
const ref = String(args.artifact_ref ?? "").trim();
|
|
544
|
+
if (!ref) {
|
|
545
|
+
return refusal("review_artifact exige une référence d'artefact localisable (lien Figma, chemin de " +
|
|
546
|
+
"fichier, URL). Un review sans artefact qu'on peut ouvrir n'en est pas un — c'est un " +
|
|
547
|
+
"avis en l'air. Fournis `artifact_ref`.");
|
|
548
|
+
}
|
|
549
|
+
const mode = effectiveMode(args);
|
|
550
|
+
const topic = String(args.topic ?? "") || ref;
|
|
551
|
+
const chapters = rankChapters(topic, mode, 4);
|
|
552
|
+
if (chapters.length === 0) {
|
|
553
|
+
return text(`Artefact : ${ref}\nAucun chapitre ne ressort pour « ${topic} ». Précise le sujet du review.`);
|
|
554
|
+
}
|
|
555
|
+
const grid = chapters
|
|
556
|
+
.map((u) => {
|
|
557
|
+
const rules = atomsOf(u.id, ["rule", "anti-pattern", "scorecard"]);
|
|
558
|
+
const rulesText = rules.length
|
|
559
|
+
? "\n" + rules.map((r) => ` • [${r.kind}] ${r.title}`).join("\n")
|
|
560
|
+
: "";
|
|
561
|
+
return (`## ${u.title} — ${cite(u)}\n` +
|
|
562
|
+
(u.gate ? `Critère de gate : ${u.gate.criterion}` : "") +
|
|
563
|
+
rulesText);
|
|
564
|
+
})
|
|
565
|
+
.join("\n\n");
|
|
566
|
+
return text(`Review de l'artefact : ${ref}\n\n` +
|
|
567
|
+
`Confronte-le aux critères du Handbook ci-dessous : réponds oui/non à chacun, en citant la ` +
|
|
568
|
+
`source. Le MCP donne la grille ; le jugement reste le tien.\n\n${grid}`);
|
|
569
|
+
}
|
|
570
|
+
function toolPlan(args) {
|
|
571
|
+
const situation = String(args.situation ?? "");
|
|
572
|
+
const mode = effectiveMode(args);
|
|
573
|
+
const cleared = new Set(Object.keys(currentState.gates));
|
|
574
|
+
const pool = rankChapters(situation, mode, 12);
|
|
575
|
+
const fresh = pool.filter((u) => !cleared.has(u.id));
|
|
576
|
+
const chapters = fresh.slice(0, 6);
|
|
577
|
+
const skipped = pool.length - fresh.length;
|
|
578
|
+
if (chapters.length === 0) {
|
|
579
|
+
return text(pool.length === 0
|
|
580
|
+
? `Rien de pertinent trouvé pour « ${situation} ».`
|
|
581
|
+
: `Tout ce qui répond à « ${situation} » est déjà franchi (${pool.length} gate(s)).`);
|
|
582
|
+
}
|
|
583
|
+
const plan = chapters
|
|
584
|
+
.map((u, i) => {
|
|
585
|
+
const g = u.gate;
|
|
586
|
+
return (`${i + 1}. ${u.title}\n` +
|
|
587
|
+
(g
|
|
588
|
+
? ` À produire : ${g.artifact}\n Gate franchi quand : ${g.criterion}\n Budget : ~${g.minutes} min\n`
|
|
589
|
+
: "") +
|
|
590
|
+
` Source : ${cite(u)}`);
|
|
591
|
+
})
|
|
592
|
+
.join("\n\n");
|
|
593
|
+
return text(`Plan d'action pour : « ${situation} ».\n` +
|
|
594
|
+
`Ce sont des GATES à franchir dans l'ordre — ce qu'il faut PRODUIRE, pas des conseils. ` +
|
|
595
|
+
`Chaque gate se ferme sur un artefact et un critère observable.` +
|
|
596
|
+
(skipped ? ` (${skipped} déjà franchi(s), écarté(s).)` : "") +
|
|
597
|
+
`\n\n${plan}`);
|
|
598
|
+
}
|
|
599
|
+
// ─── Reviews suivies (étape C) ───────────────────────────────────────────────
|
|
600
|
+
function sevCounts(findings) {
|
|
601
|
+
const order = ["bloquant", "critique", "majeur", "mineur", "note"];
|
|
602
|
+
const c = {};
|
|
603
|
+
for (const f of findings) {
|
|
604
|
+
const s = f.severity ?? "note";
|
|
605
|
+
c[s] = (c[s] ?? 0) + 1;
|
|
606
|
+
}
|
|
607
|
+
const parts = order.filter((s) => c[s]).map((s) => `${c[s]} ${s}`);
|
|
608
|
+
return parts.length ? parts.join(", ") : "aucun";
|
|
609
|
+
}
|
|
610
|
+
function renderFinding(f) {
|
|
611
|
+
return ` • [${f.severity ?? "note"}] ${f.location ? `${f.location} — ` : ""}${f.summary ?? f.category ?? ""}`;
|
|
612
|
+
}
|
|
613
|
+
function toolSaveReview(args) {
|
|
614
|
+
const artifact_ref = String(args.artifact_ref ?? "").trim();
|
|
615
|
+
if (!artifact_ref) {
|
|
616
|
+
return refusal("save_review exige une référence d'artefact localisable. Une review sans artefact " +
|
|
617
|
+
"rattaché n'est pas suivable — c'est un avis en l'air.");
|
|
618
|
+
}
|
|
619
|
+
const findings = Array.isArray(args.findings) ? args.findings : [];
|
|
620
|
+
const r = saveReview({
|
|
621
|
+
skill: String(args.skill ?? "review"),
|
|
622
|
+
artifact_ref,
|
|
623
|
+
delivery_mode: args.delivery_mode ? String(args.delivery_mode) : undefined,
|
|
624
|
+
verdict: args.verdict ? String(args.verdict) : undefined,
|
|
625
|
+
notes: args.notes ? String(args.notes) : undefined,
|
|
626
|
+
findings,
|
|
627
|
+
}, new Date());
|
|
628
|
+
return text(`Review enregistrée : ${r.id}\n` +
|
|
629
|
+
`Artefact : ${artifact_ref}\n` +
|
|
630
|
+
`Verdict : ${r.verdict ?? "—"} · Problèmes : ${sevCounts(findings)}\n` +
|
|
631
|
+
`Re-lance le skill plus tard, puis compare_reviews pour mesurer la régression.`);
|
|
632
|
+
}
|
|
633
|
+
function toolListReviews(args) {
|
|
634
|
+
const rows = listReviews({
|
|
635
|
+
artifact_ref: args.artifact_ref ? String(args.artifact_ref) : undefined,
|
|
636
|
+
skill: args.skill ? String(args.skill) : undefined,
|
|
637
|
+
}).slice(0, Math.max(1, Math.min(100, Number(args.limit ?? 20))));
|
|
638
|
+
if (rows.length === 0)
|
|
639
|
+
return text("Aucune review enregistrée.");
|
|
640
|
+
const body = rows
|
|
641
|
+
.map((r) => `• ${r.id} [${r.skill}] ${r.verdict ?? "—"} (${sevCounts(r.findings)})\n ${r.artifact_ref}`)
|
|
642
|
+
.join("\n");
|
|
643
|
+
return text(`${rows.length} review(s) :\n${body}`);
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Le pont vers le compte.
|
|
647
|
+
*
|
|
648
|
+
* L'audit se produit ici, dans l'éditeur ; il doit réapparaître dans le cockpit
|
|
649
|
+
* du site. Ce handler ne connaît AUCUN secret fort : il porte un jeton
|
|
650
|
+
* d'appareil, révocable depuis le compte, et l'échange contre une écriture via
|
|
651
|
+
* l'Edge Function. Si le jeton fuit, on le révoque — rien d'autre n'est exposé.
|
|
652
|
+
*/
|
|
653
|
+
/**
|
|
654
|
+
* L'adresse de réception est la même pour tout le monde et ne change jamais :
|
|
655
|
+
* la demander à chaque utilisateur allongeait la commande de branchement d'une
|
|
656
|
+
* ligne illisible, pour une valeur qu'aucun d'eux ne choisit. Seul le jeton est
|
|
657
|
+
* personnel — c'est donc le seul réglage à fournir. La variable reste lisible
|
|
658
|
+
* pour pointer un environnement de test.
|
|
659
|
+
*/
|
|
660
|
+
const DEFAULT_INGEST_URL = "https://hbugvurcdmimhvvfdlos.supabase.co/functions/v1/ingest-audit";
|
|
661
|
+
async function toolPushAudit(args) {
|
|
662
|
+
const token = process.env.DWAI_TOKEN?.trim();
|
|
663
|
+
const url = process.env.DWAI_INGEST_URL?.trim() || DEFAULT_INGEST_URL;
|
|
664
|
+
if (!token) {
|
|
665
|
+
return refusal("push_audit a besoin de DWAI_TOKEN dans l'environnement du serveur MCP. " +
|
|
666
|
+
"Génère un jeton dans ton compte (page Compte → « Connecter mon éditeur »), " +
|
|
667
|
+
"puis ajoute-le à la config de ton éditeur.");
|
|
668
|
+
}
|
|
669
|
+
const review = args.review_id
|
|
670
|
+
? getReview(String(args.review_id))
|
|
671
|
+
: (listReviews()[0] ?? null);
|
|
672
|
+
if (!review) {
|
|
673
|
+
return refusal("Aucune review à envoyer. Lance d'abord un skill et enregistre le résultat avec save_review.");
|
|
674
|
+
}
|
|
675
|
+
// On n'envoie que le résumé utile au cockpit : ni preuve brute, ni contenu de
|
|
676
|
+
// fichier. Ce qui part est ce qui s'affiche.
|
|
677
|
+
const payload = {
|
|
678
|
+
token,
|
|
679
|
+
audit: {
|
|
680
|
+
product_ref: review.artifact_ref,
|
|
681
|
+
verdict: review.verdict ?? null,
|
|
682
|
+
scope: args.scope ? String(args.scope) : review.skill,
|
|
683
|
+
// L'outil source voyage à part du scope : le cockpit s'en sert pour ranger
|
|
684
|
+
// les constats par axe, sans avoir à interpréter une phrase libre.
|
|
685
|
+
tool: review.skill,
|
|
686
|
+
findings: review.findings.map((f) => ({
|
|
687
|
+
severity: f.severity ?? null,
|
|
688
|
+
summary: f.summary ?? null,
|
|
689
|
+
location: f.location ?? null,
|
|
690
|
+
})),
|
|
691
|
+
},
|
|
692
|
+
};
|
|
693
|
+
try {
|
|
694
|
+
const res = await fetch(url, {
|
|
695
|
+
method: "POST",
|
|
696
|
+
headers: { "content-type": "application/json" },
|
|
697
|
+
body: JSON.stringify(payload),
|
|
698
|
+
});
|
|
699
|
+
const body = (await res.json().catch(() => ({})));
|
|
700
|
+
if (!res.ok) {
|
|
701
|
+
return refusal(`L'envoi a été refusé (${res.status} ${String(body.error ?? "")}). ` +
|
|
702
|
+
(res.status === 401
|
|
703
|
+
? "Le jeton est inconnu ou révoqué : regénères-en un depuis ton compte."
|
|
704
|
+
: "Vérifie DWAI_INGEST_URL."));
|
|
705
|
+
}
|
|
706
|
+
return text(`Audit envoyé sous ton compte.\n` +
|
|
707
|
+
`Review : ${review.id} [${review.skill}] ${review.verdict ?? "—"}\n` +
|
|
708
|
+
`Objet : ${review.artifact_ref}\n` +
|
|
709
|
+
`Constats : ${review.findings.length}\n` +
|
|
710
|
+
`Id côté compte : ${String(body.id ?? "—")}\n` +
|
|
711
|
+
`Il apparaît maintenant dans ton cockpit, sur n'importe quel appareil connecté.`);
|
|
712
|
+
}
|
|
713
|
+
catch (e) {
|
|
714
|
+
return refusal(`Impossible de joindre ${url} : ${e instanceof Error ? e.message : String(e)}. ` +
|
|
715
|
+
"Réseau filtré ? Essaie depuis un autre réseau.");
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
function toolCompare(args) {
|
|
719
|
+
let base = null;
|
|
720
|
+
let head = null;
|
|
721
|
+
if (args.review_base && args.review_head) {
|
|
722
|
+
base = getReview(String(args.review_base));
|
|
723
|
+
head = getReview(String(args.review_head));
|
|
724
|
+
}
|
|
725
|
+
else if (args.artifact_ref) {
|
|
726
|
+
const rows = listReviews({ artifact_ref: String(args.artifact_ref) });
|
|
727
|
+
if (rows.length >= 2) {
|
|
728
|
+
head = rows[0];
|
|
729
|
+
base = rows[1];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
if (!base || !head) {
|
|
733
|
+
return refusal("Fournis review_base + review_head, ou un artifact_ref ayant au moins deux reviews.");
|
|
734
|
+
}
|
|
735
|
+
const d = compareReviews(base, head);
|
|
736
|
+
const block = (label, arr) => arr.length ? `\n${label} (${arr.length}) :\n${arr.map(renderFinding).join("\n")}` : `\n${label} : aucun`;
|
|
737
|
+
return text(`Régression — ${base.id} → ${head.id}\nArtefact : ${head.artifact_ref}` +
|
|
738
|
+
block("✓ Résolus", d.resolved) +
|
|
739
|
+
block("+ Nouveaux", d.added) +
|
|
740
|
+
block("• Persistants", d.persisting));
|
|
741
|
+
}
|
|
742
|
+
// ─── Boucle d'état des gates (étape D, version locale) ───────────────────────
|
|
743
|
+
function toolClearGate(args) {
|
|
744
|
+
const artifact_ref = String(args.artifact_ref ?? "").trim();
|
|
745
|
+
if (!artifact_ref) {
|
|
746
|
+
return refusal("clear_gate exige une référence d'artefact. Un gate qu'on ferme sans rien montrer est " +
|
|
747
|
+
"un mensonge qu'on se fait à soi-même — donne le lien, le chemin ou l'URL.");
|
|
748
|
+
}
|
|
749
|
+
const ch = resolveChapter(String(args.chapter ?? ""));
|
|
750
|
+
if (!ch)
|
|
751
|
+
return refusal(`Aucun chapitre trouvé pour « ${String(args.chapter ?? "")} ».`);
|
|
752
|
+
currentState = clearGate(currentState, ch.id, artifact_ref, new Date());
|
|
753
|
+
saveState(currentState);
|
|
754
|
+
const count = Object.keys(currentState.gates).length;
|
|
755
|
+
return text(`Gate franchi : ${ch.title}\n` +
|
|
756
|
+
`Critère : ${ch.gate?.criterion ?? "—"}\n` +
|
|
757
|
+
`Artefact : ${artifact_ref}\n` +
|
|
758
|
+
`Progression : ${count}/${chapterUnits.length} gate(s).`);
|
|
759
|
+
}
|
|
760
|
+
function toolReopenGate(args) {
|
|
761
|
+
const ch = resolveChapter(String(args.chapter ?? ""));
|
|
762
|
+
if (!ch)
|
|
763
|
+
return refusal(`Aucun chapitre trouvé pour « ${String(args.chapter ?? "")} ».`);
|
|
764
|
+
if (!currentState.gates[ch.id])
|
|
765
|
+
return text(`Le gate « ${ch.title} » n'était pas franchi.`);
|
|
766
|
+
currentState = reopenGate(currentState, ch.id);
|
|
767
|
+
saveState(currentState);
|
|
768
|
+
return text(`Gate rouvert : ${ch.title}.`);
|
|
769
|
+
}
|
|
770
|
+
function toolGateStatus(args) {
|
|
771
|
+
const clearedIds = Object.keys(currentState.gates);
|
|
772
|
+
const total = chapterUnits.length;
|
|
773
|
+
const recent = clearedIds
|
|
774
|
+
.map((id) => ({ id, unit: chapterUnits.find((u) => u.id === id), gate: currentState.gates[id] }))
|
|
775
|
+
.filter((x) => x.unit)
|
|
776
|
+
.sort((a, b) => b.gate.clearedAt.localeCompare(a.gate.clearedAt))
|
|
777
|
+
.slice(0, 5)
|
|
778
|
+
.map((x) => ` ✓ ${x.unit.title}\n ${x.gate.artifactRef}`)
|
|
779
|
+
.join("\n");
|
|
780
|
+
const clearedSet = new Set(clearedIds);
|
|
781
|
+
const mode = effectiveMode(args);
|
|
782
|
+
const objective = String(args.objective ?? "");
|
|
783
|
+
const pool = objective
|
|
784
|
+
? rankChapters(objective, mode, 12)
|
|
785
|
+
: chapterUnits.filter((u) => chapterMatchesMode(u, mode));
|
|
786
|
+
const next = pool
|
|
787
|
+
.filter((u) => !clearedSet.has(u.id))
|
|
788
|
+
.slice(0, 5)
|
|
789
|
+
.map((u) => ` ○ ${u.title}${u.gate ? ` — ${u.gate.artifact}` : ""}`)
|
|
790
|
+
.join("\n");
|
|
791
|
+
return text(`Gates : ${clearedIds.length}/${total} franchi(s).\n\n` +
|
|
792
|
+
`Derniers franchis :\n${recent || " (aucun)"}\n\n` +
|
|
793
|
+
`Prochains${objective ? ` pour « ${objective} »` : ""} :\n${next || " (rien à proposer)"}`);
|
|
794
|
+
}
|
|
795
|
+
// ─── Flow « audite mon vrai projet » (étape E) ───────────────────────────────
|
|
796
|
+
function toolAuditProject(args) {
|
|
797
|
+
const ref = String(args.artifact_ref ?? "").trim();
|
|
798
|
+
if (!ref) {
|
|
799
|
+
return refusal("audit_project exige une référence d'artefact réel (lien Figma, dépôt, URL). " +
|
|
800
|
+
"Sans artefact, il n'y a rien à auditer — c'est le principe.");
|
|
801
|
+
}
|
|
802
|
+
const mode = effectiveMode(args);
|
|
803
|
+
const objective = String(args.objective ?? "");
|
|
804
|
+
const scope = String(args.scope ?? "");
|
|
805
|
+
const q = [objective, scope].filter(Boolean).join(" ") || ref;
|
|
806
|
+
const cleared = new Set(Object.keys(currentState.gates));
|
|
807
|
+
const pool = rankChapters(q, mode, 8);
|
|
808
|
+
const fresh = pool.filter((u) => !cleared.has(u.id)).slice(0, 5);
|
|
809
|
+
const doneNote = pool.length - pool.filter((u) => !cleared.has(u.id)).length;
|
|
810
|
+
const anchor = fresh.map((u) => ` • ${u.title} — ${u.gate?.criterion ?? "—"}\n ${cite(u)}`).join("\n") ||
|
|
811
|
+
" (précise l'objectif pour ancrer la revue dans le Handbook)";
|
|
812
|
+
const ctx = currentProfile
|
|
813
|
+
? profileSummary(currentProfile) || "(profil chargé)"
|
|
814
|
+
: "profil non déclaré — set_profile pour contextualiser";
|
|
815
|
+
const step1 = skillByName.has("better-interface")
|
|
816
|
+
? "Applique le skill `better-interface` (prompt /better-interface) sur cet artefact : sélectionne UNIQUEMENT les experts pertinents, agrège leurs diagnostics, arbitre les conflits. N'exécute pas tous les skills par défaut."
|
|
817
|
+
: "Sélectionne les skills pertinents, agrège leurs diagnostics, arbitre les conflits.";
|
|
818
|
+
return text(`FLOW « audite mon vrai projet »\n` +
|
|
819
|
+
`Artefact : ${ref}\n` +
|
|
820
|
+
`Contexte : ${ctx}\n` +
|
|
821
|
+
`Mode : ${mode ?? "non déterminé"}\n` +
|
|
822
|
+
`Périmètre : ${scope || objective || "(à cadrer avant de lancer)"}\n\n` +
|
|
823
|
+
`1. ORCHESTRE\n${step1}\n\n` +
|
|
824
|
+
`2. ANCRE DANS LE HANDBOOK\nConfronte chaque finding aux critères ci-dessous, et cite la source :\n${anchor}` +
|
|
825
|
+
(doneNote ? `\n (${doneNote} gate(s) déjà franchi(s), écarté(s).)` : "") +
|
|
826
|
+
`\n\n3. TRACE — rien n'est « fait » tant que ce n'est pas tracé\n` +
|
|
827
|
+
`- Enregistre le résultat : save_review(skill:"better-interface", artifact_ref:"${ref}", verdict, findings:[…]).\n` +
|
|
828
|
+
`- Pour chaque gate dont le critère est atteint par l'artefact : clear_gate(chapter, artifact_ref:"${ref}").\n` +
|
|
829
|
+
`- Plus tard, re-lance puis compare_reviews pour mesurer la régression.`);
|
|
830
|
+
}
|
|
831
|
+
// ─── Banc d'éval des skills (clef de voûte) ──────────────────────────────────
|
|
832
|
+
function toolListEvalCases(args) {
|
|
833
|
+
const skill = String(args.skill ?? "");
|
|
834
|
+
const cases = loadCases(skill);
|
|
835
|
+
if (cases.length === 0)
|
|
836
|
+
return text(`Aucun cas d'éval pour « ${skill} ».`);
|
|
837
|
+
const body = cases.map((c) => `• ${c.id} [${c.kind}]${c.delivery_mode ? ` (${c.delivery_mode})` : ""}`).join("\n");
|
|
838
|
+
return text(`${cases.length} cas pour ${skill} :\n${body}\n\n` +
|
|
839
|
+
`get_eval_case <id> pour l'artefact (à l'aveugle), puis score_review.`);
|
|
840
|
+
}
|
|
841
|
+
function toolGetEvalCase(args) {
|
|
842
|
+
const c = getCase(String(args.case_id ?? ""));
|
|
843
|
+
if (!c)
|
|
844
|
+
return refusal(`Cas d'éval introuvable : ${String(args.case_id ?? "")}`);
|
|
845
|
+
// On ne révèle JAMAIS la golden : le run doit être aveugle.
|
|
846
|
+
return text(`Cas ${c.id} · skill ${c.skill}${c.delivery_mode ? ` · mode ${c.delivery_mode}` : ""}\n\n` +
|
|
847
|
+
`ARTEFACT :\n${c.artifact}\n\n` +
|
|
848
|
+
`Applique le skill ${c.skill} sur cet artefact (reste dans ton périmètre, refuse si l'artefact ` +
|
|
849
|
+
`est insuffisant), puis appelle score_review(case_id:"${c.id}", findings:[…], verdict, refused).`);
|
|
850
|
+
}
|
|
851
|
+
function toolScoreReview(args) {
|
|
852
|
+
const c = getCase(String(args.case_id ?? ""));
|
|
853
|
+
if (!c)
|
|
854
|
+
return refusal(`Cas d'éval introuvable : ${String(args.case_id ?? "")}`);
|
|
855
|
+
const cand = {
|
|
856
|
+
findings: Array.isArray(args.findings) ? args.findings : [],
|
|
857
|
+
verdict: args.verdict ? String(args.verdict) : undefined,
|
|
858
|
+
refused: typeof args.refused === "boolean" ? args.refused : undefined,
|
|
859
|
+
};
|
|
860
|
+
const s = scoreCase(c, cand, new Date());
|
|
861
|
+
saveScore(s);
|
|
862
|
+
const pct = (x) => `${Math.round(x * 100)}%`;
|
|
863
|
+
return text(`Cas ${s.case_id} [${s.kind}] — ${s.passed ? "PASSÉ ✓" : "ÉCHOUÉ ✗"}\n` +
|
|
864
|
+
`Détection : ${s.detected}/${s.expected}${s.expected ? ` (${pct(s.detection_rate)})` : " (rien à trouver)"}\n` +
|
|
865
|
+
(s.missed.length ? `Manqués : ${s.missed.join(" · ")}\n` : "") +
|
|
866
|
+
`Périmètre : ${s.scope_ok ? "respecté" : `VIOLÉ → ${s.forbidden_hits.join(" · ")}`}\n` +
|
|
867
|
+
`Refus attendu : ${(c.golden.should_refuse ?? false) ? "oui" : "non"} · obtenu : ${cand.refused ? "oui" : "non"} → ${s.refusal_ok ? "ok" : "KO"}\n` +
|
|
868
|
+
`Faux positifs (majeur+) : ${s.extra_significant} · notes hors-golden : ${s.extra_findings - s.extra_significant}\n` +
|
|
869
|
+
(c.golden.expected_verdict
|
|
870
|
+
? `Verdict attendu / obtenu : ${c.golden.expected_verdict} / ${cand.verdict ?? "—"} — ${s.verdict_ok ? "✓" : "✗"}\n`
|
|
871
|
+
: "") +
|
|
872
|
+
`Résultat enregistré dans la scorecard.`);
|
|
873
|
+
}
|
|
874
|
+
function toolScorecard(args) {
|
|
875
|
+
const skill = String(args.skill ?? "");
|
|
876
|
+
const sc = scorecard(skill);
|
|
877
|
+
const pct = (x) => (x === null ? "—" : `${Math.round(x * 100)}%`);
|
|
878
|
+
return text(`${skill} — scorecard\n\n` +
|
|
879
|
+
`Cas testés : ${sc.cases_tested}/${sc.cases_total}\n` +
|
|
880
|
+
`Passés : ${sc.passed}/${sc.cases_tested}\n` +
|
|
881
|
+
`Détection critique : ${pct(sc.detection_rate)}\n` +
|
|
882
|
+
`Faux positifs : ${pct(sc.false_positive_rate)}\n` +
|
|
883
|
+
`Respect du périmètre : ${pct(sc.scope_adherence)}\n` +
|
|
884
|
+
`Justesse du refus : ${pct(sc.refusal_accuracy)}\n` +
|
|
885
|
+
`Dernière validation : ${sc.last_validated ?? "jamais"}`);
|
|
886
|
+
}
|
|
887
|
+
// ─── Vérification & décisions (étapes verify_fix / decision_record) ──────────
|
|
888
|
+
function toolVerifyFix(args) {
|
|
889
|
+
const ref = String(args.artifact_ref ?? "").trim();
|
|
890
|
+
if (!ref) {
|
|
891
|
+
return refusal("verify_fix exige une référence d'artefact courant : sans regarder l'état actuel, on ne " +
|
|
892
|
+
"vérifie rien — on suppose.");
|
|
893
|
+
}
|
|
894
|
+
const results = Array.isArray(args.results)
|
|
895
|
+
? args.results
|
|
896
|
+
: [];
|
|
897
|
+
if (results.length === 0) {
|
|
898
|
+
return refusal("verify_fix exige au moins un critère d'acceptation à vérifier.");
|
|
899
|
+
}
|
|
900
|
+
const met = results.filter((r) => r.met);
|
|
901
|
+
const failed = results.filter((r) => !r.met);
|
|
902
|
+
const metNoEvidence = met.filter((r) => !r.evidence || !String(r.evidence).trim());
|
|
903
|
+
const verdict = failed.length === 0 ? "corrigé" : met.length === 0 ? "non corrigé" : "partiel";
|
|
904
|
+
const status = verdict === "corrigé" ? "fixed" : verdict === "partiel" ? "partial" : "open";
|
|
905
|
+
let updated = "";
|
|
906
|
+
if (args.review_id != null && args.finding_index != null) {
|
|
907
|
+
const f = updateFindingStatus(String(args.review_id), Number(args.finding_index), status, ref);
|
|
908
|
+
updated = f
|
|
909
|
+
? `\nStatut du finding mis à jour : ${status}.`
|
|
910
|
+
: `\n(finding non trouvé pour la mise à jour.)`;
|
|
911
|
+
}
|
|
912
|
+
return text(`Vérification — ${verdict.toUpperCase()}\n` +
|
|
913
|
+
`Artefact courant : ${ref}\n` +
|
|
914
|
+
(args.expected_fix ? `Correction attendue : ${String(args.expected_fix)}\n` : "") +
|
|
915
|
+
`Critères remplis : ${met.length}/${results.length}\n` +
|
|
916
|
+
(failed.length ? `Non remplis :\n${failed.map((r) => ` ✗ ${r.criterion}`).join("\n")}\n` : "") +
|
|
917
|
+
(metNoEvidence.length
|
|
918
|
+
? `\n⚠ Déclarés remplis SANS preuve (à étayer, sinon c'est une supposition) :\n${metNoEvidence
|
|
919
|
+
.map((r) => ` ? ${r.criterion}`)
|
|
920
|
+
.join("\n")}\n`
|
|
921
|
+
: "") +
|
|
922
|
+
updated);
|
|
923
|
+
}
|
|
924
|
+
function toolGenerateDecision(args) {
|
|
925
|
+
const title = String(args.title ?? "").trim();
|
|
926
|
+
const decision = String(args.decision ?? "").trim();
|
|
927
|
+
if (!title || !decision) {
|
|
928
|
+
return refusal("generate_decision_record exige au moins un titre et une décision.");
|
|
929
|
+
}
|
|
930
|
+
const arr = (k) => Array.isArray(args[k]) ? args[k].map((x) => String(x)) : undefined;
|
|
931
|
+
const out = saveDecision({
|
|
932
|
+
title,
|
|
933
|
+
decision,
|
|
934
|
+
context: args.context ? String(args.context) : undefined,
|
|
935
|
+
evidence: arr("evidence"),
|
|
936
|
+
options: arr("options"),
|
|
937
|
+
accepted_risks: arr("accepted_risks"),
|
|
938
|
+
review_criteria: arr("review_criteria"),
|
|
939
|
+
sources: arr("sources"),
|
|
940
|
+
artifact_ref: args.artifact_ref ? String(args.artifact_ref) : undefined,
|
|
941
|
+
}, new Date());
|
|
942
|
+
return text(`Décision enregistrée : ${out.decision.id}\nFichier : ${out.path}\n\n${out.markdown}`);
|
|
943
|
+
}
|
|
944
|
+
// ─── Prochain gate, conscient du graphe (prérequis) ─────────────────────────
|
|
945
|
+
function toolNextGate(args) {
|
|
946
|
+
const cleared = new Set(Object.keys(currentState.gates));
|
|
947
|
+
const mode = effectiveMode(args);
|
|
948
|
+
const objective = String(args.objective ?? "");
|
|
949
|
+
const prereqsMet = (id) => (REQUIRES[id] ?? []).every((p) => cleared.has(p));
|
|
950
|
+
const missing = (id) => (REQUIRES[id] ?? []).filter((p) => !cleared.has(p));
|
|
951
|
+
const titleOf = (id) => chapterById.get(id)?.title ?? id;
|
|
952
|
+
const pool = objective
|
|
953
|
+
? rankChapters(objective, mode, 20)
|
|
954
|
+
: chapterUnits
|
|
955
|
+
.filter((u) => chapterMatchesMode(u, mode))
|
|
956
|
+
.sort((a, b) => (orderOf.get(a.uri) ?? 0) - (orderOf.get(b.uri) ?? 0));
|
|
957
|
+
const fresh = pool.filter((u) => !cleared.has(u.id));
|
|
958
|
+
const ready = fresh.filter((u) => prereqsMet(u.id)).slice(0, 3);
|
|
959
|
+
const blocked = fresh.filter((u) => !prereqsMet(u.id)).slice(0, 3);
|
|
960
|
+
if (ready.length === 0 && blocked.length === 0) {
|
|
961
|
+
return text(objective
|
|
962
|
+
? `Rien de pertinent (et pas déjà franchi) pour « ${objective} ».`
|
|
963
|
+
: "Tous les gates pertinents sont franchis.");
|
|
964
|
+
}
|
|
965
|
+
const readyBlock = ready
|
|
966
|
+
.map((u, i) => {
|
|
967
|
+
const n = (REQUIRES[u.id] ?? []).length;
|
|
968
|
+
return (`${i === 0 ? "Prochain gate" : "Puis"} : ${u.title}\n` +
|
|
969
|
+
` Pourquoi : ${n ? `prérequis franchis (${n})` : "aucun prérequis"} ; pas encore fait.\n` +
|
|
970
|
+
(u.gate ? ` Artefact attendu : ${u.gate.artifact}\n Réussi quand : ${u.gate.criterion}` : ""));
|
|
971
|
+
})
|
|
972
|
+
.join("\n\n");
|
|
973
|
+
const blockedBlock = blocked.length
|
|
974
|
+
? `\n\nBloqués (prérequis manquants) :\n` +
|
|
975
|
+
blocked.map((u) => ` ⊘ ${u.title} — manque : ${missing(u.id).map(titleOf).join(", ")}`).join("\n")
|
|
976
|
+
: "";
|
|
977
|
+
return text((ready.length ? readyBlock : "Aucun gate débloqué pour l'instant.") + blockedBlock);
|
|
978
|
+
}
|
|
979
|
+
// ─── Connecteur dépôt local (étape connecteurs) ─────────────────────────────
|
|
980
|
+
function toolConnectRepo(args) {
|
|
981
|
+
const p = String(args.path ?? "").trim();
|
|
982
|
+
if (!p)
|
|
983
|
+
return refusal("connect_repo exige un chemin de dépôt local.");
|
|
984
|
+
const snap = readRepoSnapshot(p);
|
|
985
|
+
if ("error" in snap)
|
|
986
|
+
return refusal(snap.error);
|
|
987
|
+
const srcLine = Object.entries(snap.sources)
|
|
988
|
+
.map(([k, v]) => `${k}: ${v}`)
|
|
989
|
+
.join(" · ");
|
|
990
|
+
const absent = Object.entries(snap.sources)
|
|
991
|
+
.filter(([, v]) => v === "absent")
|
|
992
|
+
.map(([k]) => k);
|
|
993
|
+
return text(`Dépôt : ${snap.name ?? p}\n` +
|
|
994
|
+
`Stack : ${snap.framework.join(", ") || "indéterminée"} · ${snap.dependencies} dépendances\n` +
|
|
995
|
+
`Composants : ${snap.components.count}` +
|
|
996
|
+
(snap.components.dir ? ` dans ${snap.components.dir}` : "") +
|
|
997
|
+
(snap.components.sample.length ? ` (${snap.components.sample.join(", ")}…)` : "") +
|
|
998
|
+
`\nSources : ${srcLine}` +
|
|
999
|
+
(absent.length ? `\n\nÀ connecter pour un audit complet : ${absent.join(", ")}.` : "") +
|
|
1000
|
+
`\nLes sources « distant » (Figma, production) se lisent via leurs serveurs MCP dédiés, pas ici.`);
|
|
1001
|
+
}
|
|
1002
|
+
// ─── Serveur ─────────────────────────────────────────────────────────────────
|
|
1003
|
+
const server = new Server({ name: "designwithai-handbook", version: "0.1.0" }, { capabilities: { tools: {}, resources: {}, prompts: {} } });
|
|
1004
|
+
// Les arguments communs à tous les prompts-skills : de quoi renseigner l'en-tête
|
|
1005
|
+
// d'invocation (« Artefact / Objectif / Source / Contraintes / Mode »).
|
|
1006
|
+
const PROMPT_ARGS = [
|
|
1007
|
+
{
|
|
1008
|
+
name: "artifact",
|
|
1009
|
+
description: "L'artefact à auditer : lien, chemin de fichier, capture décrite ou périmètre.",
|
|
1010
|
+
required: false,
|
|
1011
|
+
},
|
|
1012
|
+
{ name: "objective", description: "Ce que la revue doit servir.", required: false },
|
|
1013
|
+
{
|
|
1014
|
+
name: "delivery_mode",
|
|
1015
|
+
description: "ai-builder | classique | hybride.",
|
|
1016
|
+
required: false,
|
|
1017
|
+
},
|
|
1018
|
+
{
|
|
1019
|
+
name: "source_of_truth",
|
|
1020
|
+
description: "Figma, code, Storybook, générateur IA, production…",
|
|
1021
|
+
required: false,
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
name: "constraints",
|
|
1025
|
+
description: "Contraintes à respecter (conserver X, ne pas modifier Y…).",
|
|
1026
|
+
required: false,
|
|
1027
|
+
},
|
|
1028
|
+
];
|
|
1029
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS }));
|
|
1030
|
+
server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
1031
|
+
const args = (req.params.arguments ?? {});
|
|
1032
|
+
switch (req.params.name) {
|
|
1033
|
+
case "search_handbook":
|
|
1034
|
+
return toolSearch(args);
|
|
1035
|
+
case "recommend_workflow":
|
|
1036
|
+
return toolRecommend(args);
|
|
1037
|
+
case "review_artifact":
|
|
1038
|
+
return toolReview(args);
|
|
1039
|
+
case "build_action_plan":
|
|
1040
|
+
return toolPlan(args);
|
|
1041
|
+
case "set_profile":
|
|
1042
|
+
return toolSetProfile(args);
|
|
1043
|
+
case "save_review":
|
|
1044
|
+
return toolSaveReview(args);
|
|
1045
|
+
case "list_reviews":
|
|
1046
|
+
return toolListReviews(args);
|
|
1047
|
+
case "push_audit":
|
|
1048
|
+
return toolPushAudit(args);
|
|
1049
|
+
case "compare_reviews":
|
|
1050
|
+
return toolCompare(args);
|
|
1051
|
+
case "clear_gate":
|
|
1052
|
+
return toolClearGate(args);
|
|
1053
|
+
case "reopen_gate":
|
|
1054
|
+
return toolReopenGate(args);
|
|
1055
|
+
case "gate_status":
|
|
1056
|
+
return toolGateStatus(args);
|
|
1057
|
+
case "audit_project":
|
|
1058
|
+
return toolAuditProject(args);
|
|
1059
|
+
case "list_eval_cases":
|
|
1060
|
+
return toolListEvalCases(args);
|
|
1061
|
+
case "get_eval_case":
|
|
1062
|
+
return toolGetEvalCase(args);
|
|
1063
|
+
case "score_review":
|
|
1064
|
+
return toolScoreReview(args);
|
|
1065
|
+
case "skill_scorecard":
|
|
1066
|
+
return toolScorecard(args);
|
|
1067
|
+
case "verify_fix":
|
|
1068
|
+
return toolVerifyFix(args);
|
|
1069
|
+
case "generate_decision_record":
|
|
1070
|
+
return toolGenerateDecision(args);
|
|
1071
|
+
case "next_gate":
|
|
1072
|
+
return toolNextGate(args);
|
|
1073
|
+
case "connect_repo":
|
|
1074
|
+
return toolConnectRepo(args);
|
|
1075
|
+
default:
|
|
1076
|
+
return refusal(`Outil inconnu : ${req.params.name}`);
|
|
1077
|
+
}
|
|
1078
|
+
});
|
|
1079
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
1080
|
+
resources: handbook.units.map((u) => ({
|
|
1081
|
+
uri: u.uri,
|
|
1082
|
+
name: `${u.kind} · ${u.title}`,
|
|
1083
|
+
description: u.summary || undefined,
|
|
1084
|
+
mimeType: "text/plain",
|
|
1085
|
+
})),
|
|
1086
|
+
}));
|
|
1087
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
1088
|
+
const u = byUri.get(req.params.uri);
|
|
1089
|
+
if (!u)
|
|
1090
|
+
throw new Error(`Ressource introuvable : ${req.params.uri}`);
|
|
1091
|
+
const header = `# ${u.title}\n${cite(u)}\n` +
|
|
1092
|
+
(u.gate ? `\nGate — artefact : ${u.gate.artifact}\ncritère : ${u.gate.criterion}\n` : "") +
|
|
1093
|
+
(u.delivery_modes?.length ? `Modes spécifiques : ${u.delivery_modes.join(", ")}\n` : "");
|
|
1094
|
+
return {
|
|
1095
|
+
contents: [{ uri: u.uri, mimeType: "text/plain", text: `${header}\n${u.text}` }],
|
|
1096
|
+
};
|
|
1097
|
+
});
|
|
1098
|
+
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
1099
|
+
prompts: skills.map((s) => ({
|
|
1100
|
+
name: s.name,
|
|
1101
|
+
description: s.description,
|
|
1102
|
+
arguments: PROMPT_ARGS,
|
|
1103
|
+
})),
|
|
1104
|
+
}));
|
|
1105
|
+
server.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
1106
|
+
const skill = skillByName.get(req.params.name);
|
|
1107
|
+
if (!skill)
|
|
1108
|
+
throw new Error(`Prompt inconnu : ${req.params.name}`);
|
|
1109
|
+
const a = (req.params.arguments ?? {});
|
|
1110
|
+
const invocation = [
|
|
1111
|
+
`/${skill.name}`,
|
|
1112
|
+
a.artifact ? `Artefact: ${a.artifact}` : null,
|
|
1113
|
+
a.objective ? `Objectif: ${a.objective}` : null,
|
|
1114
|
+
a.source_of_truth ? `Source de vérité: ${a.source_of_truth}` : null,
|
|
1115
|
+
a.constraints ? `Contraintes: ${a.constraints}` : null,
|
|
1116
|
+
a.delivery_mode ? `Mode de delivery: ${a.delivery_mode}` : null,
|
|
1117
|
+
]
|
|
1118
|
+
.filter(Boolean)
|
|
1119
|
+
.join("\n");
|
|
1120
|
+
// Le prompt est SELF-CONTAINED : socle commun + corps du skill + invocation.
|
|
1121
|
+
// Claude reçoit tout ce qu'il faut pour appliquer le skill, sans lire de fichier.
|
|
1122
|
+
const text = `Tu incarnes le skill **${skill.name}**${skill.role ? ` (${skill.role})` : ""}. ` +
|
|
1123
|
+
`Applique-le strictement : reste dans ton périmètre, exige des preuves, donne un ` +
|
|
1124
|
+
`critère d'acceptation par recommandation, et ne modifie rien sans demande explicite.\n\n` +
|
|
1125
|
+
`# Socle commun\n\n${sharedContract}\n\n` +
|
|
1126
|
+
`# Skill\n\n${skill.body}\n\n` +
|
|
1127
|
+
`# Invocation\n\n` +
|
|
1128
|
+
(invocation ||
|
|
1129
|
+
"(Aucun argument fourni — demande l'artefact et l'objectif avant d'auditer.)");
|
|
1130
|
+
return {
|
|
1131
|
+
description: skill.description,
|
|
1132
|
+
messages: [{ role: "user", content: { type: "text", text } }],
|
|
1133
|
+
};
|
|
1134
|
+
});
|
|
1135
|
+
await server.connect(new StdioServerTransport());
|
|
1136
|
+
process.stderr.write("[designwithai-mcp] prêt (stdio).\n");
|