@roomi-fields/grillhouse 0.23.1 → 0.24.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.
Files changed (64) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/METACADRE.md +1 -1
  3. package/agents/explorateur.md +4 -2
  4. package/agents/questionneur.md +1 -1
  5. package/bin/grillhouse.mjs +3 -58
  6. package/cadre/templates/projet/CLAUDE.md +5 -1
  7. package/cadre/templates/projet/docs/agents/issue-tracker.md +4 -4
  8. package/charte.md +23 -6
  9. package/docs/ARCHITECTURE.md +127 -1
  10. package/hooks/register.tsx +3 -13
  11. package/package.json +13 -1
  12. package/scripts/bilan.mjs +134 -88
  13. package/scripts/circuit.mjs +1547 -2248
  14. package/scripts/composants-affectes.mjs +27 -108
  15. package/scripts/consommateurs-arbitres.mjs +15 -21
  16. package/scripts/consommateurs.d.ts +8 -0
  17. package/scripts/consommateurs.mjs +5 -1
  18. package/scripts/dette.d.ts +23 -0
  19. package/scripts/dette.mjs +76 -13
  20. package/scripts/droits.mjs +38 -44
  21. package/scripts/elements-du-lot.mjs +246 -0
  22. package/scripts/enveloppe/enveloppe.mjs +105 -232
  23. package/scripts/enveloppe/lancer.sh +79 -49
  24. package/scripts/enveloppe/paquets-du-plan.mjs +49 -0
  25. package/scripts/etapes.mjs +148 -213
  26. package/scripts/gardes-du-cadre.mjs +8 -6
  27. package/scripts/grillhouse-maj.mjs +31 -9
  28. package/scripts/index-interfaces.mjs +21 -1
  29. package/scripts/integration/integrer.mjs +2 -2
  30. package/scripts/interfaces-contre-code.mjs +25 -17
  31. package/scripts/migrations/0.23.0-config-vitest.mjs +26 -23
  32. package/scripts/migrations/0.23.2-copies-du-cadre.mjs +201 -0
  33. package/scripts/migrations/0.24.0-circuit-simplifie.mjs +231 -0
  34. package/scripts/migrations.mjs +41 -6
  35. package/scripts/paquets.d.ts +5 -0
  36. package/scripts/paquets.mjs +3 -0
  37. package/scripts/references-par-les-types.mjs +55 -36
  38. package/scripts/registre.mjs +7 -15
  39. package/scripts/resultats.mjs +283 -142
  40. package/scripts/resumes.mjs +1 -1
  41. package/scripts/session-start.sh +1 -2
  42. package/scripts/sources.mjs +28 -0
  43. package/scripts/sous-commandes.mjs +62 -0
  44. package/scripts/tableau.mjs +85 -191
  45. package/scripts/tests.mjs +19 -17
  46. package/scripts/verifier.mjs +5 -4
  47. package/scripts/verrou-de-machine.mjs +131 -24
  48. package/scripts/verrous/verrou.mjs +66 -5
  49. package/skills/developpeur/SKILL.md +60 -50
  50. package/skills/explorateur/SKILL.md +90 -64
  51. package/skills/grill/SKILL.md +1 -1
  52. package/skills/mesure/SKILL.md +2 -1
  53. package/skills/pitmaster/SKILL.md +132 -118
  54. package/skills/pitmaster/references/consigne-agent.md +54 -148
  55. package/skills/questionneur/SKILL.md +18 -18
  56. package/skills/relecteur/SKILL.md +69 -23
  57. package/skills/testwriter/SKILL.md +46 -35
  58. package/types/index.d.ts +1 -3
  59. package/agents/referent-technique.md +0 -9
  60. package/agents/regulateur.md +0 -11
  61. package/scripts/enveloppe/composant.mjs +0 -11
  62. package/scripts/etat-du-circuit.mjs +0 -104
  63. package/skills/referent-technique/SKILL.md +0 -145
  64. package/skills/regulateur/SKILL.md +0 -220
@@ -5,49 +5,66 @@
5
5
  //
6
6
  // npx grillhouse resultats --schema <rôle> prints the JSON schema of a role's sheet
7
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
8
+ // - The sheet of the end of a session (« la fiche ») of the explorateur, the questionneur, the
9
+ // testwriter, the developpeur and the reviewer: the schema of each role (FICHES), which the session receives
10
10
  // (`claude -p --output-format json --json-schema <schéma>`) and fills as its last act; claude
11
11
  // writes it in the field `structured_output` of its result (Claude Code, « headless »). A field
12
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);
13
+ // - `statut` (FAIT, BLOQUÉ, BESOIN), with `raison` for BLOQUÉ and `question` for BESOIN, each
14
+ // with its `piece`: the circuit goes on, or stops at « hors-portee » or « besoin », the piece
15
+ // with the stop (testwriter, developpeur);
15
16
  // - `verts_attendus` (`fichier` from the root, `test` its full name, or none for every green of a
16
17
  // 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);
18
+ // - `renvoi` (`role` testwriter): the circuit relaunches the testwriter, then the proof and the
19
+ // developpeur (developpeur);
19
20
  // - `ecarts` (`sujet`, `texte`): the body of the commit message, written by the circuit, each
20
21
  // subject once, its value from the last sheet that carries it (developpeur, reviewer);
21
22
  // - `decouvertes` (`sujet`, `corps`): each one a ticket filed closed under the ticket, labelled
22
23
  // `decouverte`, created by the circuit (every role);
23
- // - `verdict` (ACCEPTÉ, RENDU), `constats` (`titre`, `corps`, `genre` bloquant or remarque; a
24
+ // - `verdict` (ACCEPTÉ, RENDU), `constats` (`titre`, `corps`, `piece`, `genre` bloquant or
25
+ // remarque; a
24
26
  // 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);
27
+ // `role` its recipient, or `critere_en_cause`): as Conventional Comments mark
28
+ // a comment blocking or not, the circuit plays the blocking findings alone, each open until a
29
+ // review marks it lifted, and writes the verdict at the ticket for the human, the remarks with
30
+ // it; a RENDU carries a blocking finding, an ACCEPTÉ none (reviewer);
31
+ // - `conformite` (`element`, `conforme` oui or non, `document` CADRE, INTERFACE or
32
+ // ARCHITECTURE, `reference`, `piece`; a « non » with `role` its recipient):
33
+ // one line per element of the lot (scripts/elements-du-lot.mjs), its rule cited from a
34
+ // document of its package, a new export's the entry of the INTERFACE that names it; a line
35
+ // without its reference is refused; a « non » is a blocking finding (`bloquantsDeLaFiche`),
36
+ // and the circuit refuses a sheet without a line for each element (`conformiteDuLot`)
37
+ // (reviewer);
29
38
  // - `suivi` (`titre`, `etat` levé or non levé, `preuve`): after a review sent back, each finding
30
39
  // before it judged on the difference of the lot, which the circuit writes at the ticket with
31
40
  // 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).
41
+ // - `question`, with its `piece` (reviewer): what goes up to the owner, a real word of the
42
+ // language written in the code; the circuit stops at « besoin » before it commits or
43
+ // relaunches anyone;
44
+ // - `criteres`, `paquets`, `questions` (each with its `piece`): the plan, which the circuit
45
+ // writes at the ticket: the criteria it adds after those the ticket holds, which the circuit
46
+ // keeps as they are; the packages in the title as it names its components (a package name or a
47
+ // path), the list the envelope of every later session writes; the open questions, a change or
48
+ // a removal of a criterion written among them, go up to the supervisor (explorateur);
49
+ // - `reponses` (`numero` of the question, `reponse` CHANGE or GARDE, `texte`): the explorateur's
50
+ // one answer to the questionneur, which the circuit writes at the ticket; a question without
51
+ // an answer stays open (explorateur);
52
+ // - `issue` (QUESTIONS with its `questions`, each with what it aims at, `vise`, and its
53
+ // `piece`; AUCUNE; DÉSACCORD
54
+ // with its `desaccord`): the explorateur answers the questions, the disagreement as one, and
55
+ // the circuit writes them at the ticket (questionneur).
48
56
  // `lireFiche` reads a session's output: the sheet and the session's id, or the error that
49
57
  // 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).
58
+ // status without its reason, a blocking finding without its criterion or rule, a line of
59
+ // conformity without its rule or a « non » without its recipient, a plan without packages, a
60
+ // fact without its piece, a piece in neither form, and, given the copy, a file piece not in it).
61
+ // - The piece of a fact (« la pièce »), one format, as a code review cites the line it judges: a
62
+ // finding, a question or a stop carries it, so the agent who takes the fact up re-reads it
63
+ // before he builds on it (BP 320.9: a line read wrong, copied by five agents up to the owner).
64
+ // Either `<fichier>:<ligne>` or `<fichier>:<ligne>-<fin>`, the file from the root of the copy,
65
+ // the line from 1, then, after a space, what it shows (« packages/a/src/x.ts:3 divise par sa valeur. »); or `$ <commande>` on
66
+ // its first line and its output on the lines after. `lirePiece` reads it; the circuit gives
67
+ // `lireFiche` the copy, where each file piece's file stands and holds its line.
51
68
  // - The findings of a guard, in SARIF 2.1.0 (OASIS), beside its output for the human: one run per
52
69
  // guard, `tool.driver.name` the guard; each finding a result, `ruleId` the guard, `level`
53
70
  // (error by default), `message.text`, its file and line as its location when it has them, and its
@@ -75,6 +92,7 @@ import {
75
92
  readFileSync,
76
93
  rmdirSync,
77
94
  rmSync,
95
+ statSync,
78
96
  writeFileSync,
79
97
  } from 'node:fs';
80
98
  import path from 'node:path';
@@ -85,18 +103,12 @@ import { RESULTATS, VITEST } from './vitest-config/resultats.mjs';
85
103
  export { RESULTATS };
86
104
  const SARIF = 'sarif';
87
105
  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
-
106
+ // The kind of a note of a guard, no red: a key of the known debt inscribed above what the guard
107
+ // finds, which the circuit lowers (scripts/dette.mjs, scripts/circuit.mjs).
108
+ export const A_ELAGUER = 'a-elaguer';
109
+ // The kinds of a guard's finding: to fix (a developpeur's work), to settle (a person's decision),
110
+ // to lower (a note).
111
+ const GENRES = ['a-corriger', 'a-arbitrer', A_ELAGUER];
100
112
  const texte = description => ({ type: 'string', description });
101
113
  const liste = (items, description) => ({ type: 'array', description, items });
102
114
  const objet = (properties, required = [], description) => ({
@@ -106,10 +118,15 @@ const objet = (properties, required = [], description) => ({
106
118
  required,
107
119
  properties,
108
120
  });
121
+ // The description of a piece in a sheet's schema, the format `lirePiece` reads.
122
+ const PIECE =
123
+ 'fichier:ligne depuis la racine de la copie, puis ce qu’elle montre ; ou « $ <commande> » et sa sortie aux lignes suivantes';
124
+ const piece = quoi => texte(`${quoi} : ${PIECE}.`);
109
125
  const STATUT = {
110
126
  statut: { type: 'string', enum: ['FAIT', 'BLOQUÉ', 'BESOIN'] },
111
127
  raison: texte('BLOQUÉ : ce qui reste hors de ta portée, et où.'),
112
128
  question: texte('BESOIN : la question qui t’arrête, ses options et ta recommandation.'),
129
+ piece: piece('BLOQUÉ ou BESOIN : la pièce de ta raison ou de ta question'),
113
130
  };
114
131
  const DECOUVERTES = {
115
132
  decouvertes: liste(
@@ -135,23 +152,46 @@ const ECARTS = {
135
152
  ),
136
153
  };
137
154
 
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
- };
155
+ // The documents a line of a reviewer's conformity cites its rule from.
156
+ const DOCUMENTS = ['CADRE', 'INTERFACE', 'ARCHITECTURE'];
152
157
 
153
158
  // The schema of the sheet of each role the circuit leads.
154
159
  const FICHES = {
160
+ explorateur: objet(
161
+ {
162
+ criteres: liste(
163
+ texte('Un critère, sans son numéro : un comportement que ses tests mesurent.'),
164
+ 'Les critères que le plan ajoute, dans leur ordre ; le circuit garde tels quels ceux que le ticket porte et écrit les tiens après eux. Changer ou retirer un critère écrit est une question ouverte.'
165
+ ),
166
+ paquets: liste(
167
+ texte('Un paquet, comme le titre le nomme : le nom de son paquet, ou son chemin.'),
168
+ 'Les paquets à toucher, le fournisseur d’abord ; le circuit les écrit au titre, et chaque séance n’écrit qu’eux.'
169
+ ),
170
+ questions: liste(
171
+ objet(
172
+ {
173
+ question: texte('La question qui reste ouverte, ses options et ta recommandation.'),
174
+ piece: piece('Sa pièce'),
175
+ },
176
+ ['question', 'piece']
177
+ ),
178
+ 'Les questions ouvertes du plan, que tu ne tranches pas ; elles montent au superviseur.'
179
+ ),
180
+ reponses: liste(
181
+ objet(
182
+ {
183
+ numero: texte('Le numéro de la question, « 1 ».'),
184
+ reponse: { type: 'string', enum: ['CHANGE', 'GARDE'] },
185
+ texte: texte('CHANGE : ce que le plan devient ; GARDE : pourquoi il reste tel quel.'),
186
+ },
187
+ ['numero', 'reponse', 'texte']
188
+ ),
189
+ 'Ta réponse, une fois, à chaque question du questionneur.'
190
+ ),
191
+ ...DECOUVERTES,
192
+ },
193
+ ['criteres', 'paquets', 'questions']
194
+ ),
155
195
  testwriter: objet(
156
196
  {
157
197
  ...STATUT,
@@ -176,12 +216,9 @@ const FICHES = {
176
216
  {
177
217
  ...STATUT,
178
218
  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.'
219
+ { role: { type: 'string', enum: ['testwriter'] } },
220
+ ['role'],
221
+ 'Le rouge qui reste est dans les tests du lot : le circuit relance le testwriter.'
185
222
  ),
186
223
  ...ECARTS,
187
224
  ...DECOUVERTES,
@@ -191,10 +228,35 @@ const FICHES = {
191
228
  relecteur: objet(
192
229
  {
193
230
  verdict: { type: 'string', enum: ['ACCEPTÉ', 'RENDU'] },
231
+ conformite: liste(
232
+ objet(
233
+ {
234
+ element: texte('L’élément, tel que la liste des éléments du lot l’écrit.'),
235
+ conforme: {
236
+ type: 'string',
237
+ enum: ['oui', 'non'],
238
+ description: 'non : il rend le lot, à son destinataire.',
239
+ },
240
+ document: {
241
+ type: 'string',
242
+ enum: DOCUMENTS,
243
+ description:
244
+ 'Le document de son paquet dont la règle est citée ; un export neuf, l’INTERFACE.',
245
+ },
246
+ reference: texte(
247
+ 'La règle dans ce document (« 000-forms, R2 », « § Les frontières ») ; un export neuf, le nom de son entrée.'
248
+ ),
249
+ piece: piece('Sa pièce'),
250
+ role: { type: 'string', enum: ['testwriter', 'developpeur'] },
251
+ },
252
+ ['element', 'conforme', 'document', 'reference', 'piece']
253
+ ),
254
+ 'Une ligne par élément du lot, chacun une fois : sa conformité à l’architecture.'
255
+ ),
194
256
  constats: liste(
195
257
  objet(
196
258
  {
197
- titre: texte('Son adresse : fichier et ligne.'),
259
+ titre: texte('Son sujet, une ligne.'),
198
260
  genre: {
199
261
  type: 'string',
200
262
  enum: ['bloquant', 'remarque'],
@@ -204,12 +266,16 @@ const FICHES = {
204
266
  regle: texte(
205
267
  '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
268
  ),
207
- role: { type: 'string', enum: ['testwriter', 'developpeur'] },
208
- composant: texte('Le composant du titre où la correction s’écrit.'),
269
+ role: {
270
+ type: 'string',
271
+ enum: ['testwriter', 'developpeur'],
272
+ description: 'Bloquant : l’agent qui écrit la correction, à qui le circuit le rend.',
273
+ },
209
274
  corps: texte('Sa preuve : son exemple et ce qu’il montre.'),
275
+ piece: piece('Sa pièce'),
210
276
  critere_en_cause: texte('Le critère lui-même en cause, au lieu d’un destinataire.'),
211
277
  },
212
- ['titre', 'corps', 'genre']
278
+ ['titre', 'corps', 'genre', 'piece']
213
279
  ),
214
280
  'Chaque constat, bloquant ou remarque ; un verdict RENDU en porte au moins un bloquant, un verdict ACCEPTÉ aucun.'
215
281
  ),
@@ -225,50 +291,14 @@ const FICHES = {
225
291
  'Relecture bornée : chaque constat de la relecture d’avant, levé ou non.'
226
292
  ),
227
293
  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.'
294
+ 'Ce qui revient au responsable (par exemple un vrai mot du langage écrit en dur) : la question et ta recommandation ; le circuit s’arrête avant de commiter ou de relancer.'
229
295
  ),
296
+ piece: piece('question : sa pièce'),
230
297
  ...ECARTS,
231
298
  ...DECOUVERTES,
232
299
  },
233
300
  ['verdict']
234
301
  ),
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
302
  questionneur: objet(
273
303
  {
274
304
  issue: { type: 'string', enum: ['QUESTIONS', 'AUCUNE', 'DÉSACCORD'] },
@@ -277,26 +307,16 @@ const FICHES = {
277
307
  {
278
308
  question: texte('Ta question, dans les mots de la proposition.'),
279
309
  vise: { type: 'string', enum: ['flou', 'intention', 'référence', 'règle', 'critère'] },
310
+ piece: piece('Sa pièce, ce qui dans la proposition ou le dépôt la fait naître'),
280
311
  },
281
- ['question', 'vise']
312
+ ['question', 'vise', 'piece']
282
313
  ),
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.'
314
+ 'QUESTIONS : tes questions.'
287
315
  ),
316
+ desaccord: texte('DÉSACCORD : l’écart que la proposition laisse, et pourquoi.'),
288
317
  },
289
318
  ['issue']
290
319
  ),
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
320
  };
301
321
 
302
322
  // The schema of the sheet of <role>, or null for a role the circuit does not lead.
@@ -331,24 +351,134 @@ function erreurs(schema, valeur, ou) {
331
351
  return [];
332
352
  }
333
353
 
354
+ // The rule a line of conformity <l> cites: its document and its reference.
355
+ export const regleDeLaLigne = l => `${l.document} ${l.reference}`;
356
+
357
+ // The blocking findings of a reviewer's sheet <fiche>, the ones that send the lot back: its
358
+ // findings of `genre` bloquant, then each line of its conformity that says « non », as a finding
359
+ // on its element, its rule, its recipient and its piece.
360
+ export const bloquantsDeLaFiche = fiche => [
361
+ ...(fiche.constats ?? []).filter(c => c.genre === 'bloquant'),
362
+ ...(fiche.conformite ?? [])
363
+ .filter(l => l.conforme === 'non')
364
+ .map(l => ({
365
+ titre: l.element,
366
+ genre: 'bloquant',
367
+ regle: regleDeLaLigne(l),
368
+ role: l.role,
369
+ corps: `Non conforme : ${l.piece}`,
370
+ })),
371
+ ];
372
+
373
+ // What makes the reviewer's sheet <fiche> out of conformity with the elements of its lot
374
+ // <elements> (scripts/elements-du-lot.mjs: `{ id, nom, statut, exporte }`), as a review asks a
375
+ // word on every changed file: an element without its line, a line on an element out of the list,
376
+ // a new export whose rule does not name its entry in the INTERFACE of its package. The fault, or
377
+ // null.
378
+ export function conformiteDuLot(fiche, elements) {
379
+ const lignes = fiche.conformite ?? [];
380
+ const ids = new Set(elements.map(e => e.id));
381
+ const fautes = [];
382
+ const sans = elements.filter(e => !lignes.some(l => l.element === e.id)).map(e => e.id);
383
+ if (sans.length) fautes.push(`conformite : sans ligne pour ${sans.join(', ')}`);
384
+ for (const [i, l] of lignes.entries()) {
385
+ if (!ids.has(l.element)) {
386
+ fautes.push(`conformite[${i}].element : ${l.element} n'est pas un élément du lot`);
387
+ }
388
+ }
389
+ for (const e of elements.filter(x => x.statut === 'neuf' && x.exporte)) {
390
+ for (const l of lignes.filter(x => x.element === e.id)) {
391
+ if (!(l.document === 'INTERFACE' && l.reference.includes(e.nom))) {
392
+ fautes.push(
393
+ `conformite : ${e.id} est un export neuf ; sa règle nomme son entrée « ${e.nom} » dans l'INTERFACE de son paquet`
394
+ );
395
+ }
396
+ }
397
+ }
398
+ return fautes.length ? fautes.join(' ; ') : null;
399
+ }
400
+
401
+ // The piece of a fact <texte>: `{ fichier, ligne }` for « <fichier>:<ligne> », `{ fichier, ligne,
402
+ // fin }` for « <fichier>:<ligne>-<fin> » (as SARIF's region has a start line and an optional end
403
+ // line), then what it shows after a space, the place in backquotes or followed by punctuation
404
+ // read the same; `{ commande, sortie }` for « $ <commande> » and its output on the lines after;
405
+ // null for a text in neither form.
406
+ export function lirePiece(texte) {
407
+ const t = String(texte ?? '').trim();
408
+ if (t.startsWith('$ ')) {
409
+ const saut = t.indexOf('\n');
410
+ const commande = saut < 0 ? '' : t.slice(2, saut).trim();
411
+ const sortie = saut < 0 ? '' : t.slice(saut + 1).trim();
412
+ return commande && sortie ? { commande, sortie } : null;
413
+ }
414
+ let lieu = t.split(/\s/)[0];
415
+ while (lieu && '`.,;:)'.includes(lieu.at(-1))) lieu = lieu.slice(0, -1);
416
+ while (lieu.startsWith('`')) lieu = lieu.slice(1);
417
+ const deux = lieu.lastIndexOf(':');
418
+ const [debut, fin, ...reste] = lieu.slice(deux + 1).split('-');
419
+ const nombre = n => (/^\d+$/.test(n ?? '') && Number(n) >= 1 ? Number(n) : null);
420
+ const ligne = nombre(debut);
421
+ if (deux < 1 || !ligne || reste.length) return null;
422
+ if (fin === undefined) return { fichier: lieu.slice(0, deux), ligne };
423
+ if (!nombre(fin) || nombre(fin) < ligne) return null;
424
+ return { fichier: lieu.slice(0, deux), ligne, fin: nombre(fin) };
425
+ }
426
+
427
+ // The pieces of the sheet <f>, each `[son chemin, son texte]`: of the stop it carries (a status,
428
+ // a reviewer's question), of its findings, of its lines of conformity, of its questions.
429
+ const piecesDe = f => [
430
+ ...('piece' in f ? [['piece', f.piece]] : []),
431
+ ...['constats', 'conformite', 'questions'].flatMap(k =>
432
+ (f[k] ?? []).map((x, i) => [`${k}[${i}].piece`, x.piece]).filter(([, p]) => p !== undefined)
433
+ ),
434
+ ];
435
+
436
+ // What makes a file piece of the sheet <f> not found in the copy <copie>: a file absent from it,
437
+ // or out of it, or a line (the end of a range) past its end; each fault, naming its piece.
438
+ function introuvables(f, copie) {
439
+ const out = [];
440
+ const racine = path.resolve(copie);
441
+ for (const [ou, p] of piecesDe(f)) {
442
+ const lu = lirePiece(p);
443
+ if (!lu?.fichier) continue;
444
+ const abs = path.resolve(racine, lu.fichier);
445
+ const lieu = `${lu.fichier}:${lu.ligne}${lu.fin ? `-${lu.fin}` : ''}`;
446
+ if (!abs.startsWith(`${racine}${path.sep}`) || !existsSync(abs) || !statSync(abs).isFile()) {
447
+ out.push(`${ou} : ${lieu} introuvable dans la copie : pas de tel fichier`);
448
+ continue;
449
+ }
450
+ const contenu = readFileSync(abs, 'utf8');
451
+ const lignes = contenu.split('\n').length - (contenu.endsWith('\n') ? 1 : 0);
452
+ if ((lu.fin ?? lu.ligne) > lignes) {
453
+ out.push(`${ou} : ${lieu} introuvable dans la copie : le fichier a ${lignes} lignes`);
454
+ }
455
+ }
456
+ return out;
457
+ }
458
+
334
459
  // 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).
460
+ // BESOIN, the packages of a plan and the number of each answer, the questions of a QUESTIONS, the text of a
461
+ // DÉSACCORD, the criterion or rule of a blocking finding, the reference of the rule each line of
462
+ // a reviewer's conformity cites in its document and the recipient of a « non », and a reviewer's verdict that follows its blocking findings, its « non » among them
463
+ // (RENDU with one, ACCEPTÉ without), the piece of a BLOQUÉ, a BESOIN and a reviewer's question,
464
+ // and each piece in one of its two forms (`lirePiece`).
339
465
  function manques(role, f) {
340
466
  const out = [];
341
467
  const plein = k => typeof f[k] === 'string' && f[k].trim() !== '';
342
468
  if (f.statut === 'BLOQUÉ' && !plein('raison')) out.push('raison : manque, pour BLOQUÉ');
343
469
  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');
470
+ if (['BLOQUÉ', 'BESOIN'].includes(f.statut) && !('piece' in f)) {
471
+ out.push(`piece : manque, pour ${f.statut}`);
346
472
  }
347
- if (role === 'regulateur' && f.decision === 'RELANCE --depuis' && !f.depuis?.length) {
348
- out.push('depuis : manque, pour RELANCE --depuis');
473
+ if (role === 'relecteur' && 'question' in f && !('piece' in f)) {
474
+ out.push('piece : manque, pour la question');
349
475
  }
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');
476
+ for (const [ou, p] of piecesDe(f)) {
477
+ if (!lirePiece(p)) {
478
+ out.push(
479
+ `${ou} : « ${p} » n'est pas une pièce (fichier:ligne, ou « $ <commande> » et sa sortie)`
480
+ );
481
+ }
352
482
  }
353
483
  if (role === 'relecteur') {
354
484
  const constats = f.constats ?? [];
@@ -357,7 +487,17 @@ function manques(role, f) {
357
487
  out.push(`constats[${i}].regle : manque, pour un constat bloquant`);
358
488
  }
359
489
  }
360
- const bloque = constats.some(c => c.genre === 'bloquant');
490
+ for (const [i, l] of (f.conformite ?? []).entries()) {
491
+ if (!l.reference.trim()) {
492
+ out.push(`conformite[${i}].reference : manque, la règle citée dans le ${l.document}`);
493
+ }
494
+ if (l.conforme === 'non') {
495
+ if (!(typeof l.role === 'string' && l.role.trim())) {
496
+ out.push(`conformite[${i}].role : manque, pour un élément non conforme`);
497
+ }
498
+ }
499
+ }
500
+ const bloque = bloquantsDeLaFiche(f).length > 0;
361
501
  if (f.verdict === 'RENDU' && !bloque) {
362
502
  out.push('verdict : RENDU sans constat bloquant ; un lot sans constat bloquant est ACCEPTÉ');
363
503
  }
@@ -365,9 +505,12 @@ function manques(role, f) {
365
505
  out.push('verdict : ACCEPTÉ avec un constat bloquant, qui rend le lot');
366
506
  }
367
507
  }
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}`);
508
+ if (role === 'explorateur') {
509
+ if (!(f.paquets ?? []).length) out.push('paquets : manque, pour un plan');
510
+ for (const [i, r] of (f.reponses ?? []).entries()) {
511
+ if (!/^[1-9]\d*$/.test(r.numero)) {
512
+ out.push(`reponses[${i}].numero : « ${r.numero} » n'est pas le numéro d'une question`);
513
+ }
371
514
  }
372
515
  }
373
516
  if (role === 'questionneur' && f.issue === 'QUESTIONS' && !(f.questions ?? []).length) {
@@ -376,20 +519,13 @@ function manques(role, f) {
376
519
  if (role === 'questionneur' && f.issue === 'DÉSACCORD' && !plein('desaccord')) {
377
520
  out.push('desaccord : manque, pour DÉSACCORD');
378
521
  }
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
522
  return out;
388
523
  }
389
524
 
390
525
  // 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) {
526
+ // seance }`, or `{ erreur }` naming what lacks or does not conform; with <copie>, the directory
527
+ // of the copy, each file piece of the sheet stands in it, its file and its line.
528
+ export function lireFiche(role, sortie, { copie } = {}) {
393
529
  const schema = schemaDeFiche(role);
394
530
  if (!schema) return { erreur: `le rôle ${role} n'a pas de fiche` };
395
531
  let resultat;
@@ -404,6 +540,7 @@ export function lireFiche(role, sortie) {
404
540
  const fiche = resultat.structured_output;
405
541
  const fautes = erreurs(schema, fiche, '');
406
542
  if (!fautes.length) fautes.push(...manques(role, fiche));
543
+ if (!fautes.length && copie) fautes.push(...introuvables(fiche, copie));
407
544
  if (fautes.length) return { erreur: `fiche non conforme : ${fautes.join(' ; ')}` };
408
545
  return { fiche, seance: resultat.session_id ?? null };
409
546
  }
@@ -418,7 +555,8 @@ function paquetDe(racine, fichier) {
418
555
  }
419
556
 
420
557
  // 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.
558
+ // message, regle, niveau, genre, trouve }`, all but the message optional (`trouve`, the count a
559
+ // note of the debt finds).
422
560
  export function sarif({ garde, constats, racine = process.cwd() }) {
423
561
  return {
424
562
  $schema: 'https://json.schemastore.org/sarif-2.1.0.json',
@@ -446,6 +584,7 @@ export function sarif({ garde, constats, racine = process.cwd() }) {
446
584
  ...(c.regle ? { regle: c.regle } : {}),
447
585
  paquet: paquetDe(racine, c.fichier),
448
586
  genre: c.genre ?? GENRES[0],
587
+ ...(c.trouve !== undefined ? { trouve: c.trouve } : {}),
449
588
  },
450
589
  })),
451
590
  },
@@ -482,7 +621,8 @@ const lireJson = f => {
482
621
 
483
622
  // The guards that wrote their SARIF under the results directory <dossier>, and their findings:
484
623
  // `{ gardes, constats }`, each finding `{ garde, regle, fichier, ligne, niveau, message, paquet,
485
- // genre }`, `fichier`, `ligne`, `regle` and `paquet` null when the guard gave none.
624
+ // genre }`, `fichier`, `ligne`, `regle` and `paquet` null when the guard gave none, and `trouve`,
625
+ // the count a note of the debt finds, when it carries one.
486
626
  export function lireSarif(dossier) {
487
627
  const gardes = new Set();
488
628
  const constats = [];
@@ -501,6 +641,7 @@ export function lireSarif(dossier) {
501
641
  message: String(r.message?.text ?? ''),
502
642
  paquet: r.properties?.paquet ?? null,
503
643
  genre: r.properties?.genre ?? GENRES[0],
644
+ ...(r.properties?.trouve !== undefined ? { trouve: r.properties.trouve } : {}),
504
645
  });
505
646
  }
506
647
  }
@@ -15,7 +15,7 @@
15
15
  // written in the documentation comment of the export, and `npm run api` carries it over.
16
16
  // - A refusal inscribed in the project's debt (`grillhouse-dette.json`, see `dette.mjs`) passes,
17
17
  // keyed by the interface and `resume-trop-long`.
18
- // - In the envelope of a component (`GRILLHOUSE_ENVELOPPE`), that component alone is judged.
18
+ // - In an envelope (`GRILLHOUSE_ENVELOPPE`), the components of its plan alone are judged.
19
19
  import { existsSync, readFileSync } from 'node:fs';
20
20
  import path from 'node:path';
21
21
  import { pathToFileURL } from 'node:url';
@@ -27,8 +27,7 @@
27
27
  # A session of a role agent of the frame (`claude --agent <rôle>`, the circuit's sessions: the
28
28
  # hook's input names it, `agent_type`, bare or as `grillhouse:<rôle>`, and the frame's plugin
29
29
  # defines it, `$CLAUDE_PLUGIN_ROOT/agents/<rôle>.md`, the plugin whose hook runs this script) receives nothing:
30
- # what the project lacks is the supervisor's to deal with, and in an envelope the documents of the
31
- # hidden packages read as absent.
30
+ # what the project lacks is the supervisor's to deal with.
32
31
  set -uo pipefail
33
32
 
34
33
  # The project is Claude Code's, else the working directory; the frame's scripts are this one's