@roomi-fields/grillhouse 0.0.0-stage → 0.23.1
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/.claude-plugin/plugin.json +9 -0
- package/METACADRE.md +41 -0
- package/agents/developpeur.md +10 -0
- package/agents/explorateur.md +9 -0
- package/agents/questionneur.md +11 -0
- package/agents/referent-technique.md +9 -0
- package/agents/regulateur.md +11 -0
- package/agents/relecteur.md +9 -0
- package/agents/testwriter.md +10 -0
- package/bin/grillhouse.mjs +125 -0
- package/cadre/templates/projet/.claude/settings.json +30 -0
- package/cadre/templates/projet/.editorconfig +47 -0
- package/cadre/templates/projet/.env.example +57 -0
- package/cadre/templates/projet/.eslintrc.cjs +56 -0
- package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
- package/cadre/templates/projet/.mcp.json +13 -0
- package/cadre/templates/projet/.prettierrc +11 -0
- package/cadre/templates/projet/CHANGELOG.md +61 -0
- package/cadre/templates/projet/CLAUDE.md +46 -0
- package/cadre/templates/projet/CONTRIBUTING.md +243 -0
- package/cadre/templates/projet/CREDITS.md +41 -0
- package/cadre/templates/projet/README.md +152 -0
- package/cadre/templates/projet/_gitignore +91 -0
- package/cadre/templates/projet/_npmignore +46 -0
- package/cadre/templates/projet/deployment/README.md +98 -0
- package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
- package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
- package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
- package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
- package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
- package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
- package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
- package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
- package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
- package/cadre/templates/projet/ecosystem.config.cjs +73 -0
- package/cadre/templates/projet/package.json +90 -0
- package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
- package/cadre/templates/projet/src/config.ts +68 -0
- package/cadre/templates/projet/src/errors.ts +102 -0
- package/cadre/templates/projet/src/index.ts +46 -0
- package/cadre/templates/projet/src/services/.gitkeep +8 -0
- package/cadre/templates/projet/src/types/index.ts +24 -0
- package/cadre/templates/projet/src/utils/logger.ts +106 -0
- package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
- package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
- package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
- package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
- package/cadre/templates/projet/tsconfig.eslint.json +9 -0
- package/cadre/templates/projet/tsconfig.json +41 -0
- package/cadre/templates/projet/tsdoc.json +6 -0
- package/cadre/templates/projet/vitest.config.ts +44 -0
- package/cadre/templates/turbo/paquets.json +29 -0
- package/cadre/templates/turbo/sans-paquets.json +28 -0
- package/charte.md +107 -0
- package/docs/ARCHITECTURE.md +90 -0
- package/hooks/crochet.mjs +33 -0
- package/hooks/hooks.json +58 -0
- package/hooks/register.tsx +558 -0
- package/package.json +75 -4
- package/scripts/avancement.mjs +244 -0
- package/scripts/bilan.mjs +523 -0
- package/scripts/branche-de-nuit.mjs +59 -0
- package/scripts/cadre.mjs +109 -0
- package/scripts/circuit.mjs +4375 -0
- package/scripts/code-mort.mjs +371 -0
- package/scripts/composants-affectes.mjs +598 -0
- package/scripts/consommateurs-arbitres.mjs +192 -0
- package/scripts/consommateurs.mjs +310 -0
- package/scripts/crochet.mjs +24 -0
- package/scripts/crochets.mjs +135 -0
- package/scripts/dernier-vert.mjs +208 -0
- package/scripts/dette.mjs +181 -0
- package/scripts/droits.mjs +354 -0
- package/scripts/enveloppe/codes.mjs +17 -0
- package/scripts/enveloppe/composant.mjs +11 -0
- package/scripts/enveloppe/enveloppe.mjs +628 -0
- package/scripts/enveloppe/lancer.sh +311 -0
- package/scripts/etapes.mjs +323 -0
- package/scripts/etat-du-circuit.mjs +104 -0
- package/scripts/frontieres.mjs +456 -0
- package/scripts/gardes-du-cadre.mjs +72 -0
- package/scripts/greffons.mjs +338 -0
- package/scripts/grillhouse-maj.mjs +514 -0
- package/scripts/index-interfaces.mjs +293 -0
- package/scripts/init.mjs +117 -0
- package/scripts/integration/file-attente.mjs +61 -0
- package/scripts/integration/file.mjs +494 -0
- package/scripts/integration/integrer.mjs +381 -0
- package/scripts/integration/rejeu.mjs +139 -0
- package/scripts/integration/verdict.mjs +13 -0
- package/scripts/interfaces-contre-code.mjs +547 -0
- package/scripts/ligne-etat/defaut.sh +110 -0
- package/scripts/ligne-etat/ligne-etat.mjs +93 -0
- package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
- package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
- package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
- package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
- package/scripts/migrations.mjs +43 -0
- package/scripts/nuit.sh +269 -0
- package/scripts/paquets.mjs +79 -0
- package/scripts/poussee-cles.mjs +46 -0
- package/scripts/preparation.mjs +84 -0
- package/scripts/references-par-les-types.mjs +420 -0
- package/scripts/registre.mjs +336 -0
- package/scripts/resultats.mjs +580 -0
- package/scripts/resumes.mjs +57 -0
- package/scripts/retirer-copie.sh +127 -0
- package/scripts/session-start.sh +203 -0
- package/scripts/sortie.mjs +155 -0
- package/scripts/sous-modules.sh +316 -0
- package/scripts/structure-guard.sh +27 -0
- package/scripts/superviseur.mjs +102 -0
- package/scripts/tableau.mjs +1886 -0
- package/scripts/tests.mjs +236 -0
- package/scripts/ticket.sh +23 -0
- package/scripts/turbo-des-paquets.mjs +899 -0
- package/scripts/un-commit-par-ticket.mjs +212 -0
- package/scripts/verifier.mjs +469 -0
- package/scripts/verrou-de-fichier.mjs +130 -0
- package/scripts/verrou-de-machine.mjs +211 -0
- package/scripts/verrous/commande.mjs +180 -0
- package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
- package/scripts/verrous/verrou.mjs +165 -0
- package/scripts/vitest-config/avancement.mjs +39 -0
- package/scripts/vitest-config/avancement.reporter.mjs +56 -0
- package/scripts/vitest-config/base-de-passe.mjs +128 -0
- package/scripts/vitest-config/base.d.ts +6 -0
- package/scripts/vitest-config/base.mjs +87 -0
- package/scripts/vitest-config/changements.mjs +173 -0
- package/scripts/vitest-config/cibles.mjs +207 -0
- package/scripts/vitest-config/duree.reporter.mjs +98 -0
- package/scripts/vitest-config/machine.setup.mjs +14 -0
- package/scripts/vitest-config/mesure.d.ts +6 -0
- package/scripts/vitest-config/mesure.mjs +13 -0
- package/scripts/vitest-config/mesure.setup.mjs +12 -0
- package/scripts/vitest-config/pression.mjs +90 -0
- package/scripts/vitest-config/resultats.mjs +23 -0
- package/scripts/vitest-config/resultats.setup.mjs +19 -0
- package/scripts/vitest.nuit.config.mjs +26 -0
- package/skills/developpeur/SKILL.md +175 -0
- package/skills/explorateur/SKILL.md +84 -0
- package/skills/grill/SKILL.md +238 -0
- package/skills/grill/references/releve-structurel.md +38 -0
- package/skills/mesure/SKILL.md +168 -0
- package/skills/pitmaster/SKILL.md +382 -0
- package/skills/pitmaster/references/cadre.md +119 -0
- package/skills/pitmaster/references/consigne-agent.md +308 -0
- package/skills/pitmaster/references/erreurs.md +25 -0
- package/skills/pitmaster/references/tour.md +124 -0
- package/skills/questionneur/SKILL.md +64 -0
- package/skills/redacteur/SKILL.md +84 -0
- package/skills/redacteur/references/documents-d-un-composant.md +105 -0
- package/skills/referent-technique/SKILL.md +145 -0
- package/skills/regulateur/SKILL.md +220 -0
- package/skills/release/SKILL.md +134 -0
- package/skills/release/scripts/update-version.cjs +92 -0
- package/skills/relecteur/SKILL.md +152 -0
- package/skills/relecteur/references/LICENSE-MIT +30 -0
- package/skills/relecteur/references/SOURCE.md +8 -0
- package/skills/relecteur/references/claims-check.md +14 -0
- package/skills/relecteur/references/deletion-check.md +14 -0
- package/skills/relecteur/references/edge-case-hunter.md +80 -0
- package/skills/relecteur/references/verification-gap.md +113 -0
- package/skills/testwriter/SKILL.md +187 -0
- package/types/index.d.ts +56 -0
- package/README.md +0 -3
|
@@ -0,0 +1,580 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The formats of the results the circuit decides on, in one module, which the circuit
|
|
3
|
+
// (scripts/circuit.mjs), the guards and the tests read; as Bazel's Build Event Protocol and the
|
|
4
|
+
// JUnit reports carry the outcome of a build, no reader reads an output meant for the human.
|
|
5
|
+
//
|
|
6
|
+
// npx grillhouse resultats --schema <rôle> prints the JSON schema of a role's sheet
|
|
7
|
+
//
|
|
8
|
+
// - The sheet of the end of a session (« la fiche ») of the testwriter, the developpeur, the reviewer,
|
|
9
|
+
// the regulator, the questionneur and the technical referent: the schema of each role (FICHES), which the session receives
|
|
10
|
+
// (`claude -p --output-format json --json-schema <schéma>`) and fills as its last act; claude
|
|
11
|
+
// writes it in the field `structured_output` of its result (Claude Code, « headless »). A field
|
|
12
|
+
// exists only when a reader reads it:
|
|
13
|
+
// - `statut` (FAIT, BLOQUÉ, BESOIN), with `raison` for BLOQUÉ and `question` for BESOIN: the
|
|
14
|
+
// circuit goes on, or stops at « hors-portee » or « besoin » (testwriter, developpeur);
|
|
15
|
+
// - `verts_attendus` (`fichier` from the root, `test` its full name, or none for every green of a
|
|
16
|
+
// file HEAD holds, `raison`): the proof of the red (testwriter);
|
|
17
|
+
// - `renvoi` (`role` testwriter, `composant`): the circuit plays that component from its testwriter
|
|
18
|
+
// (developpeur);
|
|
19
|
+
// - `ecarts` (`sujet`, `texte`): the body of the commit message, written by the circuit, each
|
|
20
|
+
// subject once, its value from the last sheet that carries it (developpeur, reviewer);
|
|
21
|
+
// - `decouvertes` (`sujet`, `corps`): each one a ticket filed closed under the ticket, labelled
|
|
22
|
+
// `decouverte`, created by the circuit (every role);
|
|
23
|
+
// - `verdict` (ACCEPTÉ, RENDU), `constats` (`titre`, `corps`, `genre` bloquant or remarque; a
|
|
24
|
+
// blocking one with `regle`, the criterion of the plan or the rule of the frame it holds, and
|
|
25
|
+
// `role` and `composant` its recipient, or `critere_en_cause`): as Conventional Comments mark
|
|
26
|
+
// a comment blocking or not, the circuit plays the blocking findings alone and writes the
|
|
27
|
+
// verdict at the ticket for the human, the remarks with it; a RENDU carries a blocking finding,
|
|
28
|
+
// an ACCEPTÉ none (reviewer);
|
|
29
|
+
// - `suivi` (`titre`, `etat` levé or non levé, `preuve`): after a review sent back, each finding
|
|
30
|
+
// before it judged on the difference of the lot, which the circuit writes at the ticket with
|
|
31
|
+
// the verdict, for the human (reviewer);
|
|
32
|
+
// - `question` (reviewer): what goes up to the owner, a real word of the language written in the
|
|
33
|
+
// code; the circuit stops at « besoin » before it commits or relaunches anyone;
|
|
34
|
+
// - `decision` (« RELANCE --reprendre » with its `contexte`, « RELANCE --depuis » with `depuis`,
|
|
35
|
+
// one or more components of the title, each with its `role` and its `contexte`, « MONTE »
|
|
36
|
+
// with `question`): what the circuit applies (regulator);
|
|
37
|
+
// - `faits` (`sujet` of SUJETS_DES_FAITS, `composant`): the facts of the state the decision
|
|
38
|
+
// rests on, which the circuit checks before it applies the decision, and compares with the
|
|
39
|
+
// facts of the RELANCE before on the same stop (regulator);
|
|
40
|
+
// - `issue` (QUESTIONS with its `questions`, each with what it aims at, `vise`; AUCUNE; DÉSACCORD
|
|
41
|
+
// with its `desaccord`): the circuit hands the questions to the author, ends the questioning,
|
|
42
|
+
// or sends it up to the supervisor, and writes the exchange at the ticket (questionneur);
|
|
43
|
+
// - `reponses` (`question`, `choix` CHANGE or GARDE, `texte`: what the proposal becomes, or why
|
|
44
|
+
// it stays): the circuit hands them to the questionneur and writes them at the ticket (the
|
|
45
|
+
// regulator, the technical referent);
|
|
46
|
+
// - `titre`, `description`: the plan as an answer changes it, which the circuit writes to the
|
|
47
|
+
// ticket (`bd update`) (technical referent).
|
|
48
|
+
// `lireFiche` reads a session's output: the sheet and the session's id, or the error that
|
|
49
|
+
// names what lacks or does not conform (no `structured_output`, a field out of the schema, a
|
|
50
|
+
// status without its reason, a blocking finding without its criterion or rule).
|
|
51
|
+
// - The findings of a guard, in SARIF 2.1.0 (OASIS), beside its output for the human: one run per
|
|
52
|
+
// guard, `tool.driver.name` the guard; each finding a result, `ruleId` the guard, `level`
|
|
53
|
+
// (error by default), `message.text`, its file and line as its location when it has them, and its
|
|
54
|
+
// properties: `regle` (the guard's own rule), `paquet` (the directory of the package that holds
|
|
55
|
+
// the file, null without a file) and `genre` (`a-corriger` by default, or `a-arbitrer`: a
|
|
56
|
+
// finding a person settles, never a developpeur). A guard writes them by `ecrireSarif` while the
|
|
57
|
+
// run names its results directory (RESULTATS, `<dossier>/sarif/<garde>-<pid>-<n>.sarif`), and
|
|
58
|
+
// nothing without it; a green guard writes its run, empty, so a reader knows it played. The
|
|
59
|
+
// frame's guards write through the known debt (scripts/dette.mjs), their runner
|
|
60
|
+
// (scripts/gardes-du-cadre.mjs) writes for a red guard that wrote none a finding without
|
|
61
|
+
// location, and a project's guards (its script `gardes`) import `ecrireSarif` from this module.
|
|
62
|
+
// - The summary of a run of Turborepo (`turbo run … --summarize`, `.turbo/runs/<id>.json`):
|
|
63
|
+
// `garderLesResumes` moves the summaries a run wrote into `<dossier>/turbo/`, and `lireResumes`
|
|
64
|
+
// gives each task, its package and directory, its script, its exit code, its cache status and
|
|
65
|
+
// its log file (from the root).
|
|
66
|
+
// - The JSON report of Vitest, which the frame's base configuration writes under
|
|
67
|
+
// `<dossier>/vitest/` while the variable is set (scripts/vitest-config/resultats.mjs):
|
|
68
|
+
// `lireRapportsVitest` gives each test file, its status, the message of a file that does not
|
|
69
|
+
// load, and each test, its title, its full name, its status and its first message.
|
|
70
|
+
import {
|
|
71
|
+
copyFileSync,
|
|
72
|
+
existsSync,
|
|
73
|
+
mkdirSync,
|
|
74
|
+
readdirSync,
|
|
75
|
+
readFileSync,
|
|
76
|
+
rmdirSync,
|
|
77
|
+
rmSync,
|
|
78
|
+
writeFileSync,
|
|
79
|
+
} from 'node:fs';
|
|
80
|
+
import path from 'node:path';
|
|
81
|
+
import { pathToFileURL } from 'node:url';
|
|
82
|
+
import { paquets } from './paquets.mjs';
|
|
83
|
+
import { RESULTATS, VITEST } from './vitest-config/resultats.mjs';
|
|
84
|
+
|
|
85
|
+
export { RESULTATS };
|
|
86
|
+
const SARIF = 'sarif';
|
|
87
|
+
const TURBO = 'turbo';
|
|
88
|
+
// The kinds of a guard's finding: to fix (a developpeur's work), to settle (a person's decision).
|
|
89
|
+
const GENRES = ['a-corriger', 'a-arbitrer'];
|
|
90
|
+
// The subjects of the facts a regulator's decision rests on, which the circuit checks on its
|
|
91
|
+
// state (scripts/circuit.mjs, `faitFaux`): all but sous-charge name their component.
|
|
92
|
+
export const SUJETS_DES_FAITS = [
|
|
93
|
+
'au-titre',
|
|
94
|
+
'hors-titre',
|
|
95
|
+
'rouge-de',
|
|
96
|
+
'sans-changement',
|
|
97
|
+
'sous-charge',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const texte = description => ({ type: 'string', description });
|
|
101
|
+
const liste = (items, description) => ({ type: 'array', description, items });
|
|
102
|
+
const objet = (properties, required = [], description) => ({
|
|
103
|
+
type: 'object',
|
|
104
|
+
...(description ? { description } : {}),
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
required,
|
|
107
|
+
properties,
|
|
108
|
+
});
|
|
109
|
+
const STATUT = {
|
|
110
|
+
statut: { type: 'string', enum: ['FAIT', 'BLOQUÉ', 'BESOIN'] },
|
|
111
|
+
raison: texte('BLOQUÉ : ce qui reste hors de ta portée, et où.'),
|
|
112
|
+
question: texte('BESOIN : la question qui t’arrête, ses options et ta recommandation.'),
|
|
113
|
+
};
|
|
114
|
+
const DECOUVERTES = {
|
|
115
|
+
decouvertes: liste(
|
|
116
|
+
objet({ sujet: texte('Son sujet, une ligne.'), corps: texte('Son exemple et son adresse.') }, [
|
|
117
|
+
'sujet',
|
|
118
|
+
'corps',
|
|
119
|
+
]),
|
|
120
|
+
'Chaque découverte hors des critères du ticket ; le circuit la consigne fermée sous le ticket.'
|
|
121
|
+
),
|
|
122
|
+
};
|
|
123
|
+
const ECARTS = {
|
|
124
|
+
ecarts: liste(
|
|
125
|
+
objet(
|
|
126
|
+
{
|
|
127
|
+
sujet: texte(
|
|
128
|
+
'Ce qui s’écarte (une référence, un test, un plafond), nommé comme une fiche d’avant le nomme.'
|
|
129
|
+
),
|
|
130
|
+
texte: texte('L’écart tel que le lot le laisse, sa cause et le ticket qui le referme.'),
|
|
131
|
+
},
|
|
132
|
+
['sujet', 'texte']
|
|
133
|
+
),
|
|
134
|
+
'Ce que le message de commit nomme, une fois par sujet, de la dernière fiche qui le porte.'
|
|
135
|
+
),
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// The answers of an author to the questions of the questionneur.
|
|
139
|
+
const REPONSES = {
|
|
140
|
+
reponses: liste(
|
|
141
|
+
objet(
|
|
142
|
+
{
|
|
143
|
+
question: texte('La question, telle que le questionneur l’a posée.'),
|
|
144
|
+
choix: { type: 'string', enum: ['CHANGE', 'GARDE'] },
|
|
145
|
+
texte: texte('CHANGE : ce que ta proposition devient ; GARDE : pourquoi elle reste.'),
|
|
146
|
+
},
|
|
147
|
+
['question', 'choix', 'texte']
|
|
148
|
+
),
|
|
149
|
+
'Ta réponse à chaque question du questionneur.'
|
|
150
|
+
),
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// The schema of the sheet of each role the circuit leads.
|
|
154
|
+
const FICHES = {
|
|
155
|
+
testwriter: objet(
|
|
156
|
+
{
|
|
157
|
+
...STATUT,
|
|
158
|
+
verts_attendus: liste(
|
|
159
|
+
objet(
|
|
160
|
+
{
|
|
161
|
+
fichier: texte('Le fichier de tests, depuis la racine du dépôt.'),
|
|
162
|
+
test: texte(
|
|
163
|
+
'Le nom complet du test ; absent, chaque vert du fichier, s’il est un fichier que HEAD tient.'
|
|
164
|
+
),
|
|
165
|
+
raison: texte('Pourquoi ce test passe avant le code.'),
|
|
166
|
+
},
|
|
167
|
+
['fichier', 'raison']
|
|
168
|
+
),
|
|
169
|
+
'Chaque test vert avant le code, que la preuve du rouge admet.'
|
|
170
|
+
),
|
|
171
|
+
...DECOUVERTES,
|
|
172
|
+
},
|
|
173
|
+
['statut']
|
|
174
|
+
),
|
|
175
|
+
developpeur: objet(
|
|
176
|
+
{
|
|
177
|
+
...STATUT,
|
|
178
|
+
renvoi: objet(
|
|
179
|
+
{
|
|
180
|
+
role: { type: 'string', enum: ['testwriter'] },
|
|
181
|
+
composant: texte('Un composant du titre.'),
|
|
182
|
+
},
|
|
183
|
+
['role', 'composant'],
|
|
184
|
+
'Le rouge qui reste est dans les tests de ce composant, le tien ou un d’avant.'
|
|
185
|
+
),
|
|
186
|
+
...ECARTS,
|
|
187
|
+
...DECOUVERTES,
|
|
188
|
+
},
|
|
189
|
+
['statut']
|
|
190
|
+
),
|
|
191
|
+
relecteur: objet(
|
|
192
|
+
{
|
|
193
|
+
verdict: { type: 'string', enum: ['ACCEPTÉ', 'RENDU'] },
|
|
194
|
+
constats: liste(
|
|
195
|
+
objet(
|
|
196
|
+
{
|
|
197
|
+
titre: texte('Son adresse : fichier et ligne.'),
|
|
198
|
+
genre: {
|
|
199
|
+
type: 'string',
|
|
200
|
+
enum: ['bloquant', 'remarque'],
|
|
201
|
+
description:
|
|
202
|
+
'bloquant : il rend le lot ; remarque : écrite au ticket, elle ne rend pas le lot.',
|
|
203
|
+
},
|
|
204
|
+
regle: texte(
|
|
205
|
+
'Bloquant : le critère du plan (« critère 2 ») ou la règle du cadre (son document et sa règle) qu’il tient.'
|
|
206
|
+
),
|
|
207
|
+
role: { type: 'string', enum: ['testwriter', 'developpeur'] },
|
|
208
|
+
composant: texte('Le composant du titre où la correction s’écrit.'),
|
|
209
|
+
corps: texte('Sa preuve : son exemple et ce qu’il montre.'),
|
|
210
|
+
critere_en_cause: texte('Le critère lui-même en cause, au lieu d’un destinataire.'),
|
|
211
|
+
},
|
|
212
|
+
['titre', 'corps', 'genre']
|
|
213
|
+
),
|
|
214
|
+
'Chaque constat, bloquant ou remarque ; un verdict RENDU en porte au moins un bloquant, un verdict ACCEPTÉ aucun.'
|
|
215
|
+
),
|
|
216
|
+
suivi: liste(
|
|
217
|
+
objet(
|
|
218
|
+
{
|
|
219
|
+
titre: texte('Le titre du constat d’avant.'),
|
|
220
|
+
etat: { type: 'string', enum: ['levé', 'non levé'] },
|
|
221
|
+
preuve: texte('Ce que la différence du lot en montre.'),
|
|
222
|
+
},
|
|
223
|
+
['titre', 'etat']
|
|
224
|
+
),
|
|
225
|
+
'Relecture bornée : chaque constat de la relecture d’avant, levé ou non.'
|
|
226
|
+
),
|
|
227
|
+
question: texte(
|
|
228
|
+
'Ce qui revient au responsable (par exemple un vrai mot du langage écrit en dur) : la question, ses pièces et ta recommandation ; le circuit s’arrête avant de commiter ou de relancer.'
|
|
229
|
+
),
|
|
230
|
+
...ECARTS,
|
|
231
|
+
...DECOUVERTES,
|
|
232
|
+
},
|
|
233
|
+
['verdict']
|
|
234
|
+
),
|
|
235
|
+
regulateur: objet(
|
|
236
|
+
{
|
|
237
|
+
decision: { type: 'string', enum: ['RELANCE --reprendre', 'RELANCE --depuis', 'MONTE'] },
|
|
238
|
+
depuis: liste(
|
|
239
|
+
objet(
|
|
240
|
+
{
|
|
241
|
+
role: { type: 'string', enum: ['testwriter', 'developpeur'] },
|
|
242
|
+
composant: texte('Un composant du titre.'),
|
|
243
|
+
contexte: texte('Ce que l’agent relancé fait dans ce composant.'),
|
|
244
|
+
},
|
|
245
|
+
['role', 'composant', 'contexte']
|
|
246
|
+
),
|
|
247
|
+
'RELANCE --depuis : chaque composant que le lot reprend, avec l’agent qui le reprend et son contexte ; le circuit les rejoue dans l’ordre du titre, puis une seule passe finale.'
|
|
248
|
+
),
|
|
249
|
+
contexte: texte('RELANCE --reprendre : ce que l’agent repris fait dans son composant.'),
|
|
250
|
+
question: texte('MONTE : la question, ses pièces et ta recommandation.'),
|
|
251
|
+
arbitrage: texte(
|
|
252
|
+
'RELANCE : ce que tu tranches seul et sa source (la lettre d’un critère, un précédent écrit, une règle du cadre), en nommant chaque consommateur qu’il fixe ; le circuit l’écrit au ticket, tranché.'
|
|
253
|
+
),
|
|
254
|
+
critere: texte(
|
|
255
|
+
'RELANCE --depuis, son premier composant repris par son testwriter : le critère que tu ajoutes, sans son numéro ; le circuit l’écrit, numéroté, à la fin de la description.'
|
|
256
|
+
),
|
|
257
|
+
faits: liste(
|
|
258
|
+
objet(
|
|
259
|
+
{
|
|
260
|
+
sujet: { type: 'string', enum: SUJETS_DES_FAITS },
|
|
261
|
+
composant: texte('Le composant ou le paquet du fait ; sans objet pour sous-charge.'),
|
|
262
|
+
},
|
|
263
|
+
['sujet']
|
|
264
|
+
),
|
|
265
|
+
'Chaque fait de l’état sur lequel ta décision s’appuie : au-titre et hors-titre (un composant au titre ou non), rouge-de (un rouge de l’arrêt dans ce composant ou ce paquet), sans-changement (la copie ne change rien de ce composant), sous-charge (une commande de l’arrêt à repasser, sous charge). Le circuit vérifie chacun sur l’état avant d’appliquer ta décision, et refuse une décision qui s’appuie sur un fait faux.'
|
|
266
|
+
),
|
|
267
|
+
...REPONSES,
|
|
268
|
+
...DECOUVERTES,
|
|
269
|
+
},
|
|
270
|
+
['decision']
|
|
271
|
+
),
|
|
272
|
+
questionneur: objet(
|
|
273
|
+
{
|
|
274
|
+
issue: { type: 'string', enum: ['QUESTIONS', 'AUCUNE', 'DÉSACCORD'] },
|
|
275
|
+
questions: liste(
|
|
276
|
+
objet(
|
|
277
|
+
{
|
|
278
|
+
question: texte('Ta question, dans les mots de la proposition.'),
|
|
279
|
+
vise: { type: 'string', enum: ['flou', 'intention', 'référence', 'règle', 'critère'] },
|
|
280
|
+
},
|
|
281
|
+
['question', 'vise']
|
|
282
|
+
),
|
|
283
|
+
'QUESTIONS : tes questions de ce tour.'
|
|
284
|
+
),
|
|
285
|
+
desaccord: texte(
|
|
286
|
+
'DÉSACCORD : ce qui reste, que la réponse de l’auteur ne lève pas, et pourquoi.'
|
|
287
|
+
),
|
|
288
|
+
},
|
|
289
|
+
['issue']
|
|
290
|
+
),
|
|
291
|
+
'referent-technique': objet(
|
|
292
|
+
{
|
|
293
|
+
...REPONSES,
|
|
294
|
+
titre: texte('Le titre du ticket que ta réponse change, entier.'),
|
|
295
|
+
description: texte('La description du ticket que ta réponse change, entière.'),
|
|
296
|
+
...DECOUVERTES,
|
|
297
|
+
},
|
|
298
|
+
['reponses']
|
|
299
|
+
),
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// The schema of the sheet of <role>, or null for a role the circuit does not lead.
|
|
303
|
+
export const schemaDeFiche = role => FICHES[role] ?? null;
|
|
304
|
+
|
|
305
|
+
// The errors of <valeur> against <schema>, each naming its field by its path <ou>.
|
|
306
|
+
function erreurs(schema, valeur, ou) {
|
|
307
|
+
const nom = ou || 'la fiche';
|
|
308
|
+
if (schema.type === 'object') {
|
|
309
|
+
if (!valeur || typeof valeur !== 'object' || Array.isArray(valeur))
|
|
310
|
+
return [`${nom} : un objet`];
|
|
311
|
+
const out = [];
|
|
312
|
+
for (const k of schema.required ?? []) {
|
|
313
|
+
if (!(k in valeur)) out.push(`${ou ? `${ou}.` : ''}${k} : manque`);
|
|
314
|
+
}
|
|
315
|
+
for (const [k, v] of Object.entries(valeur)) {
|
|
316
|
+
const sous = schema.properties[k];
|
|
317
|
+
const chemin = `${ou ? `${ou}.` : ''}${k}`;
|
|
318
|
+
if (!sous) out.push(`${chemin} : hors du schéma`);
|
|
319
|
+
else out.push(...erreurs(sous, v, chemin));
|
|
320
|
+
}
|
|
321
|
+
return out;
|
|
322
|
+
}
|
|
323
|
+
if (schema.type === 'array') {
|
|
324
|
+
if (!Array.isArray(valeur)) return [`${nom} : une liste`];
|
|
325
|
+
return valeur.flatMap((v, i) => erreurs(schema.items, v, `${ou}[${i}]`));
|
|
326
|
+
}
|
|
327
|
+
if (typeof valeur !== 'string') return [`${nom} : un texte`];
|
|
328
|
+
if (schema.enum && !schema.enum.includes(valeur)) {
|
|
329
|
+
return [`${nom} : « ${valeur} » n'est pas l'un de ${schema.enum.join(', ')}`];
|
|
330
|
+
}
|
|
331
|
+
return [];
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// What a sheet of <role> needs beyond its schema: the reason of a BLOQUÉ, the question of a
|
|
335
|
+
// BESOIN or a MONTE, the agents of a RELANCE --depuis, each with its own context and none beside
|
|
336
|
+
// them, the component of a fact, the questions of a QUESTIONS, the text of a DÉSACCORD, the plan
|
|
337
|
+
// an answer CHANGE gives, the criterion or rule of a blocking finding, and a reviewer's verdict
|
|
338
|
+
// that follows its findings (RENDU with a blocking one, ACCEPTÉ without).
|
|
339
|
+
function manques(role, f) {
|
|
340
|
+
const out = [];
|
|
341
|
+
const plein = k => typeof f[k] === 'string' && f[k].trim() !== '';
|
|
342
|
+
if (f.statut === 'BLOQUÉ' && !plein('raison')) out.push('raison : manque, pour BLOQUÉ');
|
|
343
|
+
if (f.statut === 'BESOIN' && !plein('question')) out.push('question : manque, pour BESOIN');
|
|
344
|
+
if (role === 'regulateur' && f.decision === 'MONTE' && !plein('question')) {
|
|
345
|
+
out.push('question : manque, pour MONTE');
|
|
346
|
+
}
|
|
347
|
+
if (role === 'regulateur' && f.decision === 'RELANCE --depuis' && !f.depuis?.length) {
|
|
348
|
+
out.push('depuis : manque, pour RELANCE --depuis');
|
|
349
|
+
}
|
|
350
|
+
if (role === 'regulateur' && f.decision === 'RELANCE --depuis' && 'contexte' in f) {
|
|
351
|
+
out.push('contexte : sous RELANCE --depuis, chaque composant de depuis porte le sien');
|
|
352
|
+
}
|
|
353
|
+
if (role === 'relecteur') {
|
|
354
|
+
const constats = f.constats ?? [];
|
|
355
|
+
for (const [i, c] of constats.entries()) {
|
|
356
|
+
if (c.genre === 'bloquant' && !(typeof c.regle === 'string' && c.regle.trim())) {
|
|
357
|
+
out.push(`constats[${i}].regle : manque, pour un constat bloquant`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const bloque = constats.some(c => c.genre === 'bloquant');
|
|
361
|
+
if (f.verdict === 'RENDU' && !bloque) {
|
|
362
|
+
out.push('verdict : RENDU sans constat bloquant ; un lot sans constat bloquant est ACCEPTÉ');
|
|
363
|
+
}
|
|
364
|
+
if (f.verdict === 'ACCEPTÉ' && bloque) {
|
|
365
|
+
out.push('verdict : ACCEPTÉ avec un constat bloquant, qui rend le lot');
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
for (const [i, x] of (role === 'regulateur' ? (f.faits ?? []) : []).entries()) {
|
|
369
|
+
if (x.sujet !== 'sous-charge' && !(typeof x.composant === 'string' && x.composant.trim())) {
|
|
370
|
+
out.push(`faits[${i}].composant : manque, pour ${x.sujet}`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
if (role === 'questionneur' && f.issue === 'QUESTIONS' && !(f.questions ?? []).length) {
|
|
374
|
+
out.push('questions : manque, pour QUESTIONS');
|
|
375
|
+
}
|
|
376
|
+
if (role === 'questionneur' && f.issue === 'DÉSACCORD' && !plein('desaccord')) {
|
|
377
|
+
out.push('desaccord : manque, pour DÉSACCORD');
|
|
378
|
+
}
|
|
379
|
+
if (
|
|
380
|
+
role === 'referent-technique' &&
|
|
381
|
+
(f.reponses ?? []).some(r => r.choix === 'CHANGE') &&
|
|
382
|
+
!plein('titre') &&
|
|
383
|
+
!plein('description')
|
|
384
|
+
) {
|
|
385
|
+
out.push('titre ou description : manque, pour une réponse CHANGE');
|
|
386
|
+
}
|
|
387
|
+
return out;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// The sheet of <role> in <sortie>, what claude prints with `--output-format json`: `{ fiche,
|
|
391
|
+
// seance }`, or `{ erreur }` naming what lacks or does not conform.
|
|
392
|
+
export function lireFiche(role, sortie) {
|
|
393
|
+
const schema = schemaDeFiche(role);
|
|
394
|
+
if (!schema) return { erreur: `le rôle ${role} n'a pas de fiche` };
|
|
395
|
+
let resultat;
|
|
396
|
+
try {
|
|
397
|
+
resultat = JSON.parse(String(sortie ?? ''));
|
|
398
|
+
} catch {
|
|
399
|
+
return { erreur: 'la sortie de la séance n’est pas le résultat JSON de claude' };
|
|
400
|
+
}
|
|
401
|
+
if (!resultat || typeof resultat !== 'object' || !('structured_output' in resultat)) {
|
|
402
|
+
return { erreur: 'le résultat de la séance n’a pas de structured_output : aucune fiche' };
|
|
403
|
+
}
|
|
404
|
+
const fiche = resultat.structured_output;
|
|
405
|
+
const fautes = erreurs(schema, fiche, '');
|
|
406
|
+
if (!fautes.length) fautes.push(...manques(role, fiche));
|
|
407
|
+
if (fautes.length) return { erreur: `fiche non conforme : ${fautes.join(' ; ')}` };
|
|
408
|
+
return { fiche, seance: resultat.session_id ?? null };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// The directory of the package of <racine> that holds <fichier> (from the root), null for none.
|
|
412
|
+
function paquetDe(racine, fichier) {
|
|
413
|
+
if (!fichier) return null;
|
|
414
|
+
const p = paquets(racine)
|
|
415
|
+
.filter(({ dossier }) => fichier === dossier || fichier.startsWith(`${dossier}/`))
|
|
416
|
+
.sort((a, b) => b.dossier.length - a.dossier.length)[0];
|
|
417
|
+
return p ? p.dossier : null;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// The SARIF 2.1.0 of the findings <constats> of <garde> in <racine>: each `{ fichier, ligne,
|
|
421
|
+
// message, regle, niveau, genre }`, all but the message optional.
|
|
422
|
+
export function sarif({ garde, constats, racine = process.cwd() }) {
|
|
423
|
+
return {
|
|
424
|
+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
425
|
+
version: '2.1.0',
|
|
426
|
+
runs: [
|
|
427
|
+
{
|
|
428
|
+
tool: { driver: { name: garde } },
|
|
429
|
+
results: constats.map(c => ({
|
|
430
|
+
ruleId: garde,
|
|
431
|
+
level: c.niveau ?? 'error',
|
|
432
|
+
message: { text: String(c.message ?? '') },
|
|
433
|
+
...(c.fichier
|
|
434
|
+
? {
|
|
435
|
+
locations: [
|
|
436
|
+
{
|
|
437
|
+
physicalLocation: {
|
|
438
|
+
artifactLocation: { uri: c.fichier },
|
|
439
|
+
...(c.ligne ? { region: { startLine: c.ligne } } : {}),
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
],
|
|
443
|
+
}
|
|
444
|
+
: {}),
|
|
445
|
+
properties: {
|
|
446
|
+
...(c.regle ? { regle: c.regle } : {}),
|
|
447
|
+
paquet: paquetDe(racine, c.fichier),
|
|
448
|
+
genre: c.genre ?? GENRES[0],
|
|
449
|
+
},
|
|
450
|
+
})),
|
|
451
|
+
},
|
|
452
|
+
],
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
let numero = 0;
|
|
457
|
+
// Writes the SARIF of the findings <constats> of <garde> under the results directory <env> names:
|
|
458
|
+
// its path, or null without the variable.
|
|
459
|
+
export function ecrireSarif({ garde, constats, racine = process.cwd(), env = process.env }) {
|
|
460
|
+
const dossier = env[RESULTATS];
|
|
461
|
+
if (!dossier) return null;
|
|
462
|
+
const f = path.join(dossier, SARIF, `${garde}-${process.pid}-${++numero}.sarif`);
|
|
463
|
+
mkdirSync(path.dirname(f), { recursive: true });
|
|
464
|
+
writeFileSync(f, `${JSON.stringify(sarif({ garde, constats, racine }), null, 2)}\n`);
|
|
465
|
+
return f;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const fichiers = (dossier, extension) =>
|
|
469
|
+
existsSync(dossier)
|
|
470
|
+
? readdirSync(dossier)
|
|
471
|
+
.filter(f => f.endsWith(extension))
|
|
472
|
+
.sort()
|
|
473
|
+
.map(f => path.join(dossier, f))
|
|
474
|
+
: [];
|
|
475
|
+
const lireJson = f => {
|
|
476
|
+
try {
|
|
477
|
+
return JSON.parse(readFileSync(f, 'utf8'));
|
|
478
|
+
} catch {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
// The guards that wrote their SARIF under the results directory <dossier>, and their findings:
|
|
484
|
+
// `{ gardes, constats }`, each finding `{ garde, regle, fichier, ligne, niveau, message, paquet,
|
|
485
|
+
// genre }`, `fichier`, `ligne`, `regle` and `paquet` null when the guard gave none.
|
|
486
|
+
export function lireSarif(dossier) {
|
|
487
|
+
const gardes = new Set();
|
|
488
|
+
const constats = [];
|
|
489
|
+
for (const f of fichiers(path.join(dossier, SARIF), '.sarif')) {
|
|
490
|
+
for (const run of lireJson(f)?.runs ?? []) {
|
|
491
|
+
const garde = run.tool?.driver?.name ?? path.basename(f);
|
|
492
|
+
gardes.add(garde);
|
|
493
|
+
for (const r of run.results ?? []) {
|
|
494
|
+
const lieu = r.locations?.[0]?.physicalLocation;
|
|
495
|
+
constats.push({
|
|
496
|
+
garde: r.ruleId ?? garde,
|
|
497
|
+
regle: r.properties?.regle ?? null,
|
|
498
|
+
fichier: lieu?.artifactLocation?.uri ?? null,
|
|
499
|
+
ligne: lieu?.region?.startLine ?? null,
|
|
500
|
+
niveau: r.level ?? 'error',
|
|
501
|
+
message: String(r.message?.text ?? ''),
|
|
502
|
+
paquet: r.properties?.paquet ?? null,
|
|
503
|
+
genre: r.properties?.genre ?? GENRES[0],
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return { gardes: [...gardes].sort(), constats };
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// Moves the summaries of Turborepo that a run in <racine> wrote (`.turbo/runs/*.json` absent from
|
|
512
|
+
// <avant>, the names there before it) into the results directory <env> names; removes them
|
|
513
|
+
// without it. `.turbo/runs` goes too when the run created it (<avant> empty) and it is left empty.
|
|
514
|
+
export function garderLesResumes(racine, avant, env = process.env) {
|
|
515
|
+
const runs = path.join(racine, '.turbo', 'runs');
|
|
516
|
+
const neufs = (existsSync(runs) ? readdirSync(runs) : []).filter(
|
|
517
|
+
f => !avant.has(f) && f.endsWith('.json')
|
|
518
|
+
);
|
|
519
|
+
const dossier = env[RESULTATS];
|
|
520
|
+
for (const f of neufs) {
|
|
521
|
+
if (dossier) {
|
|
522
|
+
mkdirSync(path.join(dossier, TURBO), { recursive: true });
|
|
523
|
+
// The results directory may lie on another file system: a copy, then the removal.
|
|
524
|
+
copyFileSync(path.join(runs, f), path.join(dossier, TURBO, f));
|
|
525
|
+
rmSync(path.join(runs, f), { force: true });
|
|
526
|
+
} else rmSync(path.join(runs, f), { force: true });
|
|
527
|
+
}
|
|
528
|
+
if (neufs.length && !avant.size && existsSync(runs) && readdirSync(runs).length === 0) {
|
|
529
|
+
rmdirSync(runs);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
// The tasks of the summaries of Turborepo under the results directory <dossier>, in their order:
|
|
534
|
+
// `{ tache, paquet, dossier, script, code, cache, journal }`, `dossier` « . » for the root,
|
|
535
|
+
// `code` null for a task that did not run.
|
|
536
|
+
export function lireResumes(dossier) {
|
|
537
|
+
return fichiers(path.join(dossier, TURBO), '.json').flatMap(f =>
|
|
538
|
+
(lireJson(f)?.tasks ?? []).map(t => ({
|
|
539
|
+
tache: t.taskId,
|
|
540
|
+
paquet: t.package,
|
|
541
|
+
dossier: t.directory || '.',
|
|
542
|
+
script: t.task,
|
|
543
|
+
code: t.execution?.exitCode ?? null,
|
|
544
|
+
cache: t.cache?.status ?? null,
|
|
545
|
+
journal: t.logFile ?? null,
|
|
546
|
+
}))
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// The test files of the reports of Vitest under the results directory <dossier>: `{ fichier,
|
|
551
|
+
// etat, message, tests }`, `fichier` its absolute path, each test `{ titre, nom, etat, message }`.
|
|
552
|
+
export function lireRapportsVitest(dossier) {
|
|
553
|
+
return fichiers(path.join(dossier, VITEST), '.json').flatMap(f =>
|
|
554
|
+
(lireJson(f)?.testResults ?? []).map(r => ({
|
|
555
|
+
fichier: r.name,
|
|
556
|
+
etat: r.status,
|
|
557
|
+
message: String(r.message ?? ''),
|
|
558
|
+
tests: (r.assertionResults ?? []).map(a => ({
|
|
559
|
+
titre: a.title,
|
|
560
|
+
nom: a.fullName || a.title,
|
|
561
|
+
etat: a.status,
|
|
562
|
+
message: String(a.failureMessages?.[0] ?? ''),
|
|
563
|
+
})),
|
|
564
|
+
}))
|
|
565
|
+
);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
569
|
+
const [option, role] = process.argv.slice(2);
|
|
570
|
+
const schema = option === '--schema' ? schemaDeFiche(role) : null;
|
|
571
|
+
if (schema) process.stdout.write(`${JSON.stringify(schema)}\n`);
|
|
572
|
+
else {
|
|
573
|
+
process.stderr.write(
|
|
574
|
+
option === '--schema'
|
|
575
|
+
? `⛔ le rôle ${role} n'a pas de fiche.\n`
|
|
576
|
+
: 'usage: resultats.mjs --schema <rôle>\n'
|
|
577
|
+
);
|
|
578
|
+
process.exitCode = 1;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The first sentence of the summary of each published element holds in 80 characters, as Debian's
|
|
3
|
+
// lintian holds a package's synopsis (`synopsis-too-long`) and clippy a doc's first paragraph
|
|
4
|
+
// (`too_long_first_doc_paragraph`). The index of the interfaces (`docs/agents/index-des-interfaces.md`)
|
|
5
|
+
// and the section « Ce que les composants d'avant publient » of a role agent's instruction carry
|
|
6
|
+
// that sentence for every element (scripts/index-interfaces.mjs).
|
|
7
|
+
//
|
|
8
|
+
// npx grillhouse resumes run from the repository root (a guard of grillhouse:gardes)
|
|
9
|
+
//
|
|
10
|
+
// - An element is an entry of the generated part of a component's `docs/INTERFACE.md`; its
|
|
11
|
+
// sentence is the one the index keeps (`lire` of scripts/index-interfaces.mjs), measured in
|
|
12
|
+
// characters. The generated part is the TSDoc summary of the export, which
|
|
13
|
+
// scripts/interfaces-contre-code.mjs keeps equal to the code.
|
|
14
|
+
// - A refusal names the element at its interface (`<INTERFACE.md> § <element>`): the sentence is
|
|
15
|
+
// written in the documentation comment of the export, and `npm run api` carries it over.
|
|
16
|
+
// - A refusal inscribed in the project's debt (`grillhouse-dette.json`, see `dette.mjs`) passes,
|
|
17
|
+
// keyed by the interface and `resume-trop-long`.
|
|
18
|
+
// - In the envelope of a component (`GRILLHOUSE_ENVELOPPE`), that component alone is judged.
|
|
19
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
import { pathToFileURL } from 'node:url';
|
|
22
|
+
import { trancher } from './dette.mjs';
|
|
23
|
+
import { lire } from './index-interfaces.mjs';
|
|
24
|
+
import { aVerifier } from './interfaces-contre-code.mjs';
|
|
25
|
+
|
|
26
|
+
// The longest first sentence of a summary, in characters.
|
|
27
|
+
export const LONGUEUR_MAX = 80;
|
|
28
|
+
|
|
29
|
+
// The findings of the components <racine> judges under <env>: one `{ fichier, regle, message }`
|
|
30
|
+
// per element whose first sentence is longer than LONGUEUR_MAX, in the debt's form (`dette.mjs`).
|
|
31
|
+
export function verifier(racine, env = process.env) {
|
|
32
|
+
const constats = [];
|
|
33
|
+
for (const c of aVerifier(racine, env)) {
|
|
34
|
+
const iface = path.join(c.dir, 'docs', 'INTERFACE.md');
|
|
35
|
+
if (!existsSync(iface)) continue;
|
|
36
|
+
const relIface = path.relative(racine, iface);
|
|
37
|
+
const longs = lire(relIface, readFileSync(iface, 'utf8')).parties.filter(
|
|
38
|
+
p => p.titre && [...p.phrase].length > LONGUEUR_MAX
|
|
39
|
+
);
|
|
40
|
+
for (const p of longs) {
|
|
41
|
+
constats.push({
|
|
42
|
+
fichier: relIface,
|
|
43
|
+
regle: 'resume-trop-long',
|
|
44
|
+
message: `${relIface} § ${p.titre} : la première phrase du résumé fait ${[...p.phrase].length} caractères, pour ${LONGUEUR_MAX} au plus ; raccourcis la première phrase dans le commentaire de documentation de l’export, le détail va dans les phrases suivantes, puis npm run api.`,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return constats;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
52
|
+
const racine = process.cwd();
|
|
53
|
+
const { refus, lignes } = trancher(racine, 'resumes', verifier(racine));
|
|
54
|
+
for (const r of refus) process.stderr.write(`⛔ ${r.message}\n`);
|
|
55
|
+
for (const l of lignes) process.stderr.write(`${l}\n`);
|
|
56
|
+
process.exitCode = refus.length ? 1 : 0;
|
|
57
|
+
}
|