@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,208 @@
1
+ // The last green of a branch: the tree of the last pass of `npm run verifier` green on every
2
+ // package (scripts/verifier.mjs), against which the next pass measures itself, as
3
+ // `vitest --changed <ref>` and `jest --onlyChanged` play the tests a change reaches, and Bazel
4
+ // keeps by target the result of a test whose inputs did not move.
5
+ //
6
+ // - It lives in `<git common dir>/grillhouse/dernier-vert/<branche>.json`, one per branch, so that
7
+ // each copy (worktree) of a ticket keeps its own and no copy replaces another's or main's:
8
+ // `{ commit, arbre }`, HEAD at the start of that pass and the tree of the working tree then, the
9
+ // files git ignores left out, HEAD and the index untouched (a copy of the index, every file added
10
+ // to it, `git add -A`, written as a tree, as the circuit keeps a lot). A pass on a copy whose lot
11
+ // is not committed thus keeps what it played.
12
+ // - A pass measures itself against the last green of its branch when it counts, else against
13
+ // main's, the branch of the main tree (HEAD of git's common directory), as `vitest --changed
14
+ // <ref>` and Bazel measure against the last green build of the same branch: the final pass of a
15
+ // ticket relaunched plays what changed since the copy's last green, not since main's. A HEAD
16
+ // detached (the queue's replay onto main, scripts/integration/file.mjs) reads and keeps main's.
17
+ // - A last green counts when its commit is an ancestor of HEAD (or HEAD) and its tree is still in
18
+ // the repository. Then the changes are the files that differ between its tree and the tree of the
19
+ // working tree now (`git diff --raw --no-renames`): the files changed or added, and those
20
+ // removed. Any two trees compare: the ancestry keeps the measure to the history it belongs to.
21
+ // - No last green counts without git, before the first commit, and while a submodule differs
22
+ // from the commit its parent records (its files changed, or its HEAD moved) or the changes
23
+ // reach a submodule: git's tree holds a submodule's commit, not its files. The pass is then
24
+ // complete, and keeps no green.
25
+ // - The changes go to Vitest in a file that the variable `GRILLHOUSE_CHANGEMENTS` names
26
+ // (scripts/vitest-config/changements.mjs, which reads them in each package); the file lies in a
27
+ // temporary directory of its own, which the pass removes at its end.
28
+ // - `npm test` measures itself against the last green under the circuit's mark,
29
+ // `GRILLHOUSE_DEPUIS_LE_VERT` (scripts/tests.mjs, scripts/circuit.mjs); it never keeps one.
30
+ import { spawnSync } from 'node:child_process';
31
+ import {
32
+ copyFileSync,
33
+ existsSync,
34
+ mkdirSync,
35
+ mkdtempSync,
36
+ readFileSync,
37
+ realpathSync,
38
+ rmSync,
39
+ writeFileSync,
40
+ } from 'node:fs';
41
+ import os from 'node:os';
42
+ import path from 'node:path';
43
+ import { CHANGEMENTS } from './vitest-config/changements.mjs';
44
+
45
+ export const DEPUIS_LE_VERT = 'GRILLHOUSE_DEPUIS_LE_VERT';
46
+ const SHA = /^[0-9a-f]{40,64}$/;
47
+
48
+ const git = (racine, args, env = process.env) =>
49
+ spawnSync('git', ['-C', racine, ...args], {
50
+ encoding: 'utf8',
51
+ env,
52
+ maxBuffer: 256 * 1024 * 1024,
53
+ });
54
+ const sortie = (racine, args, env) => {
55
+ const r = git(racine, args, env);
56
+ return r.status === 0 ? r.stdout.trim() : null;
57
+ };
58
+
59
+ // The file of the last green of the branch <branche> of the repository at <racine>, null out of
60
+ // git.
61
+ function fichierDuVert(racine, branche) {
62
+ const commun = sortie(racine, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
63
+ return commun ? path.join(commun, 'grillhouse', 'dernier-vert', `${branche}.json`) : null;
64
+ }
65
+
66
+ // Main, the branch of the main tree of the repository at <racine> (HEAD of git's common
67
+ // directory); null when that tree is detached.
68
+ function brancheDeMain(racine) {
69
+ const commun = sortie(racine, ['rev-parse', '--path-format=absolute', '--git-common-dir']);
70
+ return commun
71
+ ? sortie(racine, [`--git-dir=${commun}`, 'symbolic-ref', '--short', '-q', 'HEAD'])
72
+ : null;
73
+ }
74
+
75
+ // Whether a submodule of the repository at <racine> differs from the commit its parent records.
76
+ function sousModuleModifie(racine) {
77
+ const r = git(racine, ['status', '--porcelain=v2', '-z', '--ignore-submodules=none']);
78
+ if (r.status !== 0) return true;
79
+ // An entry of a changed path: `1 <XY> <sub> …` (or `2` for a rename), <sub> `S<c><m><u>` for a
80
+ // submodule whose commit, tracked files or untracked files changed, `N...` for any other path.
81
+ return r.stdout.split('\0').some(e => /^[12] \S+ S/.test(e));
82
+ }
83
+
84
+ // The state of the repository at <racine> as it stands: `{ racine, commit, arbre, branche, main }`,
85
+ // its top (its real path), HEAD, the tree of its working tree, the branch of HEAD (null when
86
+ // detached) and main (brancheDeMain); `{ raison }` when no last green can count.
87
+ function etatDuDepot(racine) {
88
+ const haut = sortie(racine, ['rev-parse', '--show-toplevel']);
89
+ if (!haut) return { raison: 'hors de git' };
90
+ const top = realpathSync(haut);
91
+ const commit = sortie(top, ['rev-parse', '--verify', '-q', 'HEAD^{commit}']);
92
+ if (!commit) return { raison: 'aucun commit' };
93
+ if (sousModuleModifie(top)) return { raison: 'un sous-module diffère de son commit' };
94
+ const dir = mkdtempSync(path.join(os.tmpdir(), 'grillhouse-vert-'));
95
+ try {
96
+ const index = path.join(dir, 'index');
97
+ const reel = sortie(top, ['rev-parse', '--path-format=absolute', '--git-path', 'index']);
98
+ const env = { ...process.env, GIT_INDEX_FILE: index };
99
+ if (reel && existsSync(reel)) copyFileSync(reel, index);
100
+ else if (git(top, ['read-tree', 'HEAD'], env).status !== 0) return { raison: 'git échoue' };
101
+ if (git(top, ['add', '-A'], env).status !== 0) return { raison: 'git échoue' };
102
+ const arbre = sortie(top, ['write-tree'], env);
103
+ if (!arbre) return { raison: 'git échoue' };
104
+ const branche = sortie(top, ['symbolic-ref', '--short', '-q', 'HEAD']);
105
+ return { racine: top, commit, arbre, branche, main: brancheDeMain(top) };
106
+ } finally {
107
+ rmSync(dir, { recursive: true, force: true });
108
+ }
109
+ }
110
+
111
+ // The last green that a pass in the state <etat> (etatDuDepot) measures itself against: its
112
+ // branch's when it counts, else main's; `{ commit, arbre, branche }`, `{ raison }` when neither
113
+ // counts (main's reason, or the branch's without main).
114
+ function lireLeVert(etat) {
115
+ const branches = [...new Set([etat.branche, etat.main].filter(Boolean))];
116
+ let vert = { raison: 'aucun vert connu' };
117
+ for (const branche of branches) {
118
+ vert = lireLeVertDe(etat, branche);
119
+ if (!vert.raison) return vert;
120
+ }
121
+ return vert;
122
+ }
123
+
124
+ // The last green of the branch <branche>, when it counts for a pass in the state <etat>:
125
+ // `{ commit, arbre, branche }`; `{ raison }` otherwise.
126
+ function lireLeVertDe(etat, branche) {
127
+ const f = fichierDuVert(etat.racine, branche);
128
+ let v = null;
129
+ try {
130
+ v = JSON.parse(readFileSync(f, 'utf8'));
131
+ } catch {
132
+ return { raison: 'aucun vert connu' };
133
+ }
134
+ if (!SHA.test(v?.commit ?? '') || !SHA.test(v?.arbre ?? '')) {
135
+ return { raison: 'aucun vert connu' };
136
+ }
137
+ if (git(etat.racine, ['cat-file', '-e', `${v.arbre}^{tree}`]).status !== 0) {
138
+ return { raison: 'le dernier vert ne se lit plus' };
139
+ }
140
+ if (git(etat.racine, ['merge-base', '--is-ancestor', v.commit, etat.commit]).status !== 0) {
141
+ return { raison: 'le dernier vert n’est pas un ancêtre de HEAD' };
142
+ }
143
+ return { commit: v.commit, arbre: v.arbre, branche };
144
+ }
145
+
146
+ // Keeps <etat> (etatDuDepot) as the last green of its branch, of main when HEAD is detached.
147
+ export function garderLeVert(etat) {
148
+ const branche = etat.branche ?? etat.main;
149
+ const f = branche ? fichierDuVert(etat.racine, branche) : null;
150
+ if (!f) return;
151
+ mkdirSync(path.dirname(f), { recursive: true });
152
+ writeFileSync(f, `${JSON.stringify({ commit: etat.commit, arbre: etat.arbre }, null, 2)}\n`);
153
+ }
154
+
155
+ // The files that differ between the trees <avant> and <apres> of the repository at <racine>:
156
+ // `{ fichiers, supprimes, sousModules }`, each path from its top; null when git fails.
157
+ function difference(racine, avant, apres) {
158
+ const r = git(racine, ['diff', '--raw', '--no-renames', '-z', avant, apres]);
159
+ if (r.status !== 0) return null;
160
+ const champs = r.stdout.split('\0');
161
+ const out = { fichiers: [], supprimes: [], sousModules: [] };
162
+ for (let i = 0; i + 1 < champs.length; i += 2) {
163
+ const [modeAvant, modeApres, , , statut] = champs[i].replace(/^:/, '').split(' ');
164
+ const f = champs[i + 1];
165
+ if (modeAvant === '160000' || modeApres === '160000') out.sousModules.push(f);
166
+ else if (statut === 'D') out.supprimes.push(f);
167
+ else out.fichiers.push(f);
168
+ }
169
+ return out;
170
+ }
171
+
172
+ // What a pass in <racine> measures itself against: `{ etat, env, ligne, retirer }`, the state to
173
+ // keep once green (null when none can count), the variables to add to the pass's environment (the
174
+ // changes since the last green, none for a complete pass), the line that says which, and the
175
+ // function that removes the file of the changes. <tout> asks for the complete pass; the file lies
176
+ // in <dans>, the base of the pass (scripts/vitest-config/base-de-passe.mjs), which a killed pass
177
+ // leaves to the next one to sweep.
178
+ export function mesure(racine, { tout = false, dans = os.tmpdir() } = {}) {
179
+ const etat = etatDuDepot(racine);
180
+ const complete = raison => ({
181
+ etat: etat.raison ? null : etat,
182
+ env: {},
183
+ ligne: `↳ passe complète : ${raison}.`,
184
+ retirer: () => {},
185
+ });
186
+ if (etat.raison) return complete(etat.raison);
187
+ if (tout) return complete('demandée');
188
+ const vert = lireLeVert(etat);
189
+ if (vert.raison) return complete(vert.raison);
190
+ const ch = difference(etat.racine, vert.arbre, etat.arbre);
191
+ if (!ch) return complete('git échoue');
192
+ if (ch.sousModules.length) {
193
+ return { ...complete(`un sous-module a changé (${ch.sousModules[0]})`), etat: null };
194
+ }
195
+ const dir = mkdtempSync(path.join(dans, 'grillhouse-changements-'));
196
+ const fichier = path.join(dir, 'changements.json');
197
+ writeFileSync(
198
+ fichier,
199
+ JSON.stringify({ racine: etat.racine, fichiers: ch.fichiers, supprimes: ch.supprimes })
200
+ );
201
+ const n = ch.fichiers.length + ch.supprimes.length;
202
+ return {
203
+ etat,
204
+ env: { [CHANGEMENTS]: fichier },
205
+ ligne: `↳ depuis le dernier vert de ${vert.branche} (${vert.commit.slice(0, 12)}, arbre ${vert.arbre.slice(0, 12)}) : ${n} fichier(s) changé(s) ; chaque paquet que Turborepo rejoue joue les tests que Vitest y relie.`,
206
+ retirer: () => rmSync(dir, { recursive: true, force: true }),
207
+ };
208
+ }
@@ -0,0 +1,181 @@
1
+ // The known debt of the guards: the findings a project carries when it adopts the frame, written
2
+ // and frozen in one file at its root, on the model of ESLint's bulk suppressions
3
+ // (`eslint-suppressions.json`, `--suppress-all`, `--prune-suppressions`) and of betterer.
4
+ //
5
+ // The file `grillhouse-dette.json` holds, for each guard, each file and each rule, the count of
6
+ // findings inscribed:
7
+ //
8
+ // {
9
+ // "code-mort": { "src/a.ts": { "exports": { "count": 2 } } },
10
+ // "consommateurs": { "packages/b/docs/INTERFACE.md": { "usage-non-declare": { "count": 1 } } }
11
+ // }
12
+ //
13
+ // A finding is `{ fichier, regle, message }`, and its `ligne` when the guard knows it: its key is
14
+ // the guard, the file and the rule (never a line number), its message what the guard prints. A finding without `fichier` or `regle` is never
15
+ // inscribed: the guard refuses it every time (a generated file out of date, a measure that failed).
16
+ //
17
+ // - A guard passes when, for every key, the findings are no more than the inscribed count. A key
18
+ // whose count goes beyond refuses with all its findings, as ESLint reports them all, and a line
19
+ // naming the key, its count and the inscribed one.
20
+ // - A key inscribed above what the guard finds is reported, without refusing, so it gets pruned.
21
+ // - `GRILLHOUSE_DETTE=inscrire` writes the guard's whole section from what it finds (its debt of
22
+ // today); `GRILLHOUSE_DETTE=elaguer` lowers each inscribed count to what the guard finds and
23
+ // drops the keys at zero, never raising a count nor adding a key. Each guard then judges against
24
+ // the file it wrote. The npm scripts `grillhouse:dette:inscrire` and `grillhouse:dette:elaguer`
25
+ // run every guard so.
26
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
27
+ import path from 'node:path';
28
+ import { ecrireSarif } from './resultats.mjs';
29
+
30
+ export const FICHIER = 'grillhouse-dette.json';
31
+ // The environment variable that names the mode of a run: `inscrire` or `elaguer`.
32
+ export const MODE = 'GRILLHOUSE_DETTE';
33
+ const MODES = new Set(['inscrire', 'elaguer']);
34
+
35
+ const inscriptible = c => Boolean(c.fichier && c.regle);
36
+
37
+ // The findings counted per file and rule: Map fichier → Map regle → count.
38
+ function compter(constats) {
39
+ const out = new Map();
40
+ for (const c of constats.filter(inscriptible)) {
41
+ if (!out.has(c.fichier)) out.set(c.fichier, new Map());
42
+ const parRegle = out.get(c.fichier);
43
+ parRegle.set(c.regle, (parRegle.get(c.regle) ?? 0) + 1);
44
+ }
45
+ return out;
46
+ }
47
+
48
+ // The section of a guard as written in the file, its keys sorted: { fichier: { regle: { count } } }.
49
+ function section(comptes) {
50
+ const out = {};
51
+ for (const fichier of [...comptes.keys()].sort()) {
52
+ const regles = comptes.get(fichier);
53
+ out[fichier] = {};
54
+ for (const regle of [...regles.keys()].sort()) {
55
+ out[fichier][regle] = { count: regles.get(regle) };
56
+ }
57
+ }
58
+ return out;
59
+ }
60
+
61
+ // The debt written at <racine>: {} without the file. Throws, naming the file, when it is not JSON.
62
+ export function lire(racine) {
63
+ const f = path.join(racine, FICHIER);
64
+ if (!existsSync(f)) return {};
65
+ try {
66
+ return JSON.parse(readFileSync(f, 'utf8'));
67
+ } catch (e) {
68
+ throw new Error(`${FICHIER} n'est pas un JSON lisible : ${e.message}`);
69
+ }
70
+ }
71
+
72
+ // <dette> with the section of <garde> replaced by what <constats> count; the guard leaves the file
73
+ // when it finds nothing.
74
+ export function inscrire(dette, garde, constats) {
75
+ const reste = { ...dette };
76
+ delete reste[garde];
77
+ const comptes = compter(constats);
78
+ return comptes.size ? { ...reste, [garde]: section(comptes) } : reste;
79
+ }
80
+
81
+ // <dette> with each count of <garde> lowered to what <constats> count, the keys at zero dropped;
82
+ // a count never rises and no key is added.
83
+ export function elaguer(dette, garde, constats) {
84
+ const { [garde]: ancienne = {}, ...reste } = dette;
85
+ const comptes = compter(constats);
86
+ const garde2 = new Map();
87
+ for (const [fichier, regles] of Object.entries(ancienne)) {
88
+ for (const [regle, { count }] of Object.entries(regles)) {
89
+ const n = Math.min(count, comptes.get(fichier)?.get(regle) ?? 0);
90
+ if (n === 0) continue;
91
+ if (!garde2.has(fichier)) garde2.set(fichier, new Map());
92
+ garde2.get(fichier).set(regle, n);
93
+ }
94
+ }
95
+ return garde2.size ? { ...reste, [garde]: section(garde2) } : reste;
96
+ }
97
+
98
+ // <constats> of <garde> judged against <dette>: `refus` the findings the guard refuses (those
99
+ // without key, and every finding of a key beyond its count), `depasses` the keys beyond a non-zero
100
+ // count, `corriges` the keys inscribed above what is found, `tus` the number of findings the debt
101
+ // covers.
102
+ export function juger(dette, garde, constats) {
103
+ const inscrite = dette[garde] ?? {};
104
+ const comptes = compter(constats);
105
+ const permis = (fichier, regle) => inscrite[fichier]?.[regle]?.count ?? 0;
106
+ const refus = constats.filter(
107
+ c => !inscriptible(c) || comptes.get(c.fichier).get(c.regle) > permis(c.fichier, c.regle)
108
+ );
109
+ const depasses = [];
110
+ for (const [fichier, regles] of comptes) {
111
+ for (const [regle, trouve] of regles) {
112
+ const inscrit = permis(fichier, regle);
113
+ if (inscrit > 0 && trouve > inscrit) depasses.push({ fichier, regle, trouve, inscrit });
114
+ }
115
+ }
116
+ const corriges = [];
117
+ for (const [fichier, regles] of Object.entries(inscrite)) {
118
+ for (const [regle, { count }] of Object.entries(regles)) {
119
+ const trouve = comptes.get(fichier)?.get(regle) ?? 0;
120
+ if (trouve < count) corriges.push({ fichier, regle, trouve, inscrit: count });
121
+ }
122
+ }
123
+ return { refus, depasses, corriges, tus: constats.length - refus.length };
124
+ }
125
+
126
+ // Runs the debt of <garde> at <racine> on its <constats>, in the mode <mode> (the environment's
127
+ // `GRILLHOUSE_DETTE` by default): writes the file in mode `inscrire` or `elaguer`, then judges.
128
+ // Returns `refus`, the findings to print as refusals, and `lignes`, the lines to print after them
129
+ // (the keys beyond their count, the keys to prune, what the run wrote). An unknown mode, or a file
130
+ // that is not JSON, refuses every finding, behind a refusal that says why. The refusals are the
131
+ // guard's findings in SARIF too, under the results directory <env> names (scripts/resultats.mjs),
132
+ // each with its file, its line when the guard gives one, and its rule.
133
+ export function trancher(racine, garde, constats, mode = process.env[MODE] || undefined, env) {
134
+ const { refus, lignes } = juge(racine, garde, constats, mode);
135
+ ecrireSarif({ garde, constats: refus, racine, env: env ?? process.env });
136
+ return { refus, lignes };
137
+ }
138
+
139
+ function juge(racine, garde, constats, mode) {
140
+ let avant;
141
+ try {
142
+ if (mode !== undefined && !MODES.has(mode)) {
143
+ throw new Error(`${MODE}=${mode} : les modes sont « inscrire » et « elaguer ».`);
144
+ }
145
+ avant = lire(racine);
146
+ } catch (e) {
147
+ return { refus: [{ message: e.message }, ...constats], lignes: [] };
148
+ }
149
+ let dette = avant;
150
+ const lignes = [];
151
+ if (mode) {
152
+ dette =
153
+ mode === 'inscrire' ? inscrire(avant, garde, constats) : elaguer(avant, garde, constats);
154
+ const f = path.join(racine, FICHIER);
155
+ if (JSON.stringify(dette) !== JSON.stringify(avant)) {
156
+ const triee = Object.fromEntries(
157
+ Object.keys(dette)
158
+ .sort()
159
+ .map(g => [g, dette[g]])
160
+ );
161
+ writeFileSync(f, `${JSON.stringify(triee, null, 2)}\n`);
162
+ lignes.push(
163
+ `📝 ${FICHIER} : la dette de ${garde} est ${mode === 'inscrire' ? 'inscrite' : 'élaguée'}.`
164
+ );
165
+ }
166
+ }
167
+ const { refus, depasses, corriges, tus } = juger(dette, garde, constats);
168
+ for (const d of depasses) {
169
+ lignes.push(
170
+ `⛔ ${garde} : ${d.fichier} · ${d.regle} — ${d.trouve} constat(s) pour ${d.inscrit} inscrit(s) dans ${FICHIER} : un constat est neuf.`
171
+ );
172
+ }
173
+ for (const c of corriges) {
174
+ lignes.push(
175
+ `⚠️ ${garde} : ${c.fichier} · ${c.regle} — ${c.trouve} constat(s) pour ${c.inscrit} inscrit(s) dans ${FICHIER} : npm run grillhouse:dette:elaguer retire ce qui est corrigé.`
176
+ );
177
+ }
178
+ if (tus > 0)
179
+ lignes.push(`ℹ️ ${garde} : ${tus} constat(s) connu(s), inscrit(s) dans ${FICHIER}.`);
180
+ return { refus, lignes };
181
+ }