@mostajs/kind-catalog 0.2.0 → 0.4.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/CHANGELOG.md +120 -0
- package/docs/DEVTEST-PLAN.kind-catalog.json +255 -0
- package/docs/EPREUVE-01-RESTOTRAX-06092026.md +177 -0
- package/docs/EPREUVE-02-LABTRAX-06092026.md +152 -0
- package/docs/PLAN-DEV-KIND-CATALOG.md +52 -1
- package/docs/PROPOSITION-REGLE-RELIRE-LE-PLAN.md +210 -0
- package/kinds/chiffres.kind.mjs +8 -1
- package/kinds/decision.kind.mjs +1 -0
- package/kinds/integration.kind.mjs +1 -0
- package/kinds/traces.kind.mjs +13 -0
- package/llms.txt +11 -0
- package/package.json +1 -1
- package/src/diagnostic.js +209 -0
- package/src/index.js +1 -0
- package/src/kind.js +12 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DIAGNOSTIQUER — lire le plan d'une application et lui dire ce qui lui MANQUE.
|
|
3
|
+
*
|
|
4
|
+
* ── LE TRANCHANT « AMÉLIORER » ─────────────────────────────────────────────
|
|
5
|
+
* Le catalogue sert deux fois : à BÂTIR (une application neuve reçoit ses exigences) et à
|
|
6
|
+
* AMÉLIORER (une application qui tourne reçoit ce que d'autres ont payé). Ce fichier est le
|
|
7
|
+
* second, et c'est celui qui se rend en une heure, sur un plan qu'on nous donne, sans toucher au
|
|
8
|
+
* code :
|
|
9
|
+
*
|
|
10
|
+
* « Votre plan porte quatre exigences de périmètre. La fiche en connaît six erreurs.
|
|
11
|
+
* Trois ne sont mentionnées nulle part chez vous — voici lesquelles, et ce qu'elles coûtent. »
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ IL PROPOSE, IL N'APPLIQUE PAS. Un rapprochement automatique entre les exigences d'une
|
|
14
|
+
* application et les fiches SE TROMPERA : les intitulés varient, les métiers diffèrent, et une
|
|
15
|
+
* ressemblance de mots n'est pas une identité de règle. Un outil qui appliquerait ses
|
|
16
|
+
* rapprochements tout seul poserait des blocs d'erreurs sur des exigences sans rapport, et
|
|
17
|
+
* ruinerait la confiance dans les blocs justes. C'est `KIND-ECRITURE-GARDEE-01` appliqué à
|
|
18
|
+
* l'outil lui-même.
|
|
19
|
+
*
|
|
20
|
+
* ⚠️ LE RAPPROCHEMENT EST EXPLICABLE, ET C'EST SA SEULE DÉFENSE. Chaque candidat rend LES MOTS
|
|
21
|
+
* qui l'ont désigné : un score opaque serait pire que pas de score du tout — on ne peut ni le
|
|
22
|
+
* contester ni le corriger.
|
|
23
|
+
*
|
|
24
|
+
* Author: Dr Hamid MADANI <drmdh@msn.com> · AGPL-3.0-or-later
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** Mots vides du français — ils apparaissent partout et ne désignent rien. */
|
|
28
|
+
const VIDES = new Set(`le la les un une des du de d au aux et ou ni mais donc or car que qui quoi
|
|
29
|
+
dont ou sur sous dans par pour avec sans vers chez entre est sont etre ete a ont avoir plus moins
|
|
30
|
+
tres peu tout tous toute toutes meme aussi ainsi alors quand comme si ne pas non oui son sa ses
|
|
31
|
+
leur leurs notre nos votre vos ce cet cette ces cela ceci il elle ils elles on nous vous je tu
|
|
32
|
+
doit doivent peut peuvent faire fait fais rien jamais toujours deja encore apres avant lors
|
|
33
|
+
chaque autre autres bien mal seul seule sauf selon sera seront soit soient afin ceux celui
|
|
34
|
+
celle quel quels quelle telle tels deux puis nul nulle dune dun cest lune lun elles`.split(/\s+/).filter(Boolean));
|
|
35
|
+
|
|
36
|
+
/** Normalise : minuscules, sans accents, sans ponctuation. */
|
|
37
|
+
export const normaliser = (t) => String(t ?? '')
|
|
38
|
+
.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '')
|
|
39
|
+
.replace(/[^a-z0-9]+/g, ' ').trim();
|
|
40
|
+
|
|
41
|
+
/** Les mots qui DÉSIGNENT quelque chose — au moins cinq lettres, hors mots vides. */
|
|
42
|
+
export function motsCles(texte) {
|
|
43
|
+
return new Set(normaliser(texte).split(' ').filter((m) => m.length >= 4 && !VIDES.has(m)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const commun = (a, b) => [...a].filter((m) => b.has(m));
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Construit la CANONISATION d'une fiche : chaque groupe de synonymes déclaré ramène ses membres au
|
|
50
|
+
* premier d'entre eux. Deux textes qui emploient deux mots du même groupe se reconnaissent alors —
|
|
51
|
+
* c'est exactement le faux positif relevé par l'épreuve rétrospective n° 1 (06/09/2026), où la
|
|
52
|
+
* fiche disait « retrait / tombale » et le plan « désactivation / horodaté ».
|
|
53
|
+
*
|
|
54
|
+
* ⚠️ LES MEMBRES SONT DES RADICAUX, PAS DES MOTS, et le rapprochement se fait par PRÉFIXE. Écrit
|
|
55
|
+
* en mots entiers, ce champ demandait d'énumérer toutes les flexions du français — `supprimer`,
|
|
56
|
+
* `supprime`, `supprimée`, `supprimées`… — liste qu'on croit finie et qui ne l'est jamais : la
|
|
57
|
+
* première version de ce champ tenait neuf mots pour un seul groupe et laissait passer
|
|
58
|
+
* `supprimée`, ce qu'un essai a montré (T-KC-23). Un radical (`supprim`) les couvre tous.
|
|
59
|
+
*
|
|
60
|
+
* ⚠️ QUATRE LETTRES AU MOINS pour un radical. Plus court, il cesse de désigner : `dat` attraperait
|
|
61
|
+
* `datation` mais aussi `dattes`, et un rapprochement qu'on ne peut plus justifier est pire que
|
|
62
|
+
* pas de rapprochement — c'est la seule défense de cet outil (voir l'en-tête).
|
|
63
|
+
*/
|
|
64
|
+
function canonique(kind) {
|
|
65
|
+
const groupes = (kind.synonymes ?? []).map((groupe) => {
|
|
66
|
+
const radicaux = groupe.map((m) => normaliser(m).replace(/ /g, '')).filter(Boolean);
|
|
67
|
+
// La tête reste le PREMIER déclaré, même s'il est trop court pour servir de radical : c'est
|
|
68
|
+
// l'étiquette du groupe, pas un motif de recherche.
|
|
69
|
+
return { tete: radicaux[0], motifs: radicaux.filter((r) => r.length >= 4) };
|
|
70
|
+
}).filter((g) => g.tete && g.motifs.length > 0);
|
|
71
|
+
|
|
72
|
+
return (mots) => new Set([...mots].map((mot) => {
|
|
73
|
+
const g = groupes.find(({ motifs }) => motifs.some((r) => mot.startsWith(r)));
|
|
74
|
+
return g ? g.tete : mot;
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Tout le texte d'une exigence, épreuves comprises : c'est là que la règle se dit. */
|
|
79
|
+
function texteDeSpec(plan, spec) {
|
|
80
|
+
const essais = (plan.tests ?? []).filter((t) => t.specRef === spec.ref);
|
|
81
|
+
return [spec.title, spec.description,
|
|
82
|
+
...essais.flatMap((t) => [t.title, ...(t.steps ?? []).flatMap((s) => [s.action, s.expected])])]
|
|
83
|
+
.filter(Boolean).join(' ');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Les domaines TRANSVERSES : leurs règles valent pour tout logiciel, qu'on ait su rapprocher une
|
|
88
|
+
* exigence ou non. Les domaines MÉTIER, eux, ne concernent que les applications de ce métier.
|
|
89
|
+
*/
|
|
90
|
+
export const TRANSVERSES = new Set(['acces', 'donnees', 'apprentissage', 'decision', 'integration']);
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Diagnostique un plan contre un corpus.
|
|
94
|
+
*
|
|
95
|
+
* ⚠️ LES DEUX SORTIES SONT DÉCOUPLÉES, ET C'EST LE POINT. Elles n'ont pas le même seuil optimal,
|
|
96
|
+
* et les lier faisait taire l'une pour l'autre :
|
|
97
|
+
*
|
|
98
|
+
* · les OCCURRENCES proposées demandent de la PRÉCISION — l'humain doit les confirmer, et
|
|
99
|
+
* personne ne confirme vingt-sept lignes. Seuil haut, liste courte.
|
|
100
|
+
* · les ERREURS NON COUVERTES demandent du RAPPEL — c'est le livrable, et taire celle qui
|
|
101
|
+
* manque coûte bien plus que d'en proposer une déjà couverte. Elles sont calculées sur TOUT
|
|
102
|
+
* le plan, et rendues même quand aucune exigence n'a pu être rapprochée.
|
|
103
|
+
*
|
|
104
|
+
* Une règle TRANSVERSE rend ses erreurs dans tous les cas ; une règle MÉTIER seulement si une
|
|
105
|
+
* exigence l'a désignée — sinon un plan universitaire recevrait les pièges de l'apiculture.
|
|
106
|
+
*
|
|
107
|
+
* @param {object} plan plan `mostajs-devtest/1` — NON modifié
|
|
108
|
+
* @param {object[]} kinds
|
|
109
|
+
* @param {object} [o]
|
|
110
|
+
* @param {number} [o.minCommuns] mots communs exigés pour PROPOSER une occurrence (défaut 5)
|
|
111
|
+
* @returns {object[]} un rapport par fiche
|
|
112
|
+
*/
|
|
113
|
+
export function diagnostiquer(plan, kinds = [], { minCommuns = 5 } = {}) {
|
|
114
|
+
if (!plan?.specs) throw new Error('diagnostiquer: plan `mostajs-devtest/1` requis');
|
|
115
|
+
const toutLePlan = motsCles([
|
|
116
|
+
...plan.specs.map((s) => `${s.title} ${s.description ?? ''}`),
|
|
117
|
+
...(plan.tests ?? []).flatMap((t) => [t.title, ...(t.steps ?? []).flatMap((x) => [x.action, x.expected])]),
|
|
118
|
+
].join(' '));
|
|
119
|
+
|
|
120
|
+
return kinds.map((k) => {
|
|
121
|
+
const canon = canonique(k);
|
|
122
|
+
const cles = canon(motsCles(`${k.enonce} ${k.succes.join(' ')} ${k.erreurs.map((e) => e.titre).join(' ')}`));
|
|
123
|
+
// Le plan est canonisé AVEC LA TABLE DE CETTE FICHE : chaque fiche apporte son vocabulaire, et
|
|
124
|
+
// celui d'une fiche ne parasite pas la lecture d'une autre.
|
|
125
|
+
const planCanon = canon(toutLePlan);
|
|
126
|
+
|
|
127
|
+
// 1 · LES OCCURRENCES CANDIDATES — proposées, jamais posées.
|
|
128
|
+
const occurrences = [];
|
|
129
|
+
for (const s of plan.specs) {
|
|
130
|
+
const c = commun(cles, canon(motsCles(texteDeSpec(plan, s))));
|
|
131
|
+
if (c.length >= minCommuns) occurrences.push({ specRef: s.ref, titre: s.title, communs: c.sort() });
|
|
132
|
+
}
|
|
133
|
+
occurrences.sort((a, b) => b.communs.length - a.communs.length);
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 2 · LES ERREURS QUE LE PLAN NE MENTIONNE NULLE PART — c'est le livrable.
|
|
137
|
+
*
|
|
138
|
+
* ⚠️ ON NE COMPARE QUE LE TITRE, PAS LA CONSÉQUENCE. Corrigé le 06/09/2026, après l'épreuve
|
|
139
|
+
* rétrospective n° 1. La conséquence est NOTRE prose — elle explique le coût de l'erreur à
|
|
140
|
+
* celui qui lit la fiche. **Un plan n'a aucune raison de la contenir.** L'inclure dans la
|
|
141
|
+
* comparaison rendait une erreur d'autant plus « non couverte » qu'elle était bien expliquée,
|
|
142
|
+
* et une exigence courte mais explicite ne pouvait JAMAIS couvrir une erreur verbeuse.
|
|
143
|
+
*
|
|
144
|
+
* ⚠️ Et le seuil porte sur DEUX mots du titre, pas sur une proportion. Une proportion pénalise
|
|
145
|
+
* les titres longs, ce qui est l'inverse du bon sens : un titre long est plus reconnaissable,
|
|
146
|
+
* pas moins.
|
|
147
|
+
*/
|
|
148
|
+
const erreursNonCouvertes = k.erreurs.filter((e) => {
|
|
149
|
+
const mots = canon(motsCles(e.titre));
|
|
150
|
+
if (mots.size === 0) return false;
|
|
151
|
+
return commun(mots, planCanon).length < Math.min(2, mots.size);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const transverse = TRANSVERSES.has(k.domaine);
|
|
155
|
+
return {
|
|
156
|
+
kind: k.ref, domaine: k.domaine, verdict: k.verdict, enonce: k.enonce,
|
|
157
|
+
occurrences,
|
|
158
|
+
couverte: occurrences.length > 0,
|
|
159
|
+
transverse,
|
|
160
|
+
erreurs: k.erreurs.length,
|
|
161
|
+
// Une règle métier non rapprochée ne dit rien : elle ne concerne pas cette application.
|
|
162
|
+
erreursNonCouvertes: (transverse || occurrences.length) ? erreursNonCouvertes : [],
|
|
163
|
+
};
|
|
164
|
+
}).sort((a, b) => b.occurrences.length - a.occurrences.length);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Le rapport en texte — c'est lui qu'on présente. */
|
|
168
|
+
export function rapportDiagnostic(diag, { titre = 'Diagnostic', maxOccurrences = 4 } = {}) {
|
|
169
|
+
const l = [`# ${titre}`, ''];
|
|
170
|
+
const aTrous = diag.filter((d) => d.erreursNonCouvertes.length);
|
|
171
|
+
const reconnues = diag.filter((d) => d.couverte);
|
|
172
|
+
const trous = aTrous.reduce((n, d) => n + d.erreursNonCouvertes.length, 0);
|
|
173
|
+
|
|
174
|
+
l.push(`**${trous}** erreur(s) connue(s) que ce plan ne mentionne nulle part, sur **${aTrous.length}** règle(s) · **${reconnues.length}** règle(s) rapprochée(s) d’au moins une exigence.`, '');
|
|
175
|
+
l.push('> ⚠️ Les rapprochements sont **proposés**, jamais appliqués : une ressemblance de mots n’est pas une identité de règle. Chaque candidat rend **les mots qui l’ont désigné**, pour qu’on puisse le contester.', '');
|
|
176
|
+
l.push('> Les erreurs, elles, sont cherchées dans **tout** le plan — titres, descriptions et épreuves. Une erreur signalée à tort s’écarte en dix secondes ; une erreur tue ne se cherche jamais.', '');
|
|
177
|
+
|
|
178
|
+
l.push('## Ce qui manque', '');
|
|
179
|
+
if (!aTrous.length) l.push('Aucune erreur connue du catalogue n’est absente de ce plan.', '');
|
|
180
|
+
for (const d of aTrous) {
|
|
181
|
+
l.push(`### ${d.kind} — ${d.enonce}`, '');
|
|
182
|
+
if (d.occurrences.length) {
|
|
183
|
+
l.push(`*Reconnue dans ${d.occurrences.length} exigence(s) :* ` + d.occurrences.slice(0, maxOccurrences).map((o) => `\`${o.specRef}\``).join(', ')
|
|
184
|
+
+ (d.occurrences.length > maxOccurrences ? ` *(+${d.occurrences.length - maxOccurrences})*` : ''), '');
|
|
185
|
+
} else {
|
|
186
|
+
l.push(`*Règle transverse — aucune exigence de ce plan ne la porte explicitement.*`, '');
|
|
187
|
+
}
|
|
188
|
+
l.push(`**${d.erreursNonCouvertes.length} sur ${d.erreurs} erreur(s) connue(s) ne sont mentionnées nulle part :**`, '');
|
|
189
|
+
for (const e of d.erreursNonCouvertes) l.push(`- **${e.titre}** — ${e.consequence}`);
|
|
190
|
+
l.push('');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const propres = reconnues.filter((d) => !d.erreursNonCouvertes.length);
|
|
194
|
+
if (propres.length) {
|
|
195
|
+
l.push('## Ce qui est couvert', '');
|
|
196
|
+
l.push('Ces règles sont reconnues dans le plan, et **toutes** leurs erreurs connues y sont évoquées.', '');
|
|
197
|
+
for (const d of propres) {
|
|
198
|
+
l.push(`- \`${d.kind}\` — ${d.enonce} *(${d.occurrences.slice(0, maxOccurrences).map((o) => o.specRef).join(', ')})*`);
|
|
199
|
+
}
|
|
200
|
+
l.push('');
|
|
201
|
+
}
|
|
202
|
+
const hors = diag.filter((d) => !d.couverte && !d.erreursNonCouvertes.length);
|
|
203
|
+
if (hors.length) {
|
|
204
|
+
l.push('## Hors périmètre apparent', '');
|
|
205
|
+
l.push('Aucune exigence ne les désigne — elles ne concernent probablement pas cette application.', '');
|
|
206
|
+
for (const d of hors) l.push(`- \`${d.kind}\` (${d.domaine})`);
|
|
207
|
+
}
|
|
208
|
+
return l.join('\n');
|
|
209
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { defineKind, validateKind, VERDICTS, PROVENANCES, DOMAINES } from './kind.js';
|
|
2
2
|
export { toDevtest, PLAN_VERSION } from './projection.js';
|
|
3
3
|
export { enrichir, retirerProjection, blocDeFiche, marqueur } from './enrichir.js';
|
|
4
|
+
export { diagnostiquer, rapportDiagnostic, motsCles, normaliser, TRANSVERSES } from './diagnostic.js';
|
|
4
5
|
export { instantiate, diffInstance, emplois, AFFINABLES } from './instance.js';
|
|
5
6
|
export { loadCatalogue, findKinds, auditCatalogue, statsCatalogue } from './catalogue.js';
|
package/src/kind.js
CHANGED
|
@@ -84,6 +84,18 @@ export function defineKind(f = {}) {
|
|
|
84
84
|
verdict: propre(f.verdict) || 'propose',
|
|
85
85
|
motif: propre(f.motif) || null,
|
|
86
86
|
origine: liste(f.origine),
|
|
87
|
+
/**
|
|
88
|
+
* LES SYNONYMES DU DOMAINE — groupes de mots qui désignent la MÊME chose.
|
|
89
|
+
*
|
|
90
|
+
* ⚠️ Ajoutés le 06/09/2026, après l'épreuve rétrospective n° 1. Le diagnostic avait signalé
|
|
91
|
+
* 5 erreurs sur 5 comme « non mentionnées » dans un plan qui les couvrait explicitement : la
|
|
92
|
+
* fiche disait *retrait*, *pierre tombale*, le plan disait *désactivation*, *horodatés*. Deux
|
|
93
|
+
* textes qui disent la même chose avec d'autres mots ne se reconnaissaient pas.
|
|
94
|
+
*
|
|
95
|
+
* Aucun rapprochement lexical ne peut deviner cette équivalence — elle est propre au domaine.
|
|
96
|
+
* Elle se DÉCLARE donc, comme tout le reste de la fiche.
|
|
97
|
+
*/
|
|
98
|
+
synonymes: liste(f.synonymes).filter((g) => Array.isArray(g) && g.length > 1),
|
|
87
99
|
// OÙ la question se pose, quand ce n'est pas là où elle se calcule. Le corpus transverse n'en
|
|
88
100
|
// avait pas besoin ; le BTP (sur chantier) et l'électronique (sur l'embarqué) l'exigent.
|
|
89
101
|
seProsePose: propre(f.seProsePose) || null,
|