@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
package/scripts/bilan.mjs CHANGED
@@ -16,24 +16,26 @@
16
16
  // `circuit` of the steps the circuit leads itself; the results the circuit keeps,
17
17
  // `.claude/worktrees/<ticket>.fiches.jsonl`, each with its time: the launch of each session the
18
18
  // circuit opens or resumes (`lancement`: its role and its session id, the register line's
19
- // `agent`), the sheet of each session (`{ role, composant, fiche }`), the outcome of each proof
19
+ // `agent`), the sheet of each session (`{ role, fiche }`), the outcome of each proof
20
20
  // (`resultat`: `ok`, its counts, `vide`) and of each check (`resultat`: `issue` vert, rouge or a-repasser, `finale`), each stop
21
21
  // (`arret`, its name, `finale`), each developpeur resumed who changed nothing (`inchange`), the
22
- // end of each questioning (`questionnement`: its `objet`, plan or decision, its `tours`, whether
23
- // it changed the proposal, `change`, and its `issue`, AUCUNE, DÉSACCORD or MONTE), each end of a
24
- // run on a stop it hands to the supervisor (`montee`: its code and its stop); the circuit's
25
- // state, `.claude/worktrees/<ticket>.circuit.json`, for where the circuit ended. Every
26
- // agent declares nothing: the bilan measures.
27
- // - The steps and the roles are those of scripts/etapes.mjs. Only the circuit's roles count: an
28
- // exploration, before the circuit, is out of it. Only the sessions the circuit launched count in
22
+ // end of the questioning of the plan (`questionnement`: its `objet`, the questions the
23
+ // questionneur asked, `questions`, whether an answer changed the plan, `change`, its `issue`,
24
+ // AUCUNE, QUESTIONS or DÉSACCORD, and the questions left open, `ouvertes`), each end of a run on
25
+ // a stop (`montee`: its code, its stop and who must answer its question, `qui`); the circuit's
26
+ // state, `.claude/worktrees/<ticket>.circuit.json`, for where the circuit ended; the night the
27
+ // project declares in its package.json (`grillhouse.plageDeNuit`). Every agent declares
28
+ // nothing: the bilan measures.
29
+ // - The steps and the roles are those of scripts/etapes.mjs. Only the circuit's roles count, the
30
+ // explorateur of the plan among them. Only the sessions the circuit launched count in
29
31
  // the window, the categories, the tokens and the cost; a session of a circuit's role on the
30
32
  // ticket the circuit did not launch, the supervisor's, counts apart (`seances.horsCircuit`: its
31
33
  // number, by role, its cost).
32
34
  // - The window: from the first turn of a session or a step to the last result of the circuit or
33
35
  // the last turn; its length is the wall time.
34
36
  // - Each turn of a session and each step finds its result: a step, the last outcome or stop of
35
- // its step and component kept during it; a session, the first sheet of its role kept after its
36
- // turn, before the next turn of any other, which names its component. A step `controles` whose
37
+ // its step kept during it, before the next step of its name; a session, the first sheet of its
38
+ // role kept after its turn, before the next turn of any other. Every step is one for the whole ticket. A step `controles` whose
37
39
  // outcome or stop is the final pass's (`finale`) is the final pass.
38
40
  // - The categories, each instant once:
39
41
  // - travail resté au lot: a session that delivered, the reviewer's included;
@@ -42,26 +44,28 @@
42
44
  // proof finds the lot of the proof before it (the same counts of reds, greens of the lot,
43
45
  // broken files and greens of HEAD), after a refused proof, or no file of the lot (`vide`:
44
46
  // reds that exist already or of a build alone, no green of the lot); that proof; checks that
45
- // play the same red again on a component none of its agents delivered to since its checks
46
- // before (the final pass: none of any component's since the final pass before);
47
- // - a session of the regulator, its answers to the questionneur among them, takes the category of
48
- // what its decision gives, as a developer's: relance sans effet when the first session or step
49
- // after its arbitrage, before the circuit ends, is without effect or a replay; travail resté au
50
- // lot otherwise, a question sent up to the supervisor included;
51
- // - questionnement: a session of the questionneur, and of the referent who answers it for the
52
- // plan (kept: a look that did not write the proposal, as a review);
53
- // - attente humaine: from each end of a run on a stop (`montee`) to the first turn of a session or
54
- // step after it, or the next result of the circuit, the first of the two;
47
+ // play the same red again with no delivery of the testwriter or the developpeur since the
48
+ // checks of their kind before (the final pass: since the final pass before);
49
+ // - questionnement: a session of the questionneur (kept: a look that did not write the plan, as
50
+ // a review);
51
+ // - attente du superviseur, attente du responsable, nuit: as the lead time of DORA separates the
52
+ // wait for review from the work, each wait runs from an end of a run on a stop (`montee`) to
53
+ // the first turn of a session or step after it, or the next result of the circuit, the first
54
+ // of the two; it is the wait of who must answer its question (`qui`, the owner or the
55
+ // supervisor, as the circuit writes it), but its part in the night: the hours of `grillhouse.plageDeNuit` in the
56
+ // project's package.json (`{ debut, fin }`, in local time), from 23 h to 8 h without it;
55
57
  // - étapes automatiques: the other steps the circuit leads, but the final passes;
56
58
  // - passes finales: the final passes, but those without effect or replayed;
57
59
  // - rejeu d'étapes déjà vertes: the same, on a lot unchanged since green: checks green again,
58
60
  // a testwriter run again on a proven lot, its proof;
59
61
  // - entre deux étapes: the rest of the window.
60
62
  // What is lost is the wall time out of the work, the questioning, the automatic steps and the
61
- // final passes.
63
+ // final passes. The text gives the wall time and the wall time out of the night spent waiting
64
+ // (« hors nuit d'attente »: a night at work stays in it), the time lost and the time lost out of
65
+ // the waits.
62
66
  // - The bilan counts the sessions by role, the steps by their titles, the stops by kind (`arret`),
63
- // the ends on a stop (the waits), the rounds of questioning, the proposals they changed and those
64
- // they sent up to the supervisor, the tokens, the cache reads and the cost (a session without a cost
67
+ // the ends on a stop (the waits), the questions of the plan asked, the plans they changed and the
68
+ // questions left open, the tokens, the cache reads and the cost (a session without a cost
65
69
  // counts 0); a session's tokens go to its turns by their time.
66
70
  // - It is kept in the main tree, `.claude/worktrees/<ticket>.bilan.json`, and compares itself with
67
71
  // the latest bilan kept for another ticket of the project, or with nothing: no figure of
@@ -72,14 +76,13 @@ import os from 'node:os';
72
76
  import path from 'node:path';
73
77
  import { pathToFileURL } from 'node:url';
74
78
  import {
75
- ARBITRAGE,
79
+ AGENTS_DU_LOT,
76
80
  etapeDuCircuit,
77
- etapeDuRole,
78
- juge,
79
81
  nomDuRole,
80
82
  QUESTIONNEUR,
81
- REGULATEUR,
83
+ RESPONSABLE,
82
84
  SUITE,
85
+ SUPERVISEUR,
83
86
  } from './etapes.mjs';
84
87
 
85
88
  // The role of a register line that a step of the circuit writes, and of its results.
@@ -93,7 +96,9 @@ const CATEGORIES = [
93
96
  ['travail', 'travail resté au lot'],
94
97
  ['sansEffet', 'relance sans effet'],
95
98
  ['questionnement', 'questionnement'],
96
- ['attente', 'attente humaine'],
99
+ [SUPERVISEUR, 'attente du superviseur'],
100
+ [RESPONSABLE, 'attente du responsable'],
101
+ ['nuit', 'nuit'],
97
102
  ['automatique', 'étapes automatiques'],
98
103
  ['finales', 'passes finales'],
99
104
  ['rejeu', "rejeu d'étapes déjà vertes"],
@@ -101,9 +106,50 @@ const CATEGORIES = [
101
106
  ];
102
107
  // The categories of the time kept: the rest is lost.
103
108
  const GARDE = new Set(['travail', 'automatique', 'finales', 'questionnement']);
104
- // The roles of the circuit, by the steps they work at: every step but the exploration, and the
105
- // arbitrage.
106
- const ROLES = new Set([...SUITE, ARBITRAGE].filter(e => e.numero !== '0').flatMap(e => e.qui));
109
+ // The categories of the waits: the rest of the time lost is lost out of them.
110
+ const ATTENTES = [SUPERVISEUR, RESPONSABLE, 'nuit'];
111
+ // The night when the project declares none: from 23 h to 8 h, in local time.
112
+ const NUIT = Object.freeze({ debut: 23, fin: 8 });
113
+
114
+ // The night the project at <racine> declares in its package.json (`grillhouse.plageDeNuit`, `{
115
+ // debut, fin }`, hours from 0 to 23 in local time, distinct; a night that crosses midnight has its
116
+ // start after its end), else NUIT.
117
+ export function plageDeNuit(racine) {
118
+ let p = null;
119
+ try {
120
+ p = JSON.parse(readFileSync(path.join(racine, 'package.json'), 'utf8'))?.grillhouse
121
+ ?.plageDeNuit;
122
+ } catch {
123
+ p = null;
124
+ }
125
+ const heure = h => Number.isInteger(h) && h >= 0 && h <= 23;
126
+ return heure(p?.debut) && heure(p?.fin) && p.debut !== p.fin
127
+ ? { debut: p.debut, fin: p.fin }
128
+ : NUIT;
129
+ }
130
+
131
+ // The parts of the interval [d, f] in the night <nuit>, each night from its start hour of a day to
132
+ // its end hour, of the next day when it crosses midnight, in local time.
133
+ function nuits([d, f], nuit) {
134
+ const out = [];
135
+ const jour = new Date(d);
136
+ jour.setHours(0, 0, 0, 0);
137
+ jour.setDate(jour.getDate() - 1);
138
+ while (jour.getTime() < f) {
139
+ const a = new Date(jour);
140
+ a.setHours(nuit.debut);
141
+ const b = new Date(jour);
142
+ if (nuit.fin < nuit.debut) b.setDate(b.getDate() + 1);
143
+ b.setHours(nuit.fin);
144
+ const x = Math.max(a.getTime(), d);
145
+ const y = Math.min(b.getTime(), f);
146
+ if (y > x) out.push([x, y]);
147
+ jour.setDate(jour.getDate() + 1);
148
+ }
149
+ return out;
150
+ }
151
+ // The roles of the circuit, by the steps they work at.
152
+ const ROLES = new Set(SUITE.flatMap(e => e.qui));
107
153
 
108
154
  // The results the circuit kept, `{ t, …entrée }`, in their order; an entry without a time counts
109
155
  // nothing.
@@ -139,8 +185,16 @@ const commun = ([d, f], l) =>
139
185
  longueur(l.map(([a, b]) => [Math.max(a, d), Math.min(b, f)]).filter(([a, b]) => b > a));
140
186
 
141
187
  // The bilan of <ticket> from the register's lines <lignes> (of its project), the results the
142
- // circuit kept <entrees> (`<ticket>.fiches.jsonl`) and the circuit's state <etat> (or null).
143
- export function bilan({ ticket, lignes, entrees = [], etat = null, maintenant = Date.now() }) {
188
+ // circuit kept <entrees> (`<ticket>.fiches.jsonl`), the circuit's state <etat> (or null) and the
189
+ // project's night <nuit>.
190
+ export function bilan({
191
+ ticket,
192
+ lignes,
193
+ entrees = [],
194
+ etat = null,
195
+ nuit = NUIT,
196
+ maintenant = Date.now(),
197
+ }) {
144
198
  // The sessions the circuit launched, by their id.
145
199
  const lancees = new Set(
146
200
  entrees.filter(e => e.role === CIRCUIT && e.lancement).map(e => e.lancement.seance)
@@ -163,7 +217,6 @@ export function bilan({ ticket, lignes, entrees = [], etat = null, maintenant =
163
217
  f,
164
218
  role: l.role,
165
219
  etape: l.role === CIRCUIT ? l.etape : null,
166
- composant: l.role === CIRCUIT ? l.composant : null,
167
220
  }))
168
221
  )
169
222
  .filter(s => s.f > s.d)
@@ -177,21 +230,23 @@ export function bilan({ ticket, lignes, entrees = [], etat = null, maintenant =
177
230
  const duCircuit = gardes.filter(e => e.role === CIRCUIT);
178
231
  for (const s of segments) {
179
232
  if (s.role === CIRCUIT) {
233
+ // The next step of the same name: its result is not this one's, even within the gap.
234
+ const apres =
235
+ segments.find(x => x.role === CIRCUIT && x.etape === s.etape && x.d > s.d)?.d ?? Infinity;
180
236
  s.entree =
181
237
  duCircuit.findLast(
182
238
  e =>
183
239
  (e.resultat || e.arret) &&
184
240
  e.etape === s.etape &&
185
- e.composant === s.composant &&
186
241
  e.t >= s.d - JEU &&
187
- e.t <= s.f + JEU
242
+ e.t <= s.f + JEU &&
243
+ e.t <= apres
188
244
  ) ?? null;
189
245
  s.finale = s.etape === 'controles' && Boolean(s.entree?.resultat?.finale ?? s.entree?.finale);
190
246
  } else {
191
247
  s.suivant = segments.find(x => x.d >= s.f && x.l !== s.l)?.d ?? Infinity;
192
248
  s.entree =
193
249
  gardes.find(e => e.role === s.role && e.t >= s.f - JEU && e.t <= s.suivant + JEU) ?? null;
194
- s.composant = s.entree?.composant ?? null;
195
250
  }
196
251
  }
197
252
  const etapes = segments.filter(s => s.role === CIRCUIT);
@@ -199,7 +254,7 @@ export function bilan({ ticket, lignes, entrees = [], etat = null, maintenant =
199
254
  for (const p of preuves) p.preuve = p.entree?.resultat ?? null;
200
255
  // A proof and the category it gives the testwriter before it: the lot unchanged since the proof
201
256
  // before, or none.
202
- const avantLa = p => preuves.findLast(x => x.composant === p.composant && x.f <= p.d);
257
+ const avantLa = p => preuves.findLast(x => x.f <= p.d);
203
258
  const parLaPreuve = p => {
204
259
  const avant = avantLa(p);
205
260
  if (!p.preuve) return null;
@@ -214,42 +269,31 @@ export function bilan({ ticket, lignes, entrees = [], etat = null, maintenant =
214
269
  duCircuit.some(
215
270
  e =>
216
271
  e.etape === 'developpeur' &&
217
- e.composant === s.composant &&
218
272
  (e.arret === 'sans-livraison' || e.inchange) &&
219
273
  e.t >= s.f - JEU &&
220
274
  e.t <= s.suivant + JEU
221
275
  );
222
276
 
223
277
  // The sessions first: the checks read what they delivered.
224
- for (const s of segments.filter(x => x.role !== CIRCUIT && x.role !== REGULATEUR)) {
225
- if (juge(s.role)) s.categorie = 'questionnement';
278
+ for (const s of segments.filter(x => x.role !== CIRCUIT)) {
279
+ if (s.role === QUESTIONNEUR) s.categorie = 'questionnement';
226
280
  else if (s.role === 'developpeur') {
227
281
  s.categorie = sansLivraison(s) ? 'sansEffet' : 'travail';
228
282
  } else if (s.role === 'testwriter') {
229
- const p = preuves.find(x => x.composant === s.composant && x.d >= s.f - JEU);
283
+ const p = preuves.find(x => x.d >= s.f - JEU);
230
284
  s.categorie = p ? (parLaPreuve(p) ?? (p.preuve?.vide ? 'sansEffet' : 'travail')) : 'travail';
231
285
  } else s.categorie = 'travail';
232
286
  }
233
- // A session that changed a component's lot.
234
- const livre = s =>
235
- s.role !== CIRCUIT && s.categorie === 'travail' && etapeDuRole(s.role)?.parComposant;
287
+ // A session that changed the lot: the testwriter's or the developpeur's, that delivered.
288
+ const livre = s => AGENTS_DU_LOT.includes(s.role) && s.categorie === 'travail';
236
289
  for (const s of etapes) {
237
290
  if (s.etape === 'preuve') {
238
291
  s.categorie = parLaPreuve(s) ?? 'automatique';
239
292
  } else if (s.etape === 'controles') {
240
293
  const memes = x => x.etape === 'controles' && x.finale === s.finale;
241
- const avant = etapes.findLast(
242
- x => memes(x) && x.f <= s.d && (s.finale || x.composant === s.composant)
243
- );
294
+ const avant = etapes.findLast(x => memes(x) && x.f <= s.d);
244
295
  const change =
245
- !avant ||
246
- segments.some(
247
- x =>
248
- livre(x) &&
249
- x.d >= avant.f - JEU &&
250
- x.f <= s.d + JEU &&
251
- (s.finale || x.composant === s.composant)
252
- );
296
+ !avant || segments.some(x => livre(x) && x.d >= avant.f - JEU && x.f <= s.d + JEU);
253
297
  const i = s.entree ? issue(s.entree) : 'rouge';
254
298
  s.categorie =
255
299
  change || !avant.entree || issue(avant.entree) !== i
@@ -262,32 +306,27 @@ export function bilan({ ticket, lignes, entrees = [], etat = null, maintenant =
262
306
  } else s.categorie = 'automatique';
263
307
  }
264
308
 
265
- // The ends of the runs on a stop, each the start of a wait for the supervisor.
266
- const montees = duCircuit.filter(e => e.montee).map(e => e.t);
267
- // The regulator's sessions last: each takes the category of the first session or step after its
268
- // arbitrage, before the run ends.
269
- const arbitrage = x => x.role === REGULATEUR || x.role === QUESTIONNEUR;
270
- for (const s of segments.filter(x => x.role === REGULATEUR)) {
271
- const suite = segments.find(x => x.d >= s.f - JEU && !arbitrage(x));
272
- const coupe = suite && montees.some(t => t >= s.f - JEU && t <= suite.d);
273
- s.categorie =
274
- suite && !coupe && (suite.categorie === 'sansEffet' || suite.categorie === 'rejeu')
275
- ? 'sansEffet'
276
- : 'travail';
277
- }
278
-
279
- // The waits for the supervisor: from each end of a run on a stop to the answer, the run after.
280
- const attentes = [];
281
- for (const t of montees) {
282
- const tour = segments.find(s => s.d > t)?.d ?? Infinity;
283
- const suivant = gardes.find(e => e.t > t)?.t ?? Infinity;
309
+ // The waits: from each end of a run on a stop to the answer, the run after, each for who must
310
+ // answer its question (`qui`, the owner or the supervisor).
311
+ const lesAttentes = [];
312
+ for (const e of duCircuit.filter(x => x.montee)) {
313
+ const tour = segments.find(s => s.d > e.t)?.d ?? Infinity;
314
+ const suivant = gardes.find(x => x.t > e.t)?.t ?? Infinity;
284
315
  const f = Math.min(tour, suivant, fin);
285
- if (f > t) attentes.push([t, f]);
316
+ const { qui } = e.montee;
317
+ if (f > e.t) lesAttentes.push({ i: [e.t, f], qui, nuit: nuits([e.t, f], nuit) });
286
318
  }
319
+ const attentes = lesAttentes.map(a => a.i);
287
320
 
288
321
  const vide = () => ({ ms: 0, n: 0, jetons: 0, cache: 0, cout: 0 });
289
322
  const categories = Object.fromEntries(CATEGORIES.map(([k]) => [k, vide()]));
290
- categories.attente = { ...vide(), ms: longueur(attentes), n: attentes.length };
323
+ for (const qui of [SUPERVISEUR, RESPONSABLE]) {
324
+ const l = lesAttentes.filter(a => a.qui === qui);
325
+ const ms = l.reduce((m, a) => m + a.i[1] - a.i[0] - longueur(a.nuit), 0);
326
+ categories[qui] = { ...vide(), ms, n: l.length };
327
+ }
328
+ const laNuit = lesAttentes.filter(a => a.nuit.length);
329
+ categories.nuit = { ...vide(), ms: longueur(laNuit.flatMap(a => a.nuit)), n: laNuit.length };
291
330
  const fenetre = s => [Math.max(s.d, debut), Math.min(s.f, fin)];
292
331
  for (const s of segments) {
293
332
  const [d, f] = fenetre(s);
@@ -318,7 +357,7 @@ export function bilan({ ticket, lignes, entrees = [], etat = null, maintenant =
318
357
  // The sessions <l> by role, in the order of the steps.
319
358
  const parRoleDe = l => {
320
359
  const out = {};
321
- for (const role of new Set([...SUITE, ARBITRAGE].flatMap(e => e.qui))) {
360
+ for (const role of new Set(SUITE.flatMap(e => e.qui))) {
322
361
  const n = l.filter(x => x.role === role).length;
323
362
  if (n) out[role] = n;
324
363
  }
@@ -332,22 +371,29 @@ export function bilan({ ticket, lignes, entrees = [], etat = null, maintenant =
332
371
  }
333
372
  const arrets = {};
334
373
  for (const e of duCircuit.filter(x => x.arret)) arrets[e.arret] = (arrets[e.arret] ?? 0) + 1;
335
- // The ends of the questionings: their rounds, the proposals they changed, those sent up.
374
+ // The ends of the questionings of the plan: the questions asked, the plans an answer changed, the
375
+ // questions left open.
336
376
  const fins = duCircuit.map(e => e.questionnement).filter(Boolean);
337
377
  const questionnement = {
338
- tours: fins.reduce((m, q) => m + (q.tours ?? 0), 0),
339
- changees: fins.filter(q => q.change).length,
340
- montees: fins.filter(q => q.issue !== 'AUCUNE').length,
378
+ questions: fins.reduce((m, q) => m + (q.questions ?? 0), 0),
379
+ changes: fins.filter(q => q.change).length,
380
+ ouvertes: fins.reduce((m, q) => m + (q.ouvertes ?? 0), 0),
341
381
  };
342
382
  const somme = k => sessions.reduce((m, l) => m + (l[k] ?? 0), 0);
383
+ const perdu = CATEGORIES.filter(([k]) => !GARDE.has(k)).reduce(
384
+ (m, [k]) => m + categories[k].ms,
385
+ 0
386
+ );
343
387
  return {
344
388
  ticket,
345
389
  calcule: new Date(maintenant).toISOString(),
346
390
  debut: mur ? new Date(debut).toISOString() : null,
347
391
  fin: mur ? new Date(fin).toISOString() : null,
348
392
  mur,
393
+ murHorsNuit: mur - categories.nuit.ms,
349
394
  statut: statutDe(etat),
350
- perdu: CATEGORIES.filter(([k]) => !GARDE.has(k)).reduce((m, [k]) => m + categories[k].ms, 0),
395
+ perdu,
396
+ perduHorsAttentes: perdu - ATTENTES.reduce((m, k) => m + categories[k].ms, 0),
351
397
  categories,
352
398
  seances: {
353
399
  total: sessions.length,
@@ -395,17 +441,17 @@ export function texteDuBilan(b, avant) {
395
441
  const horsCircuit = h.total
396
442
  ? ` ; ${h.total} séance${h.total > 1 ? 's' : ''} hors circuit, lancée${h.total > 1 ? 's' : ''} par le superviseur (${lesRoles(h.parRole)}), ${dollars(h.cout)} $`
397
443
  : '';
398
- const tete = `${b.statut} en ${minutes(b.mur)} min ; ${minutes(b.perdu)} min perdues (${pourcent(b.perdu, b.mur)} %) ; ${b.seances.total} séances${roles ? ` (${roles})` : ''}${horsCircuit} ; ${dollars(b.cout)} $ ; ${millions(b.jetons)} M jetons, ${millions(b.cache)} M lus du cache.`;
444
+ const tete = `${b.statut} en ${minutes(b.mur)} min, ${minutes(b.murHorsNuit)} min hors nuit d'attente ; ${minutes(b.perdu)} min perdues (${pourcent(b.perdu, b.mur)} %), ${minutes(b.perduHorsAttentes)} min hors attentes ; ${b.seances.total} séances${roles ? ` (${roles})` : ''}${horsCircuit} ; ${dollars(b.cout)} $ ; ${millions(b.jetons)} M jetons, ${millions(b.cache)} M lus du cache.`;
399
445
  const paye = new Set(['travail', 'sansEffet', 'questionnement']);
400
- const q = b.questionnement ?? { tours: 0, changees: 0 };
446
+ const q = b.questionnement;
401
447
  const pluriel = (n, mot) => `${n} ${mot}${n > 1 ? 's' : ''}`;
402
448
  const lignes = CATEGORIES.map(([k, nom]) => {
403
449
  const c = b.categories[k];
404
450
  const plus =
405
- k === 'attente' && c.n
451
+ (k === SUPERVISEUR || k === RESPONSABLE) && c.n
406
452
  ? ` (${pluriel(c.n, 'montée')})`
407
453
  : k === 'questionnement'
408
- ? `, ${dollars(c.cout)} $ (${pluriel(q.tours, 'tour')}, ${pluriel(q.changees, 'décision')} ${q.changees > 1 ? 'changées' : 'changée'})`
454
+ ? `, ${dollars(c.cout)} $ (${pluriel(q.questions, 'question')}, ${pluriel(q.changes, 'plan')} ${q.changes > 1 ? 'changés' : 'changé'}, ${pluriel(q.ouvertes, 'question')} ${q.ouvertes > 1 ? 'ouvertes' : 'ouverte'})`
409
455
  : paye.has(k)
410
456
  ? `, ${dollars(c.cout)} $`
411
457
  : '';
@@ -494,7 +540,7 @@ function calculer(racine, ticket) {
494
540
  } catch {
495
541
  etat = null;
496
542
  }
497
- return bilan({ ticket, lignes, entrees, etat });
543
+ return bilan({ ticket, lignes, entrees, etat, nuit: plageDeNuit(racine) });
498
544
  }
499
545
 
500
546
  // The bilan of <ticket> computed, kept in the main tree <racine>, and its text, compared with the