designwithai-mcp 0.12.0 → 0.14.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/dist/cli.js +135 -0
- package/dist/craft-measures.js +134 -9
- package/dist/craft-report.js +50 -7
- package/dist/craft-store.js +76 -0
- package/dist/ds-inventory.js +81 -0
- package/dist/pr-delta.js +107 -0
- package/dist/registry-events.js +291 -0
- package/dist/registry-git.js +53 -0
- package/dist/registry-read.js +70 -0
- package/dist/server.js +178 -0
- package/dist/source-of-truth.js +9 -1
- package/dist/status-page.js +325 -0
- package/dist/status-update.js +174 -0
- package/dist/truth-store.js +58 -2
- package/package.json +3 -2
- package/templates/designwithai-status.yml +71 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LE MÊME MOTEUR, EN LIGNE DE COMMANDE.
|
|
3
|
+
*
|
|
4
|
+
* Une page d'état qui n'existe que dans un IDE ne sert pas l'équipe : elle sert
|
|
5
|
+
* la personne qui a l'IDE. La CI, elle, n'a pas de client MCP — elle a un shell.
|
|
6
|
+
* Ces sous-commandes exposent EXACTEMENT les mêmes fonctions de rendu que les
|
|
7
|
+
* outils MCP, sans en dupliquer une ligne. Une définition, un endroit : si la
|
|
8
|
+
* page change, elle change aux deux endroits parce qu'il n'y en a qu'un.
|
|
9
|
+
*
|
|
10
|
+
* Tout va sur la sortie standard, pour que `>` et `|` fassent leur travail.
|
|
11
|
+
*/
|
|
12
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { workspaceRoot } from "./paths.js";
|
|
15
|
+
import { lireRegistre } from "./registry-read.js";
|
|
16
|
+
import { rendreStatusPage } from "./status-page.js";
|
|
17
|
+
import { dernierUpdate, enregistrerUpdate, rendreStatusUpdate } from "./status-update.js";
|
|
18
|
+
import { delta, rendreDelta } from "./pr-delta.js";
|
|
19
|
+
import { lireReviewsA } from "./registry-git.js";
|
|
20
|
+
import { constats } from "./pr-delta.js";
|
|
21
|
+
export const SOUS_COMMANDES = ["status-page", "status-update", "pr-delta", "--help", "-h"];
|
|
22
|
+
const AIDE = `designwithai-mcp — le serveur MCP du Handbook, et trois rendus en ligne de commande.
|
|
23
|
+
|
|
24
|
+
designwithai-mcp démarre le serveur MCP (stdio)
|
|
25
|
+
|
|
26
|
+
designwithai-mcp status-page [--out F] [--product N]
|
|
27
|
+
Écrit la page d'état du produit, dérivée du registre.
|
|
28
|
+
Défaut : docs/etat-produit/index.html. --out - écrit sur la sortie standard.
|
|
29
|
+
|
|
30
|
+
designwithai-mcp status-update [--since ISO] [--product N] [--no-save]
|
|
31
|
+
Écrit le point d'avancement en Markdown sur la sortie standard.
|
|
32
|
+
Sans --since : depuis le dernier point enregistré.
|
|
33
|
+
|
|
34
|
+
designwithai-mcp pr-delta --base <ref> [--head <ref>] [--branch N]
|
|
35
|
+
Compare le registre de la base à celui du répertoire courant (ou de --head)
|
|
36
|
+
et rend le commentaire de PR. Suppose que .designwithai/ est VERSIONNÉ.
|
|
37
|
+
|
|
38
|
+
Le registre vit dans .designwithai/ du projet courant (DWAI_WORKSPACE pour l'imposer).`;
|
|
39
|
+
function option(argv, nom) {
|
|
40
|
+
const i = argv.indexOf(`--${nom}`);
|
|
41
|
+
if (i === -1)
|
|
42
|
+
return undefined;
|
|
43
|
+
const v = argv[i + 1];
|
|
44
|
+
return v && !v.startsWith("--") ? v : undefined;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Rend vrai quand une sous-commande a été traitée — le serveur ne doit alors
|
|
48
|
+
* PAS ouvrir stdio : un client MCP qui reçoit du Markdown sur stdout croit
|
|
49
|
+
* lire du protocole et se coupe sans message lisible.
|
|
50
|
+
*/
|
|
51
|
+
export async function traiterCli(argv) {
|
|
52
|
+
const cmd = argv[0];
|
|
53
|
+
if (!cmd)
|
|
54
|
+
return false;
|
|
55
|
+
if (cmd === "--help" || cmd === "-h") {
|
|
56
|
+
process.stdout.write(`${AIDE}\n`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
if (cmd === "status-page") {
|
|
60
|
+
const r = lireRegistre();
|
|
61
|
+
const produit = option(argv, "product");
|
|
62
|
+
const html = rendreStatusPage(r, {
|
|
63
|
+
maintenant: new Date().toISOString(),
|
|
64
|
+
...(produit ? { produit } : {}),
|
|
65
|
+
});
|
|
66
|
+
const out = option(argv, "out") ?? "docs/etat-produit/index.html";
|
|
67
|
+
if (out === "-") {
|
|
68
|
+
process.stdout.write(html);
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
const cible = path.resolve(workspaceRoot(), out);
|
|
72
|
+
mkdirSync(path.dirname(cible), { recursive: true });
|
|
73
|
+
writeFileSync(cible, html, "utf8");
|
|
74
|
+
process.stderr.write(`Page écrite : ${path.relative(workspaceRoot(), cible)}\n`);
|
|
75
|
+
/* Les fichiers illisibles vont sur stderr ET dans la page. Un registre à
|
|
76
|
+
moitié lu qui rendrait une page sereine serait le pire de cette surface. */
|
|
77
|
+
for (const i of r.illisibles)
|
|
78
|
+
process.stderr.write(`⚠ illisible : ${i.chemin} — ${i.raison}\n`);
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
if (cmd === "status-update") {
|
|
82
|
+
const r = lireRegistre();
|
|
83
|
+
const maintenant = new Date();
|
|
84
|
+
const depuis = option(argv, "since") ??
|
|
85
|
+
dernierUpdate() ??
|
|
86
|
+
new Date(maintenant.getTime() - 30 * 24 * 3600 * 1000).toISOString();
|
|
87
|
+
const produit = option(argv, "product");
|
|
88
|
+
const md = rendreStatusUpdate(r, {
|
|
89
|
+
depuis,
|
|
90
|
+
maintenant: maintenant.toISOString(),
|
|
91
|
+
...(produit ? { produit } : {}),
|
|
92
|
+
});
|
|
93
|
+
process.stdout.write(`${md}\n`);
|
|
94
|
+
if (!argv.includes("--no-save")) {
|
|
95
|
+
const chemin = enregistrerUpdate(md, maintenant);
|
|
96
|
+
process.stderr.write(`Point enregistré : ${path.relative(workspaceRoot(), chemin)}\n`);
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
if (cmd === "pr-delta") {
|
|
101
|
+
const base = option(argv, "base");
|
|
102
|
+
if (!base) {
|
|
103
|
+
process.stderr.write("pr-delta a besoin de --base <ref>.\n");
|
|
104
|
+
process.exitCode = 2;
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
const cwd = workspaceRoot();
|
|
108
|
+
let avant;
|
|
109
|
+
try {
|
|
110
|
+
avant = lireReviewsA(base, cwd);
|
|
111
|
+
}
|
|
112
|
+
catch (e) {
|
|
113
|
+
process.stderr.write(`${e instanceof Error ? e.message : String(e)}\n`);
|
|
114
|
+
process.exitCode = 2;
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
const tete = option(argv, "head");
|
|
118
|
+
const apres = tete ? lireReviewsA(tete, cwd).reviews : lireRegistre().reviews;
|
|
119
|
+
if (avant.absent) {
|
|
120
|
+
process.stdout.write(`${rendreDelta({ reglesProuves: [], declaresCorriges: [], nouveaux: [], deplaces: [], persistants: [] }, { base, baseSansRegistre: true })}\n`);
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
const d = delta(avant.reviews, apres);
|
|
124
|
+
const branche = option(argv, "branch");
|
|
125
|
+
process.stdout.write(`${rendreDelta(d, { ...(branche ? { branche } : {}), base })}\n`);
|
|
126
|
+
if (avant.illisibles.length > 0) {
|
|
127
|
+
process.stderr.write(`⚠ ${avant.illisibles.length} fichier(s) illisible(s) côté base : la comparaison est incomplète.\n`);
|
|
128
|
+
}
|
|
129
|
+
void constats;
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
process.stderr.write(`Sous-commande inconnue : ${cmd}\n\n${AIDE}\n`);
|
|
133
|
+
process.exitCode = 2;
|
|
134
|
+
return true;
|
|
135
|
+
}
|
package/dist/craft-measures.js
CHANGED
|
@@ -174,8 +174,30 @@ export const LARGEUR_DEFAUT_REM = {
|
|
|
174
174
|
* `h-auto`, `p-[7px]` ne se résolvent pas ici, et c'est très bien — ils ont
|
|
175
175
|
* chacun leur chemin.
|
|
176
176
|
*/
|
|
177
|
-
export function resoudrePasNomme(famille, pas, racine, theme = {}
|
|
177
|
+
export function resoudrePasNomme(famille, pas, racine, theme = {},
|
|
178
|
+
/**
|
|
179
|
+
* L'échelle typographique DÉCLARÉE, quand elle est énumérée.
|
|
180
|
+
*
|
|
181
|
+
* `--font-size-4` ne veut rien dire dans l'absolu : c'est le quatrième pas de
|
|
182
|
+
* l'échelle que l'équipe a choisie. Sans cette liste, la mesure ne peut que
|
|
183
|
+
* l'ignorer — et une échelle ignorée fait tomber le compte à zéro, ce qui
|
|
184
|
+
* ressemble à une victoire.
|
|
185
|
+
*/
|
|
186
|
+
echelleDeclaree,
|
|
187
|
+
/** L'échelle de rayons déclarée. Même rôle, même raison. */
|
|
188
|
+
rayonsDeclares) {
|
|
178
189
|
const cle = pas.toLowerCase();
|
|
190
|
+
/* Un pas du kit : `font-size-1` est le PREMIER de la liste, pas l'index 1. */
|
|
191
|
+
const kitTexte = cle.match(/^font-size-(\d+)$/);
|
|
192
|
+
if (kitTexte && echelleDeclaree?.length) {
|
|
193
|
+
const px = echelleDeclaree[Number(kitTexte[1]) - 1];
|
|
194
|
+
return px !== undefined ? { px, provenance: "theme" } : null;
|
|
195
|
+
}
|
|
196
|
+
const kitRayon = cle.match(/^radius-(\d+)$/);
|
|
197
|
+
if (kitRayon && rayonsDeclares?.length) {
|
|
198
|
+
const px = rayonsDeclares[Number(kitRayon[1]) - 1];
|
|
199
|
+
return px !== undefined ? { px, provenance: "theme" } : null;
|
|
200
|
+
}
|
|
179
201
|
if (famille === "espacement") {
|
|
180
202
|
if (cle in PAS_LITTERAUX) {
|
|
181
203
|
const v = PAS_LITTERAUX[cle];
|
|
@@ -242,9 +264,76 @@ function lireTheme(items) {
|
|
|
242
264
|
* C'est ce qui permet d'ajouter une unité ou une échelle sans rouvrir les huit
|
|
243
265
|
* fonctions de mesure — et ce qui garantit que toutes convertissent pareil.
|
|
244
266
|
*/
|
|
245
|
-
|
|
267
|
+
/**
|
|
268
|
+
* LA TABLE DES ALIAS — `--radius-carte` vaut `var(--radius-4)` vaut 8 pixels.
|
|
269
|
+
*
|
|
270
|
+
* Un dépôt qui a fini sa migration n'écrit plus de valeurs : il écrit des noms
|
|
271
|
+
* qui pointent sur des noms. Sans suivre la chaîne, la mesure voit
|
|
272
|
+
* `var(--radius-carte)`, n'en tire rien, et rend « 0 rayon distinct » sur un
|
|
273
|
+
* produit qui en emploie six.
|
|
274
|
+
*
|
|
275
|
+
* ELLE NE REND JAMAIS « CONFORME » CE QU'ELLE NE SAIT PAS RÉSOUDRE. Une chaîne
|
|
276
|
+
* qui n'aboutit pas ressort en illisible AVEC la chaîne citée — c'est ce qui
|
|
277
|
+
* distingue « 0 divergent sur 13 résolus » de « 0 divergent parce que 13
|
|
278
|
+
* invisibles ». La deuxième phrase est un mensonge qui a l'air d'un succès.
|
|
279
|
+
*/
|
|
280
|
+
function tableDesAlias(brut) {
|
|
281
|
+
const t = new Map();
|
|
282
|
+
for (const it of brut) {
|
|
283
|
+
if (it.kind !== "token")
|
|
284
|
+
continue;
|
|
285
|
+
const v = (it.value ?? "").trim();
|
|
286
|
+
if (!v)
|
|
287
|
+
continue;
|
|
288
|
+
/* L'inventaire range les variables CSS SANS leurs deux tirets — `radius-sm`
|
|
289
|
+
et non `--radius-sm`. Chercher la forme écrite dans le code ne trouvait
|
|
290
|
+
donc jamais rien, et toute la table restait vide : la chaîne d'alias
|
|
291
|
+
échouait au premier maillon sans qu'on voie pourquoi. On indexe les deux
|
|
292
|
+
formes plutôt que d'en imposer une : les deux existent dans le dépôt. */
|
|
293
|
+
if (it.name.startsWith("--"))
|
|
294
|
+
t.set(it.name, v);
|
|
295
|
+
else if ((it.type ?? "") === "css-variable")
|
|
296
|
+
t.set(`--${it.name}`, v);
|
|
297
|
+
}
|
|
298
|
+
return t;
|
|
299
|
+
}
|
|
300
|
+
/** Le maillon final d'une chaîne, ou la chaîne parcourue si elle n'aboutit pas. */
|
|
301
|
+
export function suivreAlias(valeur, alias, pasKit, profondeurMax = 8) {
|
|
302
|
+
const chaine = [valeur];
|
|
303
|
+
let courant = valeur.trim();
|
|
304
|
+
for (let i = 0; i < profondeurMax; i++) {
|
|
305
|
+
const m = courant.match(/^var\(\s*(--[a-zA-Z0-9-]+)\s*(?:,[^)]*)?\)$/);
|
|
306
|
+
if (!m)
|
|
307
|
+
break;
|
|
308
|
+
const nom = m[1];
|
|
309
|
+
/* Un pas du kit ne se trouve pas dans le fichier : il vient de l'échelle
|
|
310
|
+
DÉCLARÉE. C'est le bout de la chaîne, et le seul endroit où elle peut
|
|
311
|
+
aboutir sur un nombre sans que ce nombre soit écrit quelque part. */
|
|
312
|
+
const kit = pasKit(nom);
|
|
313
|
+
if (kit !== null)
|
|
314
|
+
return { px: kit };
|
|
315
|
+
const suivant = alias.get(nom);
|
|
316
|
+
if (suivant === undefined)
|
|
317
|
+
return { chaine };
|
|
318
|
+
courant = suivant.trim();
|
|
319
|
+
chaine.push(courant);
|
|
320
|
+
}
|
|
321
|
+
const px = brutEnPixels(courant, RACINE_PAR_DEFAUT);
|
|
322
|
+
return px === null ? { chaine } : { px };
|
|
323
|
+
}
|
|
324
|
+
export function resoudreItems(brut, echelleDeclaree, rayonsDeclares) {
|
|
246
325
|
const racine = resoudreRacine(brut);
|
|
247
326
|
const theme = lireTheme(brut);
|
|
327
|
+
const alias = tableDesAlias(brut);
|
|
328
|
+
const pasKit = (nom) => {
|
|
329
|
+
const r = nom.match(/^--radius-(\d+)$/);
|
|
330
|
+
if (r && rayonsDeclares?.length)
|
|
331
|
+
return rayonsDeclares[Number(r[1]) - 1] ?? null;
|
|
332
|
+
const t = nom.match(/^--font-size-(\d+)$/);
|
|
333
|
+
if (t && echelleDeclaree?.length)
|
|
334
|
+
return echelleDeclaree[Number(t[1]) - 1] ?? null;
|
|
335
|
+
return null;
|
|
336
|
+
};
|
|
248
337
|
const items = [];
|
|
249
338
|
const illisibles = [];
|
|
250
339
|
const em = [];
|
|
@@ -276,7 +365,7 @@ export function resoudreItems(brut) {
|
|
|
276
365
|
/* Un pas nommé : sa valeur ne se lit pas, elle se résout. */
|
|
277
366
|
if (it.attrs?.provenance === "nomme") {
|
|
278
367
|
const famille = (it.attrs.famille ?? type);
|
|
279
|
-
const r = resoudrePasNomme(famille, it.attrs.pas ?? "", racine.px, theme);
|
|
368
|
+
const r = resoudrePasNomme(famille, it.attrs.pas ?? "", racine.px, theme, echelleDeclaree, rayonsDeclares);
|
|
280
369
|
if (!r) {
|
|
281
370
|
illisibles.push(`${it.name}${it.location ? ` (${it.location})` : ""} — pas nommé inconnu de l'échelle`);
|
|
282
371
|
continue;
|
|
@@ -290,7 +379,19 @@ export function resoudreItems(brut) {
|
|
|
290
379
|
em.push(`${it.name} = ${v}${it.location ? ` (${it.location})` : ""}`);
|
|
291
380
|
continue;
|
|
292
381
|
}
|
|
293
|
-
|
|
382
|
+
let px = brutEnPixels(v, racine.px);
|
|
383
|
+
if (px === null && v.startsWith("var(")) {
|
|
384
|
+
const suivi = suivreAlias(v, alias, pasKit);
|
|
385
|
+
if ("px" in suivi) {
|
|
386
|
+
px = suivi.px;
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
/* La chaîne est CITÉE : sans elle, « illisible » n'aide personne à
|
|
390
|
+
réparer, et le compte des non-résolues resterait un chiffre nu. */
|
|
391
|
+
illisibles.push(`${it.name}${it.location ? ` (${it.location})` : ""} — chaîne non résolue : ${suivi.chaine.join(" → ")}`);
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
}
|
|
294
395
|
if (px === null) {
|
|
295
396
|
illisibles.push(`${it.name} = « ${v || "(vide)"} »${it.location ? ` (${it.location})` : ""}`);
|
|
296
397
|
continue;
|
|
@@ -462,7 +563,7 @@ export const TOLERANCE_TYPO = 1;
|
|
|
462
563
|
* relie deux. En dessous de trois, la mesure le DIT au lieu de rendre un
|
|
463
564
|
* chiffre qui ne veut rien dire.
|
|
464
565
|
*/
|
|
465
|
-
export function echelleTypo(items) {
|
|
566
|
+
export function echelleTypo(items, systeme) {
|
|
466
567
|
const nonMesures = [];
|
|
467
568
|
const vues = new Set();
|
|
468
569
|
for (const it of items) {
|
|
@@ -476,11 +577,33 @@ export function echelleTypo(items) {
|
|
|
476
577
|
vues.add(px);
|
|
477
578
|
}
|
|
478
579
|
const tailles = [...vues].sort((a, b) => a - b);
|
|
479
|
-
|
|
580
|
+
/* L'ÉCHELLE ÉNUMÉRÉE PASSE AVANT TOUT LE RESTE, Y COMPRIS AVANT LE SEUIL DE
|
|
581
|
+
TROIS TAILLES. Ce seuil existe parce qu'on ne DÉDUIT rien de deux valeurs ;
|
|
582
|
+
ici on ne déduit pas, on compare à une liste écrite. Une équipe qui déclare
|
|
583
|
+
son échelle a droit à sa mesure même sur un fichier qui n'en emploie
|
|
584
|
+
qu'une. */
|
|
585
|
+
const declarees = (systeme?.taillesTypo ?? []).filter((n) => Number.isFinite(n) && n > 0);
|
|
586
|
+
if (declarees.length > 0) {
|
|
587
|
+
const attendues = [...new Set(declarees)].sort((a, b) => a - b);
|
|
588
|
+
/* Conformité = APPARTENANCE. Pas de tolérance : une liste ne se discute
|
|
589
|
+
pas, et « à un pixel près d'un pas déclaré » n'est pas le pas déclaré. */
|
|
590
|
+
const dansLaListe = new Set(attendues);
|
|
591
|
+
const orphelines = tailles.filter((t) => !dansLaListe.has(t));
|
|
592
|
+
return {
|
|
593
|
+
valeur: { ratio: null, ecartMoyen: null },
|
|
594
|
+
detail: { tailles, attendues, orphelines, essais: [], origine: "énumérée" },
|
|
595
|
+
completude: { lus: tailles.length, nonMesures },
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/* UN RATIO DÉCLARÉ N'EST PAS UN RATIO À DEVINER. Tant que ce paramètre
|
|
599
|
+
n'existait pas, `ratioTypo` se déclarait et ne servait à rien : la mesure
|
|
600
|
+
cherchait le ratio le moins faux et ignorait celui de l'équipe. */
|
|
601
|
+
const ratioDeclare = systeme?.ratioTypo && systeme.ratioTypo > 1 && systeme.ratioTypo <= 3 ? systeme.ratioTypo : null;
|
|
602
|
+
if (tailles.length < 3 && !ratioDeclare) {
|
|
480
603
|
nonMesures.push(`${tailles.length} taille(s) distincte(s) : en dessous de trois, n'importe quel ratio les relie — aucune échelle n'est déductible.`);
|
|
481
604
|
return {
|
|
482
605
|
valeur: { ratio: null, ecartMoyen: null },
|
|
483
|
-
detail: { tailles, attendues: [], orphelines: [], essais: [] },
|
|
606
|
+
detail: { tailles, attendues: [], orphelines: [], essais: [], origine: "déduite" },
|
|
484
607
|
completude: { lus: tailles.length, nonMesures },
|
|
485
608
|
};
|
|
486
609
|
}
|
|
@@ -493,14 +616,16 @@ export function echelleTypo(items) {
|
|
|
493
616
|
const ecart = tailles.reduce((somme, t) => somme + Math.min(...pas.map((p) => Math.abs(p - t))), 0) / tailles.length;
|
|
494
617
|
return { ratio, ecart: Math.round(ecart * 1000) / 1000 };
|
|
495
618
|
}).sort((a, b) => a.ecart - b.ecart);
|
|
496
|
-
const gagnant =
|
|
619
|
+
const gagnant = ratioDeclare
|
|
620
|
+
? (essais.find((e) => e.ratio === ratioDeclare) ?? { ratio: ratioDeclare, ecart: 0 })
|
|
621
|
+
: essais[0];
|
|
497
622
|
const attendues = [];
|
|
498
623
|
for (let v = base; v <= max * gagnant.ratio; v *= gagnant.ratio)
|
|
499
624
|
attendues.push(Math.round(v * 10) / 10);
|
|
500
625
|
const orphelines = tailles.filter((t) => Math.min(...attendues.map((p) => Math.abs(p - t))) > TOLERANCE_TYPO);
|
|
501
626
|
return {
|
|
502
627
|
valeur: { ratio: gagnant.ratio, ecartMoyen: gagnant.ecart },
|
|
503
|
-
detail: { tailles, attendues, orphelines, essais },
|
|
628
|
+
detail: { tailles, attendues, orphelines, essais, origine: ratioDeclare ? "ratio déclaré" : "déduite" },
|
|
504
629
|
completude: { lus: tailles.length, nonMesures },
|
|
505
630
|
};
|
|
506
631
|
}
|
package/dist/craft-report.js
CHANGED
|
@@ -31,18 +31,30 @@ export function craftReport(brut, opts = {}) {
|
|
|
31
31
|
/* UNE SEULE PASSE DE RÉSOLUTION, avant tout. Les mesures qui suivent ne
|
|
32
32
|
voient plus que des pixels : la racine, le thème et l'échelle du cadre ont
|
|
33
33
|
déjà fait leur travail, et de la même façon pour toutes. */
|
|
34
|
-
const resolution = resoudreItems(brut);
|
|
34
|
+
const resolution = resoudreItems(brut, opts.systeme?.taillesTypo, opts.systeme?.rayons);
|
|
35
35
|
const items = resolution.items;
|
|
36
36
|
const reference = referenceEspacement(items, opts.systeme);
|
|
37
37
|
const refCibles = referenceCible(opts.systeme);
|
|
38
38
|
const grille = conformiteGrille(items, reference.base);
|
|
39
39
|
const pas = distinctes(items, estEspacement);
|
|
40
40
|
const coherence = echelleCoherente(pas.detail, reference.base);
|
|
41
|
-
const typo = echelleTypo(items);
|
|
41
|
+
const typo = echelleTypo(items, opts.systeme);
|
|
42
42
|
const tailles = distinctes(items, estTailleTypo);
|
|
43
43
|
const doublons = quasiDoublonsCouleur(items);
|
|
44
44
|
const couleurs = new Set(items.filter(estCouleur).map((i) => (i.value ?? "").trim().toLowerCase()));
|
|
45
|
-
|
|
45
|
+
/* L'EXCLUSION S'APPLIQUE ICI, ET ELLE SE COMPTE.
|
|
46
|
+
On ne retire pas des éléments en silence : on les met de côté, on compte
|
|
47
|
+
ce qu'on a mis de côté, et le rapport le dit fichier par fichier. Une
|
|
48
|
+
mesure qui rétrécit sans le dire ment plus efficacement qu'une mesure
|
|
49
|
+
fausse — elle a l'air juste. */
|
|
50
|
+
const exclusions = opts.systeme?.exclusions ?? [];
|
|
51
|
+
const estExclu = (it) => exclusions.some((e) => (it.location ?? "").includes(e.fichier));
|
|
52
|
+
const ecartes = exclusions.map((e) => ({
|
|
53
|
+
fichier: e.fichier,
|
|
54
|
+
raison: e.raison,
|
|
55
|
+
valeurs: items.filter((it) => estRayon(it) && (it.location ?? "").includes(e.fichier)).length,
|
|
56
|
+
}));
|
|
57
|
+
const rayons = distinctes(items.filter((it) => !estExclu(it)), estRayon);
|
|
46
58
|
const bp = bascules(items);
|
|
47
59
|
const plafonds = largeursMax(items);
|
|
48
60
|
const cibles = opts.annotations ? ciblesTactiles(opts.annotations, refCibles.base) : null;
|
|
@@ -71,6 +83,8 @@ export function craftReport(brut, opts = {}) {
|
|
|
71
83
|
ratio: typo.valeur.ratio,
|
|
72
84
|
ecartMoyen: typo.valeur.ecartMoyen,
|
|
73
85
|
tailles: typo.detail.tailles,
|
|
86
|
+
attendues: typo.detail.attendues,
|
|
87
|
+
origine: typo.detail.origine,
|
|
74
88
|
orphelines: typo.detail.orphelines,
|
|
75
89
|
nonMesures: [...typo.completude.nonMesures, ...tailles.completude.nonMesures],
|
|
76
90
|
},
|
|
@@ -80,7 +94,12 @@ export function craftReport(brut, opts = {}) {
|
|
|
80
94
|
paires: doublons.detail.paires.map((p) => ({ a: p.a, b: p.b, ecart: p.ecart })),
|
|
81
95
|
nonMesures: doublons.completude.nonMesures,
|
|
82
96
|
},
|
|
83
|
-
rayons: {
|
|
97
|
+
rayons: {
|
|
98
|
+
distincts: rayons.valeur,
|
|
99
|
+
liste: rayons.detail,
|
|
100
|
+
nonMesures: rayons.completude.nonMesures,
|
|
101
|
+
ecartes: ecartes.filter((e) => e.valeurs > 0),
|
|
102
|
+
},
|
|
84
103
|
grille: {
|
|
85
104
|
bascules: bp.detail.trouvees,
|
|
86
105
|
horsPlages: bp.detail.horsPlages,
|
|
@@ -193,13 +212,30 @@ export function rendreCraftReport(r, opts = {}) {
|
|
|
193
212
|
l.push("");
|
|
194
213
|
/* ── Typographie ── */
|
|
195
214
|
l.push("LES TAILLES DE TEXTE");
|
|
196
|
-
|
|
215
|
+
/* TROIS PHRASES, PARCE QU'IL Y A TROIS SITUATIONS — et les confondre revient
|
|
216
|
+
à opposer une supposition à une décision. « déduite » doit se lire comme
|
|
217
|
+
déduite : c'est le seul cas où l'échelle vient de nous et pas de l'équipe. */
|
|
218
|
+
if (r.typo.origine === "énumérée") {
|
|
219
|
+
l.push(` ${r.typo.tailles.length} taille(s) employée(s) : ${r.typo.tailles.join(", ")}.` + cite("TYPO-02"));
|
|
220
|
+
l.push(` Mesuré contre l'échelle ÉNUMÉRÉE que l'équipe a déclarée — ${r.typo.attendues.join(", ")}. ` +
|
|
221
|
+
"Aucun ratio n'est cherché : la conformité, ici, c'est d'être dans la liste." + cite("TYPO-01"));
|
|
222
|
+
if (r.typo.orphelines.length > 0) {
|
|
223
|
+
l.push(` ${r.typo.orphelines.length} taille(s) hors de l'échelle déclarée : ${r.typo.orphelines.join(", ")}.`);
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
l.push(" Toutes tombent sur un pas déclaré.");
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (r.typo.ratio === null) {
|
|
197
230
|
l.push(` Pas assez de tailles distinctes pour déduire une échelle (${r.typo.tailles.length}).` + cite("TYPO-01"));
|
|
198
231
|
}
|
|
199
232
|
else {
|
|
200
233
|
l.push(` ${r.typo.tailles.length} taille(s) : ${r.typo.tailles.join(", ")}.` + cite("TYPO-02"));
|
|
201
|
-
l.push(
|
|
202
|
-
`
|
|
234
|
+
l.push(r.typo.origine === "ratio déclaré"
|
|
235
|
+
? ` Mesuré contre le ratio de ${r.typo.ratio} que l'équipe a déclaré ` +
|
|
236
|
+
`(écart moyen ${r.typo.ecartMoyen} pixel).` + cite("TYPO-01")
|
|
237
|
+
: ` Aucune échelle déclarée : celle qui les explique le mieux est un ratio de ${r.typo.ratio} ` +
|
|
238
|
+
`(écart moyen ${r.typo.ecartMoyen} pixel). C'est une déduction, pas une décision.` + cite("TYPO-01"));
|
|
203
239
|
if (r.typo.orphelines.length > 0) {
|
|
204
240
|
l.push(` ${r.typo.orphelines.length} taille(s) qu'aucun pas n'explique : ${r.typo.orphelines.join(", ")}.`);
|
|
205
241
|
}
|
|
@@ -221,6 +257,13 @@ export function rendreCraftReport(r, opts = {}) {
|
|
|
221
257
|
l.push(" Aucune paire trop proche pour être distinguée.");
|
|
222
258
|
}
|
|
223
259
|
l.push(` ${r.rayons.distincts} rayon(s) distinct(s)${r.rayons.liste.length ? ` : ${r.rayons.liste.join(", ")}` : ""}.` + cite("RADIUS-01"));
|
|
260
|
+
/* CE BLOC N'EST PAS FACULTATIF. Un chiffre obtenu en écartant des valeurs ne
|
|
261
|
+
vaut que si le lecteur sait lesquelles, combien, et pourquoi — sinon il
|
|
262
|
+
compare une mesure rétrécie à une mesure pleine sans le savoir. */
|
|
263
|
+
for (const e of r.rayons.ecartes) {
|
|
264
|
+
l.push(` 1 fichier écarté de ce compte : ${e.fichier}, ${e.valeurs} valeur(s).`);
|
|
265
|
+
l.push(` Raison déclarée : ${e.raison}`);
|
|
266
|
+
}
|
|
224
267
|
l.push("");
|
|
225
268
|
/* ── Grilles ── */
|
|
226
269
|
l.push("LES BASCULES ET LES PLAFONDS");
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LES INSTANTANÉS DE MESURE — sans historique, pas de tendance.
|
|
3
|
+
*
|
|
4
|
+
* `craft_report` calculait et oubliait. Une conformité de 97 % ne dit rien toute
|
|
5
|
+
* seule : c'est « 94 puis 97 » qui raconte quelque chose, et « 97 puis 91 » qui
|
|
6
|
+
* réveille quelqu'un. On garde donc chaque instantané, horodaté, dans le projet.
|
|
7
|
+
*
|
|
8
|
+
* Ce qu'on garde est VOLONTAIREMENT MAIGRE : les comptes, pas les listes. Un
|
|
9
|
+
* instantané n'a pas à rejouer le rapport — il sert à dire ce qui a bougé, et
|
|
10
|
+
* la liste exhaustive se recalcule en une seconde depuis le code courant.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
13
|
+
import path from "node:path";
|
|
14
|
+
import { workspaceDir } from "./paths.js";
|
|
15
|
+
export function craftDir() {
|
|
16
|
+
return process.env.DWAI_CRAFT_PATH ?? workspaceDir("craft");
|
|
17
|
+
}
|
|
18
|
+
export function horodatage(now) {
|
|
19
|
+
return now.toISOString().replace(/[:.]/g, "-").replace("T", "_").replace(/Z$/, "").slice(0, 16);
|
|
20
|
+
}
|
|
21
|
+
export function depuisRapport(r, now) {
|
|
22
|
+
return {
|
|
23
|
+
at: now.toISOString(),
|
|
24
|
+
reference: { niveau: r.reference.niveau, base: r.reference.base, contre: r.reference.contre },
|
|
25
|
+
espace: {
|
|
26
|
+
total: r.espace.total,
|
|
27
|
+
conformes: r.espace.conformes,
|
|
28
|
+
pct: Math.round(r.espace.pct * 1000) / 1000,
|
|
29
|
+
divergents: r.espace.divergents.length,
|
|
30
|
+
pas: r.espace.pas,
|
|
31
|
+
},
|
|
32
|
+
typo: { tailles: r.typo.tailles.length, ratio: r.typo.ratio, orphelines: r.typo.orphelines.length },
|
|
33
|
+
couleurs: { distinctes: r.couleurs.distinctes, doublons: r.couleurs.doublons },
|
|
34
|
+
rayons: r.rayons.distincts,
|
|
35
|
+
grille: {
|
|
36
|
+
bascules: r.grille.bascules.length,
|
|
37
|
+
horsPlages: r.grille.horsPlages.length,
|
|
38
|
+
plafonds: r.grille.plafonds,
|
|
39
|
+
},
|
|
40
|
+
lecture: { rencontrees: r.lecture.rencontrees, lues: r.lecture.lues },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
export function enregistrerInstantane(i, now) {
|
|
44
|
+
const dir = craftDir();
|
|
45
|
+
mkdirSync(dir, { recursive: true });
|
|
46
|
+
const chemin = path.join(dir, `${horodatage(now)}.json`);
|
|
47
|
+
writeFileSync(chemin, `${JSON.stringify(i, null, 2)}\n`);
|
|
48
|
+
return chemin;
|
|
49
|
+
}
|
|
50
|
+
/** Tous les instantanés, du plus ancien au plus récent. Un abîmé disparaît. */
|
|
51
|
+
export function lireInstantanes() {
|
|
52
|
+
const dir = craftDir();
|
|
53
|
+
if (!existsSync(dir))
|
|
54
|
+
return [];
|
|
55
|
+
const out = [];
|
|
56
|
+
for (const f of readdirSync(dir).filter((x) => x.endsWith(".json")).sort()) {
|
|
57
|
+
try {
|
|
58
|
+
const o = JSON.parse(readFileSync(path.join(dir, f), "utf8"));
|
|
59
|
+
if (o && typeof o.at === "string" && o.espace)
|
|
60
|
+
out.push(o);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
/* Un instantané illisible n'est pas une mesure. Le fil s'en charge : c'est
|
|
64
|
+
lui qui rend l'événement « illisible », pas ce lecteur. */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return out;
|
|
68
|
+
}
|
|
69
|
+
/** Le dernier et l'avant-dernier — de quoi dire ce qui a bougé, et rien de plus. */
|
|
70
|
+
export function tendance() {
|
|
71
|
+
const tous = lireInstantanes();
|
|
72
|
+
return {
|
|
73
|
+
dernier: tous[tous.length - 1] ?? null,
|
|
74
|
+
precedent: tous[tous.length - 2] ?? null,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/dist/ds-inventory.js
CHANGED
|
@@ -38,9 +38,27 @@ export const SAIT_LIRE_STORIES = [
|
|
|
38
38
|
"le composant documenté (dernier segment du `title`, ou nom du fichier)",
|
|
39
39
|
"les états documentés (un export nommé par story)",
|
|
40
40
|
];
|
|
41
|
+
/**
|
|
42
|
+
* CE QU'ON NE LIT PAS — et la fois où cette liste a menti.
|
|
43
|
+
*
|
|
44
|
+
* En faisant tourner la mesure sur NOTRE propre dépôt pour remplir le registre,
|
|
45
|
+
* elle est revenue à 100 % de conformité d'espacement. Le réflexe est écrit
|
|
46
|
+
* quelque part depuis le premier jour : à 100 %, c'est la mesure qui est
|
|
47
|
+
* fausse, pas nous qui sommes parfaits. Elle lisait le dossier `backups` — des
|
|
48
|
+
* copies de builds de production — et y trouvait la feuille CSS compilée de
|
|
49
|
+
* Tailwind. Les variables internes de Tailwind et son échelle nommée y tombent
|
|
50
|
+
* évidemment sur la grille : ils SONT la grille. On mesurait le générateur au
|
|
51
|
+
* lieu de mesurer l'usage, et on aurait archivé ce chiffre-là.
|
|
52
|
+
*
|
|
53
|
+
* `out`, `dist`, `build` et `.next` étaient déjà là ; il manquait les
|
|
54
|
+
* conventions de sauvegarde et le dossier de travail de ce produit. Aucune de
|
|
55
|
+
* ces entrées n'est un nom propre : ce sont des conventions, et un dépôt qui
|
|
56
|
+
* y range de la source a de plus gros soucis que sa grille.
|
|
57
|
+
*/
|
|
41
58
|
const IGNORE = new Set([
|
|
42
59
|
"node_modules", ".git", ".next", "dist", "build", "out", "coverage",
|
|
43
60
|
".turbo", ".vercel", "vendor", "__snapshots__",
|
|
61
|
+
"backups", "backup", ".backups", "_next", ".designwithai", ".cache", "tmp",
|
|
44
62
|
]);
|
|
45
63
|
/** Plafonds de parcours : un dépôt réel ne se lit pas entièrement à chaud. */
|
|
46
64
|
const MAX_FICHIERS = 4000;
|
|
@@ -173,6 +191,30 @@ function lireTaillesTexte(etat, contenu, fichier) {
|
|
|
173
191
|
const vus = new Set();
|
|
174
192
|
for (const m of contenu.matchAll(/font-size\s*:\s*([^;}]+)/gi)) {
|
|
175
193
|
const brut = m[1].trim();
|
|
194
|
+
/* UN PAS DU KIT SE RÉSOUT, IL NE SE JETTE PAS.
|
|
195
|
+
`font-size: var(--font-size-4)` partait dans `unread` avec les `clamp()`,
|
|
196
|
+
et les deux n'ont rien à voir. Un clamp dépend de la largeur de la
|
|
197
|
+
fenêtre : prendre une de ses bornes ferait entrer dans l'échelle un
|
|
198
|
+
chiffre que personne n'a choisi. Un pas nommé, lui, désigne une valeur
|
|
199
|
+
précise — celle que l'équipe a déclarée dans `taillesTypo`.
|
|
200
|
+
Tant qu'on les traitait pareil, migrer les valeurs sauvages vers
|
|
201
|
+
l'échelle faisait tomber le compte à zéro : la mesure devenait aveugle,
|
|
202
|
+
et le zéro ressemblait à une victoire. */
|
|
203
|
+
const pas = brut.match(/^var\(\s*--font-size-(\d+)\s*\)$/i);
|
|
204
|
+
if (pas) {
|
|
205
|
+
const cle = `font-size-${pas[1]}`;
|
|
206
|
+
if (vus.has(cle))
|
|
207
|
+
continue;
|
|
208
|
+
vus.add(cle);
|
|
209
|
+
etat.items.push({
|
|
210
|
+
kind: "token",
|
|
211
|
+
name: cle,
|
|
212
|
+
type: "typographie",
|
|
213
|
+
location: fichier,
|
|
214
|
+
attrs: { provenance: "nomme", famille: "typographie", pas: cle },
|
|
215
|
+
});
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
176
218
|
const px = brut.match(/^([\d.]+)px$/i);
|
|
177
219
|
if (!px) {
|
|
178
220
|
if (/clamp|calc|var\(/i.test(brut)) {
|
|
@@ -392,8 +434,47 @@ function lireValeursArbitraires(etat, contenu, fichier) {
|
|
|
392
434
|
attrs: { provenance: "arbitraire" },
|
|
393
435
|
});
|
|
394
436
|
}
|
|
437
|
+
lireVariablesEnClasse(etat, contenu, fichier);
|
|
395
438
|
lirePasNommes(etat, contenu, fichier);
|
|
396
439
|
}
|
|
440
|
+
/**
|
|
441
|
+
* LES VARIABLES ÉCRITES DANS UNE CLASSE — `rounded-[var(--radius-carte)]`.
|
|
442
|
+
*
|
|
443
|
+
* C'est la forme que prend un dépôt qui a fini sa migration : plus une valeur
|
|
444
|
+
* en pixels dans le markup, rien que des noms. Tant qu'on ne la lisait pas, la
|
|
445
|
+
* mesure devenait AVEUGLE au moment précis où le travail était fait — elle
|
|
446
|
+
* rendait « 0 rayon distinct » sur un produit qui en emploie six, et le zéro
|
|
447
|
+
* ressemblait à une victoire.
|
|
448
|
+
*
|
|
449
|
+
* On note le nom ; la résolution se fait dans `craft-measures`, qui connaît la
|
|
450
|
+
* chaîne des alias et l'échelle déclarée. L'inventaire rapporte, il ne calcule
|
|
451
|
+
* pas.
|
|
452
|
+
*/
|
|
453
|
+
function lireVariablesEnClasse(etat, contenu, fichier) {
|
|
454
|
+
const vus = new Set();
|
|
455
|
+
for (const m of contenu.matchAll(/\b([a-z]+(?:-[a-z]+)?)-\[var\(\s*(--[a-z0-9-]+)\s*\)\]/gi)) {
|
|
456
|
+
const type = PREFIXE_TYPE.find(([re]) => re.test(m[1]))?.[1];
|
|
457
|
+
if (!type)
|
|
458
|
+
continue;
|
|
459
|
+
/* `text-[var(--color-…)]` est une COULEUR, pas une taille. Le préfixe est
|
|
460
|
+
le même — Tailwind s'en sert pour les deux — et la ranger en typographie
|
|
461
|
+
remplissait la liste des illisibles de fausses tailles de texte. */
|
|
462
|
+
if (type === "typographie" && /color|ink|brand|surface|border|pilier/i.test(m[2]))
|
|
463
|
+
continue;
|
|
464
|
+
const cle = `${m[1]}:${m[2]}`;
|
|
465
|
+
if (vus.has(cle))
|
|
466
|
+
continue;
|
|
467
|
+
vus.add(cle);
|
|
468
|
+
etat.items.push({
|
|
469
|
+
kind: "token",
|
|
470
|
+
name: `${m[1]}-[var(${m[2]})]`,
|
|
471
|
+
type,
|
|
472
|
+
value: `var(${m[2]})`,
|
|
473
|
+
location: fichier,
|
|
474
|
+
attrs: { provenance: "variable" },
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
397
478
|
/**
|
|
398
479
|
* LES PAS NOMMÉS — la moitié du travail qui manquait.
|
|
399
480
|
*
|