@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.
Files changed (166) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/METACADRE.md +41 -0
  3. package/agents/developpeur.md +10 -0
  4. package/agents/explorateur.md +9 -0
  5. package/agents/questionneur.md +11 -0
  6. package/agents/referent-technique.md +9 -0
  7. package/agents/regulateur.md +11 -0
  8. package/agents/relecteur.md +9 -0
  9. package/agents/testwriter.md +10 -0
  10. package/bin/grillhouse.mjs +125 -0
  11. package/cadre/templates/projet/.claude/settings.json +30 -0
  12. package/cadre/templates/projet/.editorconfig +47 -0
  13. package/cadre/templates/projet/.env.example +57 -0
  14. package/cadre/templates/projet/.eslintrc.cjs +56 -0
  15. package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
  16. package/cadre/templates/projet/.mcp.json +13 -0
  17. package/cadre/templates/projet/.prettierrc +11 -0
  18. package/cadre/templates/projet/CHANGELOG.md +61 -0
  19. package/cadre/templates/projet/CLAUDE.md +46 -0
  20. package/cadre/templates/projet/CONTRIBUTING.md +243 -0
  21. package/cadre/templates/projet/CREDITS.md +41 -0
  22. package/cadre/templates/projet/README.md +152 -0
  23. package/cadre/templates/projet/_gitignore +91 -0
  24. package/cadre/templates/projet/_npmignore +46 -0
  25. package/cadre/templates/projet/deployment/README.md +98 -0
  26. package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
  27. package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
  28. package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
  29. package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
  30. package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
  31. package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
  32. package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
  33. package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
  34. package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
  35. package/cadre/templates/projet/ecosystem.config.cjs +73 -0
  36. package/cadre/templates/projet/package.json +90 -0
  37. package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
  38. package/cadre/templates/projet/src/config.ts +68 -0
  39. package/cadre/templates/projet/src/errors.ts +102 -0
  40. package/cadre/templates/projet/src/index.ts +46 -0
  41. package/cadre/templates/projet/src/services/.gitkeep +8 -0
  42. package/cadre/templates/projet/src/types/index.ts +24 -0
  43. package/cadre/templates/projet/src/utils/logger.ts +106 -0
  44. package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
  45. package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
  46. package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
  47. package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
  48. package/cadre/templates/projet/tsconfig.eslint.json +9 -0
  49. package/cadre/templates/projet/tsconfig.json +41 -0
  50. package/cadre/templates/projet/tsdoc.json +6 -0
  51. package/cadre/templates/projet/vitest.config.ts +44 -0
  52. package/cadre/templates/turbo/paquets.json +29 -0
  53. package/cadre/templates/turbo/sans-paquets.json +28 -0
  54. package/charte.md +107 -0
  55. package/docs/ARCHITECTURE.md +90 -0
  56. package/hooks/crochet.mjs +33 -0
  57. package/hooks/hooks.json +58 -0
  58. package/hooks/register.tsx +558 -0
  59. package/package.json +75 -4
  60. package/scripts/avancement.mjs +244 -0
  61. package/scripts/bilan.mjs +523 -0
  62. package/scripts/branche-de-nuit.mjs +59 -0
  63. package/scripts/cadre.mjs +109 -0
  64. package/scripts/circuit.mjs +4375 -0
  65. package/scripts/code-mort.mjs +371 -0
  66. package/scripts/composants-affectes.mjs +598 -0
  67. package/scripts/consommateurs-arbitres.mjs +192 -0
  68. package/scripts/consommateurs.mjs +310 -0
  69. package/scripts/crochet.mjs +24 -0
  70. package/scripts/crochets.mjs +135 -0
  71. package/scripts/dernier-vert.mjs +208 -0
  72. package/scripts/dette.mjs +181 -0
  73. package/scripts/droits.mjs +354 -0
  74. package/scripts/enveloppe/codes.mjs +17 -0
  75. package/scripts/enveloppe/composant.mjs +11 -0
  76. package/scripts/enveloppe/enveloppe.mjs +628 -0
  77. package/scripts/enveloppe/lancer.sh +311 -0
  78. package/scripts/etapes.mjs +323 -0
  79. package/scripts/etat-du-circuit.mjs +104 -0
  80. package/scripts/frontieres.mjs +456 -0
  81. package/scripts/gardes-du-cadre.mjs +72 -0
  82. package/scripts/greffons.mjs +338 -0
  83. package/scripts/grillhouse-maj.mjs +514 -0
  84. package/scripts/index-interfaces.mjs +293 -0
  85. package/scripts/init.mjs +117 -0
  86. package/scripts/integration/file-attente.mjs +61 -0
  87. package/scripts/integration/file.mjs +494 -0
  88. package/scripts/integration/integrer.mjs +381 -0
  89. package/scripts/integration/rejeu.mjs +139 -0
  90. package/scripts/integration/verdict.mjs +13 -0
  91. package/scripts/interfaces-contre-code.mjs +547 -0
  92. package/scripts/ligne-etat/defaut.sh +110 -0
  93. package/scripts/ligne-etat/ligne-etat.mjs +93 -0
  94. package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
  95. package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
  96. package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
  97. package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
  98. package/scripts/migrations.mjs +43 -0
  99. package/scripts/nuit.sh +269 -0
  100. package/scripts/paquets.mjs +79 -0
  101. package/scripts/poussee-cles.mjs +46 -0
  102. package/scripts/preparation.mjs +84 -0
  103. package/scripts/references-par-les-types.mjs +420 -0
  104. package/scripts/registre.mjs +336 -0
  105. package/scripts/resultats.mjs +580 -0
  106. package/scripts/resumes.mjs +57 -0
  107. package/scripts/retirer-copie.sh +127 -0
  108. package/scripts/session-start.sh +203 -0
  109. package/scripts/sortie.mjs +155 -0
  110. package/scripts/sous-modules.sh +316 -0
  111. package/scripts/structure-guard.sh +27 -0
  112. package/scripts/superviseur.mjs +102 -0
  113. package/scripts/tableau.mjs +1886 -0
  114. package/scripts/tests.mjs +236 -0
  115. package/scripts/ticket.sh +23 -0
  116. package/scripts/turbo-des-paquets.mjs +899 -0
  117. package/scripts/un-commit-par-ticket.mjs +212 -0
  118. package/scripts/verifier.mjs +469 -0
  119. package/scripts/verrou-de-fichier.mjs +130 -0
  120. package/scripts/verrou-de-machine.mjs +211 -0
  121. package/scripts/verrous/commande.mjs +180 -0
  122. package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
  123. package/scripts/verrous/verrou.mjs +165 -0
  124. package/scripts/vitest-config/avancement.mjs +39 -0
  125. package/scripts/vitest-config/avancement.reporter.mjs +56 -0
  126. package/scripts/vitest-config/base-de-passe.mjs +128 -0
  127. package/scripts/vitest-config/base.d.ts +6 -0
  128. package/scripts/vitest-config/base.mjs +87 -0
  129. package/scripts/vitest-config/changements.mjs +173 -0
  130. package/scripts/vitest-config/cibles.mjs +207 -0
  131. package/scripts/vitest-config/duree.reporter.mjs +98 -0
  132. package/scripts/vitest-config/machine.setup.mjs +14 -0
  133. package/scripts/vitest-config/mesure.d.ts +6 -0
  134. package/scripts/vitest-config/mesure.mjs +13 -0
  135. package/scripts/vitest-config/mesure.setup.mjs +12 -0
  136. package/scripts/vitest-config/pression.mjs +90 -0
  137. package/scripts/vitest-config/resultats.mjs +23 -0
  138. package/scripts/vitest-config/resultats.setup.mjs +19 -0
  139. package/scripts/vitest.nuit.config.mjs +26 -0
  140. package/skills/developpeur/SKILL.md +175 -0
  141. package/skills/explorateur/SKILL.md +84 -0
  142. package/skills/grill/SKILL.md +238 -0
  143. package/skills/grill/references/releve-structurel.md +38 -0
  144. package/skills/mesure/SKILL.md +168 -0
  145. package/skills/pitmaster/SKILL.md +382 -0
  146. package/skills/pitmaster/references/cadre.md +119 -0
  147. package/skills/pitmaster/references/consigne-agent.md +308 -0
  148. package/skills/pitmaster/references/erreurs.md +25 -0
  149. package/skills/pitmaster/references/tour.md +124 -0
  150. package/skills/questionneur/SKILL.md +64 -0
  151. package/skills/redacteur/SKILL.md +84 -0
  152. package/skills/redacteur/references/documents-d-un-composant.md +105 -0
  153. package/skills/referent-technique/SKILL.md +145 -0
  154. package/skills/regulateur/SKILL.md +220 -0
  155. package/skills/release/SKILL.md +134 -0
  156. package/skills/release/scripts/update-version.cjs +92 -0
  157. package/skills/relecteur/SKILL.md +152 -0
  158. package/skills/relecteur/references/LICENSE-MIT +30 -0
  159. package/skills/relecteur/references/SOURCE.md +8 -0
  160. package/skills/relecteur/references/claims-check.md +14 -0
  161. package/skills/relecteur/references/deletion-check.md +14 -0
  162. package/skills/relecteur/references/edge-case-hunter.md +80 -0
  163. package/skills/relecteur/references/verification-gap.md +113 -0
  164. package/skills/testwriter/SKILL.md +187 -0
  165. package/types/index.d.ts +56 -0
  166. package/README.md +0 -3
@@ -0,0 +1,207 @@
1
+ // The tests a targeted run names (`npm test -- <paths>`, scripts/tests.mjs), carried to Vitest by
2
+ // the variable `GRILLHOUSE_TESTS` rather than by its command line: the arguments after `--` of a
3
+ // Turborepo run enter the hash of every task of that run, the builds included, while a variable
4
+ // that the `env` of the task `test:vitest` names enters the hash of the tests alone
5
+ // (https://turborepo.dev/docs/reference/configuration#env). Turborepo then plays the tests with
6
+ // what they depend on, each build and each trace hashed by Turborepo itself.
7
+ //
8
+ // - The value is JSON: `arguments`, Vitest's arguments (the paths, absolute, and the options
9
+ // written `--<name>=<value>`), which Vitest's own parser of its command line (`parseCLI`,
10
+ // vitest/node) reads; `rejouer`, the moment of a run that plays the tests for real (`--force`),
11
+ // which only moves the hash; `temoins`, the time witnesses of a pass of `npm run verifier`
12
+ // (below). The plugin takes the variable out of the environment once read: no
13
+ // process the tests start receives it.
14
+ // - A plugin of the base configuration (./base.mjs) reads it once the configuration is merged (its
15
+ // `config` hook; Vitest reads the test options in `configResolved`, after every `config` hook):
16
+ // the files played are the test files of the configuration's `include` (Vitest's default when
17
+ // it sets none) that lie under a path named (none named, the configuration's own), as Vitest's filters of its command line take an
18
+ // absolute path (a file, or a directory ending with « / »); its `exclude` still applies, Vitest's.
19
+ // A path of another package names none of its files. Each option enters the test configuration
20
+ // as Vitest merges its command line over it (`exclude` given on the command line is
21
+ // `cliExclude`, as Vitest's `normalizeCliOptions` names it).
22
+ // - A night test or a time budget bench (HORS_DU_JOUR, the day's exclusions of ./base.mjs) plays
23
+ // when its own file is named, as Bazel's target tagged `manual` builds when named and never by a
24
+ // pattern: the day's exclusions of both lift for that run, the include of the configuration
25
+ // notwithstanding (the night's own include is HORS_DU_JOUR), and the measure of time of the base
26
+ // configuration applies, its duration reporter giving 75, « à repasser », to a bench that fails
27
+ // under load. A directory named leaves them out, as the day does.
28
+ // - Without the variable, nothing changes. A run whose paths name no test file plays none, and
29
+ // fails: the base configuration keeps Vitest's `passWithNoTests: false`.
30
+ // - The time witnesses play alone, as Bazel plays a test tagged `exclusive` after the others, with
31
+ // nothing beside it: a time witness is a test file that imports the mark of a measure of time
32
+ // (`import { mesurerAuRepos } from '@roomi-fields/grillhouse/vitest-config/mesure'`, a line of its own,
33
+ // `estTemoin`). `npm run verifier` (scripts/verifier.mjs) plays its pass with `temoins: 'hors'`
34
+ // in the value, which leaves every witness of the configuration's `include` out, then a pass of
35
+ // its own with `temoins: 'seuls'`, which plays the witnesses alone, one file at a time
36
+ // (`fileParallelism: false`), and passes without one (`passWithNoTests`); a targeted run sets
37
+ // neither, and a witness named plays there as any test. The witnesses are found under the
38
+ // configuration's directory (`fichiersTemoins`), node_modules, dist, .git, .turbo and .claude
39
+ // left out.
40
+ // - The changes since the last green (`GRILLHOUSE_CHANGEMENTS`, ./changements.mjs) restrict the
41
+ // files played to those Vitest relates to them, the files named themselves kept, or leave every
42
+ // test when a change lies out of Vitest's graph.
43
+ //
44
+ // This module's top level uses Node alone: scripts/tests.mjs reads its VARIABLE.
45
+ import { globSync, readdirSync, readFileSync, statSync } from 'node:fs';
46
+ import path from 'node:path';
47
+ import { CHANGEMENTS, lireChangements, restreindre } from './changements.mjs';
48
+
49
+ export const VARIABLE = 'GRILLHOUSE_TESTS';
50
+ // The day's exclusions, which the base configuration (./base.mjs) sets and the night plays
51
+ // (scripts/vitest.nuit.config.mjs): the night tests, too long for the day, and the time budget
52
+ // benches, whose measures move with the load of the day.
53
+ export const NUIT = '**/*.nuit.{test,spec}.?(c|m)[jt]s?(x)';
54
+ export const BUDGET = '**/*.budget.{test,spec}.?(c|m)[jt]s?(x)';
55
+ export const HORS_DU_JOUR = [NUIT, BUDGET];
56
+ // Whether <r>, a path relative to the configuration's directory, is a night test or a bench.
57
+ const horsDuJour = r => HORS_DU_JOUR.some(p => path.matchesGlob(r, p));
58
+ // Vitest's default `include` (its configDefaults).
59
+ const INCLUDE = ['**/*.{test,spec}.?(c|m)[jt]s?(x)'];
60
+ // The mark of a time witness: the import of `mesurerAuRepos`, on a line of its own, from the
61
+ // package of the base configuration (by its name, or by the path of its file).
62
+ const MARQUE_TEMOIN =
63
+ /^import\s*\{[^}]*\bmesurerAuRepos\b[^}]*\}\s*from\s*['"][^'"]*vitest-config\/mesure(?:\.mjs)?['"]/m;
64
+ // Whether the text <texte> of a test file is that of a time witness.
65
+ const estTemoin = texte => MARQUE_TEMOIN.test(texte);
66
+ // The directories a search of the witnesses leaves out.
67
+ const HORS_TEMOINS = new Set(['node_modules', 'dist', '.git', '.turbo', '.claude']);
68
+
69
+ // The time witnesses under <dir>: the test files of <include> (Vitest's default without it), the
70
+ // day's exclusions left out, that `estTemoin`; each path relative to <dir>, sorted.
71
+ export function fichiersTemoins(dir, include = INCLUDE) {
72
+ const lire = f => {
73
+ try {
74
+ return readFileSync(path.join(dir, f), 'utf8');
75
+ } catch {
76
+ return '';
77
+ }
78
+ };
79
+ return [
80
+ ...new Set(
81
+ globSync(include, { cwd: dir, exclude: d => HORS_TEMOINS.has(path.basename(d)) }).map(f =>
82
+ f.split(path.sep).join('/')
83
+ )
84
+ ),
85
+ ]
86
+ .filter(f => !horsDuJour(f) && estTemoin(lire(f)))
87
+ .sort();
88
+ }
89
+ // The directories a walk under a named directory leaves out, whose files no test configuration
90
+ // plays (the base excludes them).
91
+ const SAUTES = new Set(['node_modules', '.git']);
92
+ // The characters a path escapes to be read by the glob of Vitest as itself.
93
+ const SPECIAUX = /[\\*?[\]{}()!+@|^$]/g;
94
+
95
+ // The files at <chemin> (absolute): itself when it is a file, the files under it when it is a
96
+ // directory; none when it does not exist.
97
+ function fichiers(chemin) {
98
+ let stat;
99
+ try {
100
+ stat = statSync(chemin);
101
+ } catch {
102
+ return [];
103
+ }
104
+ if (!stat.isDirectory()) {
105
+ return [chemin];
106
+ }
107
+ return readdirSync(chemin, { withFileTypes: true }).flatMap(e =>
108
+ e.isDirectory() && SAUTES.has(e.name) ? [] : fichiers(path.join(chemin, e.name))
109
+ );
110
+ }
111
+
112
+ // <valeur> merged over <cible> as Vitest merges its command line over the configuration: an object
113
+ // merged key by key, any other value replaced.
114
+ const fusion = (cible, valeur) =>
115
+ valeur && typeof valeur === 'object' && !Array.isArray(valeur)
116
+ ? Object.entries(valeur).reduce(
117
+ (o, [k, v]) => ({ ...o, [k]: fusion(o[k], v) }),
118
+ cible && typeof cible === 'object' && !Array.isArray(cible) ? cible : {}
119
+ )
120
+ : valeur;
121
+
122
+ // The plugin that restricts a run to the tests <env> names in VARIABLE.
123
+ export const ciblage = (env = process.env, dire = l => process.stdout.write(`${l}\n`)) => ({
124
+ name: 'grillhouse:cibles',
125
+ async config(config) {
126
+ const texte = env[VARIABLE];
127
+ const changements = env[CHANGEMENTS];
128
+ delete env[VARIABLE];
129
+ delete env[CHANGEMENTS];
130
+ if (!texte && !changements) {
131
+ return;
132
+ }
133
+ const test = (config.test ??= {});
134
+ // The files a targeted run names themselves, absolute.
135
+ let nommes = [];
136
+ if (texte) {
137
+ nommes = await cibler(config, test, JSON.parse(texte));
138
+ }
139
+ const ch = changements ? lireChangements(changements) : null;
140
+ // A targeted run whose paths name no test file plays none, and fails, as without the changes.
141
+ if (ch && test.include?.[0] !== `${VARIABLE}-sans-fichier`) {
142
+ restreindre(test, path.resolve(config.root ?? process.cwd()), ch, nommes, dire);
143
+ }
144
+ },
145
+ });
146
+
147
+ // Applies the value <valeur> of VARIABLE to the configuration <config>, its test options <test>:
148
+ // the files named themselves, absolute.
149
+ async function cibler(config, test, valeur) {
150
+ const { parseCLI } = await import('vitest/node');
151
+ const { filter, options } = parseCLI(['vitest', ...(valeur.arguments ?? [])]);
152
+ let nommes = [];
153
+ // The time witnesses: left out of the pass, or played alone in their own.
154
+ if (valeur.temoins === 'hors' || valeur.temoins === 'seuls') {
155
+ const dir = path.resolve(config.root ?? process.cwd(), test.dir ?? '');
156
+ const temoins = fichiersTemoins(dir, test.include ?? INCLUDE).map(r =>
157
+ r.replace(SPECIAUX, '\\$&')
158
+ );
159
+ if (valeur.temoins === 'hors') {
160
+ test.exclude = [...(test.exclude ?? ['**/node_modules/**']), ...temoins];
161
+ } else {
162
+ test.include = temoins.length ? temoins : [`${VARIABLE}-sans-temoin`];
163
+ test.passWithNoTests = true;
164
+ test.fileParallelism = false;
165
+ }
166
+ }
167
+ if (filter.length) {
168
+ const dir = path.resolve(config.root ?? process.cwd(), test.dir ?? '');
169
+ const include = test.include ?? INCLUDE;
170
+ const chemins = filter.map(f => path.resolve(f.replace(/[\\/]+$/, '')));
171
+ // The files named themselves, not through a directory.
172
+ const seuls = new Set(chemins.filter(f => fichiers(f)[0] === f));
173
+ nommes = [...seuls];
174
+ const relatif = f => path.relative(dir, f).split(path.sep).join('/');
175
+ const joues = [...new Set(chemins.flatMap(fichiers))]
176
+ .filter(f => {
177
+ const r = relatif(f);
178
+ if (r.startsWith('../')) {
179
+ return false;
180
+ }
181
+ return horsDuJour(r) ? seuls.has(f) : include.some(p => path.matchesGlob(r, p));
182
+ })
183
+ .map(relatif)
184
+ .sort();
185
+ // A bench or a night test named itself plays: the day's exclusions of both lift, and the
186
+ // files under a directory named stay out by the filter above.
187
+ if (joues.some(horsDuJour) && Array.isArray(test.exclude)) {
188
+ test.exclude = test.exclude.filter(p => !HORS_DU_JOUR.includes(p));
189
+ }
190
+ // No file named: a pattern that matches none, Vitest's « No test files found ».
191
+ test.include = joues.length
192
+ ? joues.map(r => r.replace(SPECIAUX, '\\$&'))
193
+ : [`${VARIABLE}-sans-fichier`];
194
+ }
195
+ // parseCLI gives each option under its aliases too (`t`, `test-name-pattern`): its name alone
196
+ // enters.
197
+ const { '--': _, color: __, exclude, ...reste } = options;
198
+ for (const [cle, valeur] of Object.entries(reste)) {
199
+ if (cle.length > 1 && !cle.includes('-')) {
200
+ test[cle] = fusion(test[cle], valeur);
201
+ }
202
+ }
203
+ if (exclude !== undefined) {
204
+ test.cliExclude = [exclude].flat();
205
+ }
206
+ return nommes;
207
+ }
@@ -0,0 +1,98 @@
1
+ // The duration reporter of the base configuration, beside Vitest's default reporter. At the end of
2
+ // a run with failures, it reads each one:
3
+ //
4
+ // - a failure depends on a duration when it is a delay past (an error « Test timed out in <n>ms. »
5
+ // or « Hook timed out in <n>ms. », Vitest's own message), a failure of a time budget bench (a
6
+ // file `*.budget.test.*`, `*.budget.spec.*`), or of a test marked as a measure of time
7
+ // (`mesurerAuRepos`, ./mesure.mjs); every other failure is a real red;
8
+ // - it took place under load when the CPU pressure (./pression.mjs) was at or above the threshold
9
+ // at the start or at the end of the test (noted by ./mesure.setup.mjs), or, for a hook or a file
10
+ // that fails outside of a test, at the start or at the end of the run.
11
+ //
12
+ // When every failure depends on a duration and at least one took place under load, the run is
13
+ // « à repasser »: the exit code becomes 75 (it runs after Vitest set 1, and replaces it). Else
14
+ // Vitest's code stands: a real red, a duration past at rest, an error outside the tests, a pressure
15
+ // that does not read. Each failure of duration is written with the pressure measured and the
16
+ // threshold; a pressure that does not read is said, « pression CPU illisible ».
17
+ import path from 'node:path';
18
+ import { A_REPASSER, ecrirePression, lirePression, SEUIL_PRESSION } from './pression.mjs';
19
+
20
+ const DELAI = /^(Test|Hook) timed out in \d+ms\./;
21
+ const BANC = /\.budget\.(test|spec)\.(c|m)?[jt]sx?$/;
22
+
23
+ function* taches(t) {
24
+ yield t;
25
+ for (const c of t.tasks ?? []) yield* taches(c);
26
+ }
27
+ const fichierDe = t => {
28
+ let x = t;
29
+ while (x && !x.filepath) x = x.suite ?? x.file;
30
+ return x?.filepath ?? '';
31
+ };
32
+ const nomDe = t => {
33
+ const noms = [];
34
+ for (let x = t; x && !x.filepath; x = x.suite) noms.unshift(x.name);
35
+ return noms.join(' › ');
36
+ };
37
+
38
+ export default class DureeReporter {
39
+ onInit() {
40
+ this.debut = lirePression();
41
+ }
42
+
43
+ onFinished(files = [], errors = []) {
44
+ const fin = lirePression();
45
+ const echecs = [];
46
+ for (const f of files) {
47
+ for (const t of taches(f)) {
48
+ const erreurs = t.result?.errors ?? [];
49
+ if (t.type === 'test' ? t.result?.state === 'fail' : erreurs.length > 0) echecs.push(t);
50
+ }
51
+ }
52
+ if (echecs.length === 0) return;
53
+ const dire = l => process.stdout.write(`⏱ ${l}\n`);
54
+ let reels = 0;
55
+ const pressions = [];
56
+ for (const t of echecs) {
57
+ const fichier = fichierDe(t);
58
+ const erreurs = t.result?.errors ?? [];
59
+ const cause =
60
+ t.type === 'test' && t.meta?.mesureDeTemps
61
+ ? 'mesure de temps'
62
+ : BANC.test(fichier)
63
+ ? 'banc de temps'
64
+ : erreurs.length > 0 && erreurs.every(e => DELAI.test(e.message ?? ''))
65
+ ? 'délai dépassé'
66
+ : null;
67
+ if (!cause) {
68
+ reels++;
69
+ continue;
70
+ }
71
+ const debut = t.type === 'test' ? (t.meta?.pressionDebut ?? this.debut) : this.debut;
72
+ const finT = t.type === 'test' ? (t.meta?.pressionFin ?? fin) : fin;
73
+ pressions.push(debut, finT);
74
+ const nom = nomDe(t);
75
+ dire(
76
+ `${path.relative(process.cwd(), fichier)}${nom ? ` › ${nom}` : ''} : ${cause}, pression CPU ${ecrirePression(debut)} au début, ${ecrirePression(finT)} à la fin (seuil ${SEUIL_PRESSION} %).`
77
+ );
78
+ }
79
+ if (pressions.length === 0) return;
80
+ if (reels > 0 || errors.length > 0 || process.exitCode !== 1) {
81
+ dire('un échec ne dépend pas d’une durée : verdict de Vitest.');
82
+ return;
83
+ }
84
+ const lues = pressions.filter(p => p !== null);
85
+ if (lues.length === 0) {
86
+ dire('pression CPU illisible (/proc/pressure/cpu) : verdict de Vitest.');
87
+ return;
88
+ }
89
+ if (lues.some(p => p >= SEUIL_PRESSION)) {
90
+ dire(
91
+ `à repasser (code ${A_REPASSER}) : chaque échec dépend d’une durée, et au moins un a eu lieu sous charge ; ni vert ni rouge, la mesure se rejoue au repos.`
92
+ );
93
+ process.exitCode = A_REPASSER;
94
+ return;
95
+ }
96
+ dire(`échecs de durée au repos (pression CPU sous ${SEUIL_PRESSION} %) : rouge.`);
97
+ }
98
+ }
@@ -0,0 +1,14 @@
1
+ // A setup file of the base configuration: the machine of a test file is its own. The file gets a
2
+ // lock of the heavy measure of its own (GRILLHOUSE_VERROU_MACHINE, a file of the temporary
3
+ // directory named after its process and a token, scripts/verrou-de-machine.mjs), which every
4
+ // process it starts inherits: a `npm run verifier` or a circuit that a test plays in a repository
5
+ // of trial neither waits for the lock of the machine nor holds it, and no test writes in the
6
+ // user's cache. A lock already named in the environment stays.
7
+ import { randomUUID } from 'node:crypto';
8
+ import { tmpdir } from 'node:os';
9
+ import path from 'node:path';
10
+
11
+ process.env.GRILLHOUSE_VERROU_MACHINE ||= path.join(
12
+ tmpdir(),
13
+ `grillhouse-mesure-${process.pid}-${randomUUID()}.lock`
14
+ );
@@ -0,0 +1,6 @@
1
+ // The types of ./mesure.mjs, for a strict TypeScript test that imports
2
+ // `@roomi-fields/grillhouse/vitest-config/mesure`.
3
+ import type { TestContext } from 'vitest';
4
+
5
+ // Marks the test of <ctx> as a measure of time and gives back what <fn> returns.
6
+ export declare function mesurerAuRepos<T>(ctx: TestContext, fn: () => T): T;
@@ -0,0 +1,13 @@
1
+ // A test that measures time marks itself, so that its failure under load is « à repasser », not a
2
+ // red (./duree.reporter.mjs):
3
+ //
4
+ // import { mesurerAuRepos } from '@roomi-fields/grillhouse/vitest-config/mesure';
5
+ // it('rend en moins de 50 ms', ctx => mesurerAuRepos(ctx, () => { … expect(ms).toBeLessThan(50); }));
6
+ //
7
+ // `mesurerAuRepos(ctx, fn)` marks the test of the context <ctx> as a measure of time and gives back
8
+ // what <fn> returns (a promise as it is, for the test to await). A time budget bench
9
+ // (`*.budget.test.*`) and a delay past need no mark.
10
+ export function mesurerAuRepos(ctx, fn) {
11
+ ctx.task.meta.mesureDeTemps = true;
12
+ return fn();
13
+ }
@@ -0,0 +1,12 @@
1
+ // The setup file of the base configuration: notes the CPU pressure (./pression.mjs) at the start
2
+ // and at the end of each test in its `task.meta`, which reaches the duration reporter
3
+ // (./duree.reporter.mjs). The end is noted after a delay past too: Vitest plays `afterEach` then.
4
+ import { afterEach, beforeEach } from 'vitest';
5
+ import { lirePression } from './pression.mjs';
6
+
7
+ beforeEach(({ task }) => {
8
+ task.meta.pressionDebut = lirePression();
9
+ });
10
+ afterEach(({ task }) => {
11
+ task.meta.pressionFin = lirePression();
12
+ });
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // The CPU pressure of the machine, the witness of rest for every measure of time: a time witness
3
+ // is measured with the CPU at rest, and a failure that depends on a duration under load is played
4
+ // again at rest (« à repasser »), never taken as a red nor as a green. The model is Bazel's
5
+ // TIMEOUT/FLAKY and CTest's `--repeat after-timeout`: classify, then play again.
6
+ //
7
+ // - The signal is the kernel's pressure stall information, `/proc/pressure/cpu`, line `some`,
8
+ // field `avg10`: the share of the last ten seconds in which a task waited for a CPU, in %.
9
+ // GRILLHOUSE_PRESSION_CPU names another file of the same form (the trials write it).
10
+ // - The machine is at rest under SEUIL_PRESSION (25 %); at or above it, it is under load.
11
+ // - A file that does not read (no pressure stall information on the machine) gives null: whoever
12
+ // reads it says « pression illisible » and leaves the verdict to Vitest.
13
+ // - A_REPASSER, 75, is the exit code of « à repasser » (EX_TEMPFAIL of sysexits.h: try again
14
+ // later), neither green nor red: the duration reporter of Vitest gives it
15
+ // (./duree.reporter.mjs), `npm run verifier` reads it (scripts/verifier.mjs), and so do the
16
+ // integration, the queue and the night.
17
+ //
18
+ // npx grillhouse pression attendre
19
+ //
20
+ // waits for rest (`attendreLeRepos`, bound 15 min) and exits 0 at rest, 75 at the bound: the night
21
+ // waits so before it plays its time budget benches again (scripts/nuit.sh).
22
+ import { readFileSync } from 'node:fs';
23
+ import { pathToFileURL } from 'node:url';
24
+
25
+ export const SEUIL_PRESSION = 25;
26
+ export const A_REPASSER = 75;
27
+ export const FICHIER_PRESSION = '/proc/pressure/cpu';
28
+ // The longest wait for rest, and the shortest gap between two lines that say it waits.
29
+ export const BORNE_ATTENTE_MS = 15 * 60_000;
30
+ const ECART_LIGNES_MS = 30_000;
31
+
32
+ // The pressure `some avg10` of <fichier>, in %, or null when it does not read.
33
+ export function lirePression(fichier = process.env.GRILLHOUSE_PRESSION_CPU || FICHIER_PRESSION) {
34
+ try {
35
+ const m = /^some\b.*\bavg10=(\d+(?:\.\d+)?)/m.exec(readFileSync(fichier, 'utf8'));
36
+ return m ? Number(m[1]) : null;
37
+ } catch {
38
+ return null;
39
+ }
40
+ }
41
+
42
+ // A pressure written for a line: « 31.2 % », or « illisible ».
43
+ export const ecrirePression = p => (p === null ? 'illisible' : `${p.toFixed(1)} %`);
44
+
45
+ // Sleeps <ms> milliseconds, the process held.
46
+ export const dormir = ms => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
47
+
48
+ // Waits until the pressure (`lire`) is under the threshold, or until the instant <limite>
49
+ // (`maintenant`, in ms). Says the pressure (`dire`) while it waits, one line per 30 s at most.
50
+ // True at rest, false at the bound; a pressure that does not read ends the wait (true), said.
51
+ export function attendreLeRepos({
52
+ lire = () => lirePression(),
53
+ maintenant = () => Date.now(),
54
+ dormir: dort = dormir,
55
+ limite,
56
+ pasMs = 2_000,
57
+ dire,
58
+ }) {
59
+ let derniere = -Infinity;
60
+ for (;;) {
61
+ const p = lire();
62
+ if (p === null) {
63
+ dire('⏳ pression CPU illisible : la relance part sans attendre, verdict de Vitest.');
64
+ return true;
65
+ }
66
+ if (p < SEUIL_PRESSION) return true;
67
+ const t = maintenant();
68
+ if (t >= limite) return false;
69
+ if (t - derniere >= ECART_LIGNES_MS) {
70
+ const reste = Math.ceil((limite - t) / 60_000);
71
+ dire(
72
+ `⏳ pression CPU ${ecrirePression(p)} (seuil ${SEUIL_PRESSION} %) : attente du repos, ${reste} min au plus.`
73
+ );
74
+ derniere = t;
75
+ }
76
+ dort(Math.min(pasMs, limite - t));
77
+ }
78
+ }
79
+
80
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
81
+ if (process.argv[2] !== 'attendre') {
82
+ process.stderr.write('usage: pression.mjs attendre\n');
83
+ process.exitCode = 2;
84
+ } else {
85
+ const dire = l => process.stdout.write(`${l}\n`);
86
+ const repos = attendreLeRepos({ limite: Date.now() + BORNE_ATTENTE_MS, dire });
87
+ if (!repos) dire(`⏳ pas de repos en ${BORNE_ATTENTE_MS / 60_000} min.`);
88
+ process.exitCode = repos ? 0 : A_REPASSER;
89
+ }
90
+ }
@@ -0,0 +1,23 @@
1
+ // The JSON report of Vitest in the results directory of a run (scripts/resultats.mjs, which reads
2
+ // it): the variable that names that directory, `GRILLHOUSE_RESULTATS`, and the reporter the base
3
+ // configuration adds while it is set (./base.mjs). Without it, Vitest writes no report.
4
+ //
5
+ // - Each run of Vitest writes its report to `<dossier>/vitest/<name>.json`, the name made of the
6
+ // package directory it runs in and its process: two packages, or the same one twice, never write
7
+ // the same file.
8
+ // - The variable passes through Turborepo without entering any hash (`globalPassThroughEnv` of the
9
+ // root's turbo.json, which scripts/turbo-des-paquets.mjs writes and checks).
10
+ import path from 'node:path';
11
+
12
+ export const RESULTATS = 'GRILLHOUSE_RESULTATS';
13
+ // The subdirectory of the reports of Vitest in a results directory.
14
+ export const VITEST = 'vitest';
15
+
16
+ // The reporter of the JSON report under <env>'s results directory, as Vitest's `reporters` take
17
+ // it; an empty list without the variable.
18
+ export function rapportJson(env = process.env, dossier = process.cwd()) {
19
+ const resultats = env[RESULTATS];
20
+ if (!resultats) return [];
21
+ const nom = `${dossier.split(path.sep).filter(Boolean).join('-') || 'racine'}-${process.pid}.json`;
22
+ return [['json', { outputFile: path.join(resultats, VITEST, nom) }]];
23
+ }
@@ -0,0 +1,19 @@
1
+ // A setup file of the base configuration: a test file's results are its own, as Bazel gives each
2
+ // test its own outputs directory (TEST_UNDECLARED_OUTPUTS_DIR). While the run names its results
3
+ // directory (`GRILLHOUSE_RESULTATS`, ./resultats.mjs), the file names for every process it starts
4
+ // a directory of its own, `<dossier>/essais/<process>-<token>/`, which no reader of the run reads
5
+ // (scripts/resultats.mjs reads `vitest/`, `sarif/` and `turbo/`): a Vitest, a `npm run verifier`
6
+ // or a circuit that a test plays in a repository of trial writes its results there, and its reds
7
+ // never count in the pass that plays it. The reporters of the run, set up in its main process,
8
+ // keep the run's own directory. Without the variable, nothing changes.
9
+ import { randomUUID } from 'node:crypto';
10
+ import path from 'node:path';
11
+ import { RESULTATS } from './resultats.mjs';
12
+
13
+ if (process.env[RESULTATS]) {
14
+ process.env[RESULTATS] = path.join(
15
+ process.env[RESULTATS],
16
+ 'essais',
17
+ `${process.pid}-${randomUUID()}`
18
+ );
19
+ }
@@ -0,0 +1,26 @@
1
+ // The night tests of a project: the files `*.nuit.test.*` and `*.nuit.spec.*`, too long for the
2
+ // day, and its time budget benches (`*.budget.test.*`, `*.budget.spec.*`), whose measures move with
3
+ // the load of the day; `npm test`, `npm run verifier` and the integration leave them out (the
4
+ // frame's base configuration of the day, the package @roomi-fields/grillhouse/vitest-config, which every day
5
+ // configuration extends, excludes both). The night (scripts/nuit.sh) plays them with this
6
+ // configuration, from the root of its copy (`vitest run --root <copy> --config <this file>`),
7
+ // after a green full pass (`npm run verifier -- --force`) and before the project's night script.
8
+ // It reads no day configuration of the project, whether the project has one at its root or one
9
+ // per package; it takes from the base configuration the common environment (git's settings) and
10
+ // the measure of time: the setup file that notes the CPU pressure of each test and the duration
11
+ // reporter, which gives 75, « à repasser », to a bench that fails under load
12
+ // (scripts/vitest-config/duree.reporter.mjs).
13
+ import base from './vitest-config/base.mjs';
14
+ import { HORS_DU_JOUR } from './vitest-config/cibles.mjs';
15
+
16
+ export default {
17
+ test: {
18
+ env: base.test.env,
19
+ environment: base.test.environment,
20
+ reporters: base.test.reporters,
21
+ setupFiles: base.test.setupFiles,
22
+ include: HORS_DU_JOUR,
23
+ exclude: ['**/node_modules/**', '**/dist/**', '**/.git/**'],
24
+ watch: false,
25
+ },
26
+ };