designwithai-mcp 0.12.0 → 0.13.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 +29 -5
- package/dist/craft-report.js +23 -4
- package/dist/craft-store.js +76 -0
- package/dist/ds-inventory.js +18 -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 +40 -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
|
@@ -462,7 +462,7 @@ export const TOLERANCE_TYPO = 1;
|
|
|
462
462
|
* relie deux. En dessous de trois, la mesure le DIT au lieu de rendre un
|
|
463
463
|
* chiffre qui ne veut rien dire.
|
|
464
464
|
*/
|
|
465
|
-
export function echelleTypo(items) {
|
|
465
|
+
export function echelleTypo(items, systeme) {
|
|
466
466
|
const nonMesures = [];
|
|
467
467
|
const vues = new Set();
|
|
468
468
|
for (const it of items) {
|
|
@@ -476,11 +476,33 @@ export function echelleTypo(items) {
|
|
|
476
476
|
vues.add(px);
|
|
477
477
|
}
|
|
478
478
|
const tailles = [...vues].sort((a, b) => a - b);
|
|
479
|
-
|
|
479
|
+
/* L'ÉCHELLE ÉNUMÉRÉE PASSE AVANT TOUT LE RESTE, Y COMPRIS AVANT LE SEUIL DE
|
|
480
|
+
TROIS TAILLES. Ce seuil existe parce qu'on ne DÉDUIT rien de deux valeurs ;
|
|
481
|
+
ici on ne déduit pas, on compare à une liste écrite. Une équipe qui déclare
|
|
482
|
+
son échelle a droit à sa mesure même sur un fichier qui n'en emploie
|
|
483
|
+
qu'une. */
|
|
484
|
+
const declarees = (systeme?.taillesTypo ?? []).filter((n) => Number.isFinite(n) && n > 0);
|
|
485
|
+
if (declarees.length > 0) {
|
|
486
|
+
const attendues = [...new Set(declarees)].sort((a, b) => a - b);
|
|
487
|
+
/* Conformité = APPARTENANCE. Pas de tolérance : une liste ne se discute
|
|
488
|
+
pas, et « à un pixel près d'un pas déclaré » n'est pas le pas déclaré. */
|
|
489
|
+
const dansLaListe = new Set(attendues);
|
|
490
|
+
const orphelines = tailles.filter((t) => !dansLaListe.has(t));
|
|
491
|
+
return {
|
|
492
|
+
valeur: { ratio: null, ecartMoyen: null },
|
|
493
|
+
detail: { tailles, attendues, orphelines, essais: [], origine: "énumérée" },
|
|
494
|
+
completude: { lus: tailles.length, nonMesures },
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
/* UN RATIO DÉCLARÉ N'EST PAS UN RATIO À DEVINER. Tant que ce paramètre
|
|
498
|
+
n'existait pas, `ratioTypo` se déclarait et ne servait à rien : la mesure
|
|
499
|
+
cherchait le ratio le moins faux et ignorait celui de l'équipe. */
|
|
500
|
+
const ratioDeclare = systeme?.ratioTypo && systeme.ratioTypo > 1 && systeme.ratioTypo <= 3 ? systeme.ratioTypo : null;
|
|
501
|
+
if (tailles.length < 3 && !ratioDeclare) {
|
|
480
502
|
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
503
|
return {
|
|
482
504
|
valeur: { ratio: null, ecartMoyen: null },
|
|
483
|
-
detail: { tailles, attendues: [], orphelines: [], essais: [] },
|
|
505
|
+
detail: { tailles, attendues: [], orphelines: [], essais: [], origine: "déduite" },
|
|
484
506
|
completude: { lus: tailles.length, nonMesures },
|
|
485
507
|
};
|
|
486
508
|
}
|
|
@@ -493,14 +515,16 @@ export function echelleTypo(items) {
|
|
|
493
515
|
const ecart = tailles.reduce((somme, t) => somme + Math.min(...pas.map((p) => Math.abs(p - t))), 0) / tailles.length;
|
|
494
516
|
return { ratio, ecart: Math.round(ecart * 1000) / 1000 };
|
|
495
517
|
}).sort((a, b) => a.ecart - b.ecart);
|
|
496
|
-
const gagnant =
|
|
518
|
+
const gagnant = ratioDeclare
|
|
519
|
+
? (essais.find((e) => e.ratio === ratioDeclare) ?? { ratio: ratioDeclare, ecart: 0 })
|
|
520
|
+
: essais[0];
|
|
497
521
|
const attendues = [];
|
|
498
522
|
for (let v = base; v <= max * gagnant.ratio; v *= gagnant.ratio)
|
|
499
523
|
attendues.push(Math.round(v * 10) / 10);
|
|
500
524
|
const orphelines = tailles.filter((t) => Math.min(...attendues.map((p) => Math.abs(p - t))) > TOLERANCE_TYPO);
|
|
501
525
|
return {
|
|
502
526
|
valeur: { ratio: gagnant.ratio, ecartMoyen: gagnant.ecart },
|
|
503
|
-
detail: { tailles, attendues, orphelines, essais },
|
|
527
|
+
detail: { tailles, attendues, orphelines, essais, origine: ratioDeclare ? "ratio déclaré" : "déduite" },
|
|
504
528
|
completude: { lus: tailles.length, nonMesures },
|
|
505
529
|
};
|
|
506
530
|
}
|
package/dist/craft-report.js
CHANGED
|
@@ -38,7 +38,7 @@ export function craftReport(brut, opts = {}) {
|
|
|
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()));
|
|
@@ -71,6 +71,8 @@ export function craftReport(brut, opts = {}) {
|
|
|
71
71
|
ratio: typo.valeur.ratio,
|
|
72
72
|
ecartMoyen: typo.valeur.ecartMoyen,
|
|
73
73
|
tailles: typo.detail.tailles,
|
|
74
|
+
attendues: typo.detail.attendues,
|
|
75
|
+
origine: typo.detail.origine,
|
|
74
76
|
orphelines: typo.detail.orphelines,
|
|
75
77
|
nonMesures: [...typo.completude.nonMesures, ...tailles.completude.nonMesures],
|
|
76
78
|
},
|
|
@@ -193,13 +195,30 @@ export function rendreCraftReport(r, opts = {}) {
|
|
|
193
195
|
l.push("");
|
|
194
196
|
/* ── Typographie ── */
|
|
195
197
|
l.push("LES TAILLES DE TEXTE");
|
|
196
|
-
|
|
198
|
+
/* TROIS PHRASES, PARCE QU'IL Y A TROIS SITUATIONS — et les confondre revient
|
|
199
|
+
à opposer une supposition à une décision. « déduite » doit se lire comme
|
|
200
|
+
déduite : c'est le seul cas où l'échelle vient de nous et pas de l'équipe. */
|
|
201
|
+
if (r.typo.origine === "énumérée") {
|
|
202
|
+
l.push(` ${r.typo.tailles.length} taille(s) employée(s) : ${r.typo.tailles.join(", ")}.` + cite("TYPO-02"));
|
|
203
|
+
l.push(` Mesuré contre l'échelle ÉNUMÉRÉE que l'équipe a déclarée — ${r.typo.attendues.join(", ")}. ` +
|
|
204
|
+
"Aucun ratio n'est cherché : la conformité, ici, c'est d'être dans la liste." + cite("TYPO-01"));
|
|
205
|
+
if (r.typo.orphelines.length > 0) {
|
|
206
|
+
l.push(` ${r.typo.orphelines.length} taille(s) hors de l'échelle déclarée : ${r.typo.orphelines.join(", ")}.`);
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
l.push(" Toutes tombent sur un pas déclaré.");
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
else if (r.typo.ratio === null) {
|
|
197
213
|
l.push(` Pas assez de tailles distinctes pour déduire une échelle (${r.typo.tailles.length}).` + cite("TYPO-01"));
|
|
198
214
|
}
|
|
199
215
|
else {
|
|
200
216
|
l.push(` ${r.typo.tailles.length} taille(s) : ${r.typo.tailles.join(", ")}.` + cite("TYPO-02"));
|
|
201
|
-
l.push(
|
|
202
|
-
`
|
|
217
|
+
l.push(r.typo.origine === "ratio déclaré"
|
|
218
|
+
? ` Mesuré contre le ratio de ${r.typo.ratio} que l'équipe a déclaré ` +
|
|
219
|
+
`(écart moyen ${r.typo.ecartMoyen} pixel).` + cite("TYPO-01")
|
|
220
|
+
: ` Aucune échelle déclarée : celle qui les explique le mieux est un ratio de ${r.typo.ratio} ` +
|
|
221
|
+
`(écart moyen ${r.typo.ecartMoyen} pixel). C'est une déduction, pas une décision.` + cite("TYPO-01"));
|
|
203
222
|
if (r.typo.orphelines.length > 0) {
|
|
204
223
|
l.push(` ${r.typo.orphelines.length} taille(s) qu'aucun pas n'explique : ${r.typo.orphelines.join(", ")}.`);
|
|
205
224
|
}
|
|
@@ -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;
|
package/dist/pr-delta.js
ADDED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LE DELTA DE BRANCHE — ce que cette branche change au registre.
|
|
3
|
+
*
|
|
4
|
+
* Deux registres, celui de la base et celui de la branche, et la même
|
|
5
|
+
* `diffFindings` que partout ailleurs — avec ses DÉPLACÉS. C'est le point :
|
|
6
|
+
* un sélecteur renommé ne devient pas « réglé » dans un commentaire de PR
|
|
7
|
+
* plus qu'ailleurs, et la catégorie qui dit « je ne sais pas » existe ici
|
|
8
|
+
* aussi.
|
|
9
|
+
*
|
|
10
|
+
* AUCUN SERVICE À NOUS DANS LA BOUCLE. Les fichiers du registre sont dans git ;
|
|
11
|
+
* le calcul est déterministe et local. Le commentaire est posté par l'Action
|
|
12
|
+
* avec le jeton standard de GitHub — pas de secret designwithai, pas d'appel
|
|
13
|
+
* sortant, rien à héberger.
|
|
14
|
+
*
|
|
15
|
+
* Aucune dépendance de valeur hors des modules purs.
|
|
16
|
+
*/
|
|
17
|
+
import { diffFindings } from "./finding-identity.js";
|
|
18
|
+
import { estProuve } from "./registry-events.js";
|
|
19
|
+
/** Tous les constats d'un jeu de revues, à plat. */
|
|
20
|
+
export function constats(reviews) {
|
|
21
|
+
return reviews.flatMap((r) => r.findings);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Ce que la branche change.
|
|
25
|
+
*
|
|
26
|
+
* « Réglé » se mesure sur DEUX conditions réunies : le constat a disparu de la
|
|
27
|
+
* branche, ET il portait une vérification. Un constat simplement absent est
|
|
28
|
+
* « résolu » au sens du diff — mais rien ne prouve qu'il a été corrigé, et
|
|
29
|
+
* c'est pourquoi il ne s'affiche pas comme réglé.
|
|
30
|
+
*/
|
|
31
|
+
export function delta(base, branche) {
|
|
32
|
+
const avant = constats(base);
|
|
33
|
+
const apres = constats(branche);
|
|
34
|
+
const d = diffFindings(avant, apres);
|
|
35
|
+
/* Un constat disparu de la branche : est-ce réglé ? On le dit seulement si la
|
|
36
|
+
branche porte sa vérification. Sinon il est simplement absent, et c'est un
|
|
37
|
+
autre mot. */
|
|
38
|
+
const versionApres = (f) => apres.find((x) => (x.title ?? x.summary) === (f.title ?? f.summary)) ?? f;
|
|
39
|
+
/* CE QUE LA BRANCHE CHANGE, PAS CE QU'ELLE CONTIENT. Un constat déjà prouvé
|
|
40
|
+
dans la base n'est pas réglé PAR cette branche : le recompter ferait
|
|
41
|
+
grossir le chiffre à chaque PR sans que personne ne corrige rien. */
|
|
42
|
+
const dejaProuve = new Set(avant.filter(estProuve).map((f) => f.title ?? f.summary ?? ""));
|
|
43
|
+
const reglesProuves = apres.filter((f) => estProuve(f) && !dejaProuve.has(f.title ?? f.summary ?? ""));
|
|
44
|
+
const declaresCorriges = apres.filter((f) => !estProuve(f) && (f.status === "fixed" || f.status === "partial"));
|
|
45
|
+
/* UN CONSTAT N'APPARAÎT QUE DANS UNE CASE. Un défaut trouvé ET corrigé dans
|
|
46
|
+
la même branche arrive « nouveau » au sens du diff, mais il est déjà
|
|
47
|
+
annoncé comme corrigé plus haut. Le laisser dans les deux gonflerait le
|
|
48
|
+
compte de deux façons opposées dans le même commentaire. */
|
|
49
|
+
const dejaDit = new Set([...reglesProuves, ...declaresCorriges]);
|
|
50
|
+
return {
|
|
51
|
+
reglesProuves,
|
|
52
|
+
declaresCorriges,
|
|
53
|
+
nouveaux: d.added.filter((f) => !dejaDit.has(f)),
|
|
54
|
+
deplaces: (d.deplaces ?? []).map((x) => ({ avant: x.avant, apres: versionApres(x.apres) })),
|
|
55
|
+
persistants: d.persisting.filter((f) => !dejaDit.has(f) && f.status !== "fixed"),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
const titre = (f) => f.title || f.summary || "un constat";
|
|
59
|
+
/** Le commentaire de PR, en Markdown. Une ligne de comptes, le détail dépliable. */
|
|
60
|
+
export function rendreDelta(d, opts = {}) {
|
|
61
|
+
/* LA BASE SANS REGISTRE N'EST PAS UNE BASE VIDE. Rendre « 12 nouveaux » quand
|
|
62
|
+
la base n'a simplement jamais versionné son registre transforme une absence
|
|
63
|
+
d'information en accusation. On le dit, et on ne compte rien. */
|
|
64
|
+
if (opts.baseSansRegistre) {
|
|
65
|
+
return [
|
|
66
|
+
`**Pas de comparaison possible** — ${opts.base ? `\`${opts.base}\`` : "la base"} ne contient aucun fichier de registre.`,
|
|
67
|
+
"",
|
|
68
|
+
"Ce n'est pas « aucun constat » : c'est « rien à comparer ». Le delta suppose que " +
|
|
69
|
+
"`.designwithai/` est versionné des deux côtés. S'il est dans le `.gitignore`, " +
|
|
70
|
+
"cette comparaison ne peut pas exister — et un delta rendu quand même dirait le contraire de la vérité.",
|
|
71
|
+
].join("\n");
|
|
72
|
+
}
|
|
73
|
+
const bouts = [
|
|
74
|
+
`${d.reglesProuves.length} réglé${d.reglesProuves.length > 1 ? "s" : ""} (vérifié${d.reglesProuves.length > 1 ? "s" : ""})`,
|
|
75
|
+
`${d.nouveaux.length} nouveau${d.nouveaux.length > 1 ? "x" : ""}`,
|
|
76
|
+
`${d.deplaces.length} déplacé${d.deplaces.length > 1 ? "s" : ""}`,
|
|
77
|
+
`${d.persistants.length} persistant${d.persistants.length > 1 ? "s" : ""}`,
|
|
78
|
+
];
|
|
79
|
+
const l = [];
|
|
80
|
+
l.push(`**${opts.branche ? `${opts.branche} : ` : "Cette branche : "}${bouts.join(" · ")}**`);
|
|
81
|
+
if (d.declaresCorriges.length > 0) {
|
|
82
|
+
const n = d.declaresCorriges.length;
|
|
83
|
+
l.push("");
|
|
84
|
+
l.push(`${n} autre${n > 1 ? "s sont déclarés" : " est déclaré"} corrigé${n > 1 ? "s" : ""} ` +
|
|
85
|
+
`**sans vérification** — non compté${n > 1 ? "s" : ""} dans les réglés.`);
|
|
86
|
+
}
|
|
87
|
+
const bloc = (nom, lignes) => {
|
|
88
|
+
if (lignes.length === 0)
|
|
89
|
+
return;
|
|
90
|
+
l.push("");
|
|
91
|
+
l.push(`<details><summary>${nom} (${lignes.length})</summary>`);
|
|
92
|
+
l.push("");
|
|
93
|
+
for (const x of lignes)
|
|
94
|
+
l.push(`- ${x}`);
|
|
95
|
+
l.push("");
|
|
96
|
+
l.push("</details>");
|
|
97
|
+
};
|
|
98
|
+
bloc("Réglés, avec preuve", d.reglesProuves.map((f) => `${titre(f)} — vérifié contre ${f.verified_by}`));
|
|
99
|
+
bloc("Déclarés corrigés, sans vérification", d.declaresCorriges.map(titre));
|
|
100
|
+
bloc("Nouveaux", d.nouveaux.map((f) => `${titre(f)}${f.severity ? ` (${f.severity})` : ""}`));
|
|
101
|
+
bloc("Déplacés — ni réglés, ni nouveaux", d.deplaces.map((x) => `${titre(x.apres)} — « ${x.avant.ancre?.selector ?? "?"} » → « ${x.apres.ancre?.selector ?? "?"} ». ` +
|
|
102
|
+
"Même défaut renommé, ou deux éléments jumeaux : rien ici ne permet de trancher."));
|
|
103
|
+
bloc("Persistants", d.persistants.map((f) => `${titre(f)}${f.severity ? ` (${f.severity})` : ""}`));
|
|
104
|
+
l.push("");
|
|
105
|
+
l.push("<sub>Dérivé du registre versionné, en comparant la base et la branche. Aucun service tiers n'intervient.</sub>");
|
|
106
|
+
return l.join("\n");
|
|
107
|
+
}
|