@roomi-fields/grillhouse 0.0.0-stage → 0.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +9 -0
- package/METACADRE.md +41 -0
- package/agents/developpeur.md +10 -0
- package/agents/explorateur.md +9 -0
- package/agents/questionneur.md +11 -0
- package/agents/referent-technique.md +9 -0
- package/agents/regulateur.md +11 -0
- package/agents/relecteur.md +9 -0
- package/agents/testwriter.md +10 -0
- package/bin/grillhouse.mjs +125 -0
- package/cadre/templates/projet/.claude/settings.json +30 -0
- package/cadre/templates/projet/.editorconfig +47 -0
- package/cadre/templates/projet/.env.example +57 -0
- package/cadre/templates/projet/.eslintrc.cjs +56 -0
- package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
- package/cadre/templates/projet/.mcp.json +13 -0
- package/cadre/templates/projet/.prettierrc +11 -0
- package/cadre/templates/projet/CHANGELOG.md +61 -0
- package/cadre/templates/projet/CLAUDE.md +46 -0
- package/cadre/templates/projet/CONTRIBUTING.md +243 -0
- package/cadre/templates/projet/CREDITS.md +41 -0
- package/cadre/templates/projet/README.md +152 -0
- package/cadre/templates/projet/_gitignore +91 -0
- package/cadre/templates/projet/_npmignore +46 -0
- package/cadre/templates/projet/deployment/README.md +98 -0
- package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
- package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
- package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
- package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
- package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
- package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
- package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
- package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
- package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
- package/cadre/templates/projet/ecosystem.config.cjs +73 -0
- package/cadre/templates/projet/package.json +90 -0
- package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
- package/cadre/templates/projet/src/config.ts +68 -0
- package/cadre/templates/projet/src/errors.ts +102 -0
- package/cadre/templates/projet/src/index.ts +46 -0
- package/cadre/templates/projet/src/services/.gitkeep +8 -0
- package/cadre/templates/projet/src/types/index.ts +24 -0
- package/cadre/templates/projet/src/utils/logger.ts +106 -0
- package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
- package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
- package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
- package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
- package/cadre/templates/projet/tsconfig.eslint.json +9 -0
- package/cadre/templates/projet/tsconfig.json +41 -0
- package/cadre/templates/projet/tsdoc.json +6 -0
- package/cadre/templates/projet/vitest.config.ts +44 -0
- package/cadre/templates/turbo/paquets.json +29 -0
- package/cadre/templates/turbo/sans-paquets.json +28 -0
- package/charte.md +107 -0
- package/docs/ARCHITECTURE.md +90 -0
- package/hooks/crochet.mjs +33 -0
- package/hooks/hooks.json +58 -0
- package/hooks/register.tsx +558 -0
- package/package.json +75 -4
- package/scripts/avancement.mjs +244 -0
- package/scripts/bilan.mjs +523 -0
- package/scripts/branche-de-nuit.mjs +59 -0
- package/scripts/cadre.mjs +109 -0
- package/scripts/circuit.mjs +4375 -0
- package/scripts/code-mort.mjs +371 -0
- package/scripts/composants-affectes.mjs +598 -0
- package/scripts/consommateurs-arbitres.mjs +192 -0
- package/scripts/consommateurs.mjs +310 -0
- package/scripts/crochet.mjs +24 -0
- package/scripts/crochets.mjs +135 -0
- package/scripts/dernier-vert.mjs +208 -0
- package/scripts/dette.mjs +181 -0
- package/scripts/droits.mjs +354 -0
- package/scripts/enveloppe/codes.mjs +17 -0
- package/scripts/enveloppe/composant.mjs +11 -0
- package/scripts/enveloppe/enveloppe.mjs +628 -0
- package/scripts/enveloppe/lancer.sh +311 -0
- package/scripts/etapes.mjs +323 -0
- package/scripts/etat-du-circuit.mjs +104 -0
- package/scripts/frontieres.mjs +456 -0
- package/scripts/gardes-du-cadre.mjs +72 -0
- package/scripts/greffons.mjs +338 -0
- package/scripts/grillhouse-maj.mjs +514 -0
- package/scripts/index-interfaces.mjs +293 -0
- package/scripts/init.mjs +117 -0
- package/scripts/integration/file-attente.mjs +61 -0
- package/scripts/integration/file.mjs +494 -0
- package/scripts/integration/integrer.mjs +381 -0
- package/scripts/integration/rejeu.mjs +139 -0
- package/scripts/integration/verdict.mjs +13 -0
- package/scripts/interfaces-contre-code.mjs +547 -0
- package/scripts/ligne-etat/defaut.sh +110 -0
- package/scripts/ligne-etat/ligne-etat.mjs +93 -0
- package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
- package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
- package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
- package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
- package/scripts/migrations.mjs +43 -0
- package/scripts/nuit.sh +269 -0
- package/scripts/paquets.mjs +79 -0
- package/scripts/poussee-cles.mjs +46 -0
- package/scripts/preparation.mjs +84 -0
- package/scripts/references-par-les-types.mjs +420 -0
- package/scripts/registre.mjs +336 -0
- package/scripts/resultats.mjs +580 -0
- package/scripts/resumes.mjs +57 -0
- package/scripts/retirer-copie.sh +127 -0
- package/scripts/session-start.sh +203 -0
- package/scripts/sortie.mjs +155 -0
- package/scripts/sous-modules.sh +316 -0
- package/scripts/structure-guard.sh +27 -0
- package/scripts/superviseur.mjs +102 -0
- package/scripts/tableau.mjs +1886 -0
- package/scripts/tests.mjs +236 -0
- package/scripts/ticket.sh +23 -0
- package/scripts/turbo-des-paquets.mjs +899 -0
- package/scripts/un-commit-par-ticket.mjs +212 -0
- package/scripts/verifier.mjs +469 -0
- package/scripts/verrou-de-fichier.mjs +130 -0
- package/scripts/verrou-de-machine.mjs +211 -0
- package/scripts/verrous/commande.mjs +180 -0
- package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
- package/scripts/verrous/verrou.mjs +165 -0
- package/scripts/vitest-config/avancement.mjs +39 -0
- package/scripts/vitest-config/avancement.reporter.mjs +56 -0
- package/scripts/vitest-config/base-de-passe.mjs +128 -0
- package/scripts/vitest-config/base.d.ts +6 -0
- package/scripts/vitest-config/base.mjs +87 -0
- package/scripts/vitest-config/changements.mjs +173 -0
- package/scripts/vitest-config/cibles.mjs +207 -0
- package/scripts/vitest-config/duree.reporter.mjs +98 -0
- package/scripts/vitest-config/machine.setup.mjs +14 -0
- package/scripts/vitest-config/mesure.d.ts +6 -0
- package/scripts/vitest-config/mesure.mjs +13 -0
- package/scripts/vitest-config/mesure.setup.mjs +12 -0
- package/scripts/vitest-config/pression.mjs +90 -0
- package/scripts/vitest-config/resultats.mjs +23 -0
- package/scripts/vitest-config/resultats.setup.mjs +19 -0
- package/scripts/vitest.nuit.config.mjs +26 -0
- package/skills/developpeur/SKILL.md +175 -0
- package/skills/explorateur/SKILL.md +84 -0
- package/skills/grill/SKILL.md +238 -0
- package/skills/grill/references/releve-structurel.md +38 -0
- package/skills/mesure/SKILL.md +168 -0
- package/skills/pitmaster/SKILL.md +382 -0
- package/skills/pitmaster/references/cadre.md +119 -0
- package/skills/pitmaster/references/consigne-agent.md +308 -0
- package/skills/pitmaster/references/erreurs.md +25 -0
- package/skills/pitmaster/references/tour.md +124 -0
- package/skills/questionneur/SKILL.md +64 -0
- package/skills/redacteur/SKILL.md +84 -0
- package/skills/redacteur/references/documents-d-un-composant.md +105 -0
- package/skills/referent-technique/SKILL.md +145 -0
- package/skills/regulateur/SKILL.md +220 -0
- package/skills/release/SKILL.md +134 -0
- package/skills/release/scripts/update-version.cjs +92 -0
- package/skills/relecteur/SKILL.md +152 -0
- package/skills/relecteur/references/LICENSE-MIT +30 -0
- package/skills/relecteur/references/SOURCE.md +8 -0
- package/skills/relecteur/references/claims-check.md +14 -0
- package/skills/relecteur/references/deletion-check.md +14 -0
- package/skills/relecteur/references/edge-case-hunter.md +80 -0
- package/skills/relecteur/references/verification-gap.md +113 -0
- package/skills/testwriter/SKILL.md +187 -0
- package/types/index.d.ts +56 -0
- package/README.md +0 -3
|
@@ -0,0 +1,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
|
+
}
|