@roomi-fields/grillhouse 0.23.1 → 0.24.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 (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/METACADRE.md +1 -1
  3. package/agents/explorateur.md +4 -2
  4. package/agents/questionneur.md +1 -1
  5. package/bin/grillhouse.mjs +3 -58
  6. package/cadre/templates/projet/CLAUDE.md +5 -1
  7. package/cadre/templates/projet/docs/agents/issue-tracker.md +4 -4
  8. package/charte.md +23 -6
  9. package/docs/ARCHITECTURE.md +130 -1
  10. package/hooks/register.tsx +3 -13
  11. package/package.json +13 -1
  12. package/scripts/bilan.mjs +134 -88
  13. package/scripts/circuit.mjs +1547 -2248
  14. package/scripts/composants-affectes.mjs +27 -108
  15. package/scripts/consommateurs-arbitres.mjs +15 -21
  16. package/scripts/consommateurs.d.ts +8 -0
  17. package/scripts/consommateurs.mjs +5 -1
  18. package/scripts/dette.d.ts +23 -0
  19. package/scripts/dette.mjs +76 -13
  20. package/scripts/droits.mjs +38 -44
  21. package/scripts/elements-du-lot.mjs +246 -0
  22. package/scripts/enveloppe/enveloppe.mjs +105 -232
  23. package/scripts/enveloppe/lancer.sh +79 -49
  24. package/scripts/enveloppe/paquets-du-plan.mjs +49 -0
  25. package/scripts/etapes.mjs +148 -213
  26. package/scripts/frontieres.mjs +4 -2
  27. package/scripts/gardes-du-cadre.mjs +8 -6
  28. package/scripts/grillhouse-maj.mjs +113 -19
  29. package/scripts/index-interfaces.mjs +21 -1
  30. package/scripts/integration/integrer.mjs +2 -2
  31. package/scripts/interfaces-contre-code.mjs +25 -17
  32. package/scripts/migrations/0.23.0-config-vitest.mjs +26 -23
  33. package/scripts/migrations/0.23.2-copies-du-cadre.mjs +201 -0
  34. package/scripts/migrations/0.24.0-circuit-simplifie.mjs +274 -0
  35. package/scripts/migrations.mjs +41 -6
  36. package/scripts/paquets.d.ts +5 -0
  37. package/scripts/paquets.mjs +3 -0
  38. package/scripts/references-par-les-types.mjs +55 -36
  39. package/scripts/registre.mjs +7 -15
  40. package/scripts/resultats.mjs +283 -142
  41. package/scripts/resumes.mjs +1 -1
  42. package/scripts/session-start.sh +1 -2
  43. package/scripts/sources.mjs +28 -0
  44. package/scripts/sous-commandes.mjs +62 -0
  45. package/scripts/tableau.mjs +85 -191
  46. package/scripts/tests.mjs +19 -17
  47. package/scripts/verifier.mjs +5 -4
  48. package/scripts/verrou-de-machine.mjs +131 -24
  49. package/scripts/verrous/verrou.mjs +66 -5
  50. package/skills/developpeur/SKILL.md +60 -50
  51. package/skills/explorateur/SKILL.md +90 -64
  52. package/skills/grill/SKILL.md +1 -1
  53. package/skills/mesure/SKILL.md +2 -1
  54. package/skills/pitmaster/SKILL.md +132 -118
  55. package/skills/pitmaster/references/consigne-agent.md +54 -148
  56. package/skills/questionneur/SKILL.md +18 -18
  57. package/skills/relecteur/SKILL.md +69 -23
  58. package/skills/testwriter/SKILL.md +46 -35
  59. package/types/index.d.ts +1 -3
  60. package/agents/referent-technique.md +0 -9
  61. package/agents/regulateur.md +0 -11
  62. package/scripts/enveloppe/composant.mjs +0 -11
  63. package/scripts/etat-du-circuit.mjs +0 -104
  64. package/skills/referent-technique/SKILL.md +0 -145
  65. package/skills/regulateur/SKILL.md +0 -220
@@ -0,0 +1,201 @@
1
+ // The frame's copies leave a project in 0.22.x: the package carries the scripts, its plugin the
2
+ // skills, agents, charter and meta-frame, so the copies the former update placed
3
+ // (`.claude/grillhouse/fichiers.txt`, each file's SHA-256 in `.claude/grillhouse/empreintes.json`)
4
+ // are what this version replaces, as a codemod of `@turbo/codemod` removes what a version replaces.
5
+ // The list says what to remove: nothing is guessed.
6
+ //
7
+ // - Each file `empreintes.json` records that a line of `fichiers.txt` keeps (the file itself, or a
8
+ // directory « …/ » above it; a line « - … » keeps none) goes when its bytes have the recorded
9
+ // SHA-256 (as the former update computed it), or had it before the migration 0.23.0 of Vitest
10
+ // rewrote its import of the base (`formesAnterieures` of 0.23.0-config-vitest.mjs); a directory
11
+ // it leaves empty goes with it. A file the template of a project holds at the same path
12
+ // (`tsdoc.json`, `docs/agents/issue-tracker.md`) is the project's own since 0.23.0, as
13
+ // `grillhouse init` places it, and stays.
14
+ // - Under a directory the list keeps that is no workspace of the project (scripts/paquets.mjs), a
15
+ // file the former generator of the packages' turbo.json wrote (its first line « // Généré par
16
+ // `…turbo-des-paquets…` ») goes too: it belonged to the former workspace of a copy.
17
+ // - A file whose bytes differ (the project modified it) stays: the migration leaves it to the
18
+ // project (`{ aFaire }`), with the command that removes it, and the update's exit code is
19
+ // non-zero; an npm script that runs it is named with it, since its removal breaks that script.
20
+ // While one stays, the list and the fingerprints stay, so that the next update names it again;
21
+ // once none stays, `.claude/grillhouse/` goes whole (the list, the fingerprints, the version, the
22
+ // former charter). A modified copy of a role 0.24.0 removes stays too, but is named by that
23
+ // migration alone (`deRoleSupprime` of 0.24.0-circuit-simplifie.mjs), once.
24
+ // - Each root npm script that runs a copy removed (`node scripts/<chemin>`, `bash scripts/<chemin>`,
25
+ // its arguments kept) runs its subcommand, `grillhouse <sous-commande>`, by the table of
26
+ // scripts/sous-commandes.mjs; a script that still names a copy removed (no subcommand runs it) is
27
+ // named, not rewritten.
28
+ // - A dependency of the project's root that a copy removed imported, that the package declares in
29
+ // its own `dependencies`, that the template of a project does not declare, and that no source of
30
+ // the project still imports, is named, not removed: the project decides.
31
+ // - A project without `fichiers.txt` and `empreintes.json` is not written: a second pass finds
32
+ // neither, or finds only the copies left to the project, which it names again.
33
+ import { createHash } from 'node:crypto';
34
+ import { existsSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
35
+ import path from 'node:path';
36
+ import { CADRE, PAQUET } from '../cadre.mjs';
37
+ import { paquets } from '../paquets.mjs';
38
+ import { sources } from '../sources.mjs';
39
+ import { SOUS_COMMANDES } from '../sous-commandes.mjs';
40
+ import { formesAnterieures } from './0.23.0-config-vitest.mjs';
41
+ import { deRoleSupprime } from './0.24.0-circuit-simplifie.mjs';
42
+
43
+ export const version = '0.23.2';
44
+ export const nom = 'copies du cadre';
45
+
46
+ const DOSSIER = '.claude/grillhouse';
47
+ const LISTE = `${DOSSIER}/fichiers.txt`;
48
+ const EMPREINTES = `${DOSSIER}/empreintes.json`;
49
+ const GABARIT = 'templates/projet';
50
+ const CHAMPS = ['dependencies', 'devDependencies', 'optionalDependencies'];
51
+ const CODE = /\.(?:[mc]?ts|[mc]?js)$/;
52
+ // A call of a script of the frame's former copies in an npm script: its runner, then its path.
53
+ const APPEL = /(^|[\s;&|(])(?:node|bash)\s+(?:\.\/)?(scripts\/[^\s'";&|)]+)/g;
54
+ // The first line of a file the generator of the packages' turbo.json wrote.
55
+ const GENERE = /^\/\/ Généré par `[^`\n]*turbo-des-paquets[^`\n]*`/;
56
+
57
+ const hacher = octets => createHash('sha256').update(octets).digest('hex');
58
+ const lireJson = f => JSON.parse(readFileSync(f, 'utf8'));
59
+ const echapper = t => t.replace(/[.*+?^${}()|[\]\\/]/g, '\\$&');
60
+ // Whether the source text <texte> imports the package <dep> (or a path into it).
61
+ const importe = (texte, dep) =>
62
+ new RegExp(
63
+ `(?:\\bfrom\\s*|\\bimport\\s*\\(?\\s*|\\brequire\\s*\\(\\s*)(['"])${echapper(dep)}(?:/[^'"]*)?\\1`
64
+ ).test(texte);
65
+
66
+ // The subcommand that runs each script of the package, by the path of its former copy.
67
+ const PAR_COPIE = new Map(Object.entries(SOUS_COMMANDES).map(([n, s]) => [`scripts/${s}`, n]));
68
+
69
+ // The lines of the list that keep a path: a file, or a directory « …/ ».
70
+ const gardees = texte =>
71
+ texte
72
+ .split('\n')
73
+ .map(l => l.trim())
74
+ .filter(l => l !== '' && !l.startsWith('#') && !l.startsWith('-'));
75
+ const couvert = (lignes, f) => lignes.some(l => (l.endsWith('/') ? f.startsWith(l) : f === l));
76
+
77
+ // Whether the file at <absolu> has the fingerprint <attendue>, as copied or as 0.23.0 rewrote it.
78
+ function intacte(absolu, attendue) {
79
+ const octets = readFileSync(absolu);
80
+ if (hacher(octets) === attendue) return true;
81
+ return formesAnterieures(octets.toString('utf8')).some(t => hacher(t) === attendue);
82
+ }
83
+
84
+ // The files under the directory <rel> of <projet>, as paths from it.
85
+ function fichiersSous(projet, rel) {
86
+ const absolu = path.join(projet, rel);
87
+ if (!existsSync(absolu)) return [];
88
+ return readdirSync(absolu, { withFileTypes: true }).flatMap(e => {
89
+ const r = `${rel}/${e.name}`;
90
+ if (e.isDirectory()) return fichiersSous(projet, r);
91
+ return e.isFile() ? [r] : [];
92
+ });
93
+ }
94
+
95
+ // Removes each directory above <f> (a path from <projet>) that is left empty.
96
+ function viderDossiers(projet, f) {
97
+ for (let d = path.dirname(f); d !== '.'; d = path.dirname(d)) {
98
+ const absolu = path.join(projet, d);
99
+ if (!existsSync(absolu) || readdirSync(absolu).length > 0) return;
100
+ rmSync(absolu, { recursive: true });
101
+ }
102
+ }
103
+
104
+ /** Takes the frame's former copies off the project at <projet>; returns what it did. */
105
+ export function migrer(projet) {
106
+ const liste = path.join(projet, LISTE);
107
+ const fEmpreintes = path.join(projet, EMPREINTES);
108
+ if (!existsSync(liste) || !existsSync(fEmpreintes)) return [];
109
+ const lignes = gardees(readFileSync(liste, 'utf8'));
110
+ const enregistres = Object.entries(lireJson(fEmpreintes).fichiers ?? {}).filter(
111
+ ([f]) =>
112
+ couvert(lignes, f) &&
113
+ !f.startsWith(`${DOSSIER}/`) &&
114
+ !existsSync(path.join(CADRE, GABARIT, f))
115
+ );
116
+ const sortie = [];
117
+ const retires = new Map();
118
+ const restes = [];
119
+ const retirer = (f, ligne) => {
120
+ const absolu = path.join(projet, f);
121
+ retires.set(f, CODE.test(f) ? readFileSync(absolu, 'utf8') : '');
122
+ rmSync(absolu);
123
+ viderDossiers(projet, f);
124
+ sortie.push(ligne);
125
+ };
126
+ for (const [f, attendue] of enregistres) {
127
+ if (!existsSync(path.join(projet, f))) continue;
128
+ if (intacte(path.join(projet, f), attendue)) {
129
+ retirer(f, `${f} : copie du cadre retirée, que le paquet porte`);
130
+ } else if (!deRoleSupprime(f)) {
131
+ restes.push(f);
132
+ sortie.push({
133
+ aFaire: `${f} : copie du cadre modifiée par le projet, laissée en place ; \`git rm ${f}\` la retire, le paquet porte le cadre`,
134
+ });
135
+ }
136
+ }
137
+ const espaces = new Set(paquets(projet).map(p => p.dossier));
138
+ for (const d of lignes.filter(l => l.endsWith('/') && !l.startsWith(`${DOSSIER}/`))) {
139
+ const dossier = d.replace(/\/$/, '');
140
+ if (espaces.has(dossier)) continue;
141
+ for (const f of fichiersSous(projet, dossier)) {
142
+ if (!GENERE.test(readFileSync(path.join(projet, f), 'utf8'))) continue;
143
+ retirer(f, `${f} : fichier engendré pour l'ancien espace de travail ${dossier}, retiré`);
144
+ }
145
+ }
146
+ if (restes.length === 0) {
147
+ rmSync(path.join(projet, DOSSIER), { recursive: true, force: true });
148
+ viderDossiers(projet, DOSSIER);
149
+ sortie.push(`${DOSSIER}/ : retiré (la liste et les empreintes des copies du cadre)`);
150
+ } else {
151
+ for (const e of readdirSync(path.join(projet, DOSSIER))) {
152
+ if (`${DOSSIER}/${e}` !== LISTE && `${DOSSIER}/${e}` !== EMPREINTES) {
153
+ retirer(`${DOSSIER}/${e}`, `${DOSSIER}/${e} : retiré`);
154
+ }
155
+ }
156
+ }
157
+
158
+ const fPkg = path.join(projet, 'package.json');
159
+ if (!existsSync(fPkg)) return sortie;
160
+ const pkg = lireJson(fPkg);
161
+ let reecrit = false;
162
+ for (const [n, script] of Object.entries(pkg.scripts ?? {})) {
163
+ if (typeof script !== 'string') continue;
164
+ const neuf = script.replace(APPEL, (tout, avant, f) =>
165
+ retires.has(f) && PAR_COPIE.has(f) ? `${avant}grillhouse ${PAR_COPIE.get(f)}` : tout
166
+ );
167
+ if (neuf !== script) {
168
+ pkg.scripts[n] = neuf;
169
+ reecrit = true;
170
+ sortie.push(`package.json : le script ${n} lance \`${neuf}\``);
171
+ }
172
+ for (const f of retires.keys()) {
173
+ if (neuf.includes(f)) {
174
+ sortie.push(
175
+ `package.json : le script ${n} nomme ${f}, copie retirée qu'aucune sous-commande ne remplace ; à réécrire`
176
+ );
177
+ }
178
+ }
179
+ for (const f of restes.filter(r => neuf.includes(r))) {
180
+ const sous = PAR_COPIE.get(f);
181
+ sortie.push({
182
+ aFaire: `package.json : le script ${n} lance ${f}, copie modifiée laissée en place ; son \`git rm\` le casse : ${sous ? `\`grillhouse ${sous}\` le remplace` : 'à réécrire'}`,
183
+ });
184
+ }
185
+ }
186
+ if (reecrit) writeFileSync(fPkg, `${JSON.stringify(pkg, null, 2)}\n`);
187
+
188
+ const duPaquet = Object.keys(lireJson(path.join(PAQUET, 'package.json')).dependencies ?? {});
189
+ const gabarit = lireJson(path.join(CADRE, GABARIT, 'package.json'));
190
+ const textes = sources(projet).map(f => readFileSync(path.join(projet, f), 'utf8'));
191
+ for (const dep of duPaquet) {
192
+ if (!CHAMPS.some(c => pkg[c]?.[dep] !== undefined)) continue;
193
+ if (CHAMPS.some(c => gabarit[c]?.[dep] !== undefined)) continue;
194
+ if (![...retires.values()].some(t => importe(t, dep))) continue;
195
+ if (textes.some(t => importe(t, dep))) continue;
196
+ sortie.push(
197
+ `package.json : la dépendance ${dep} n'est plus importée que par le paquet, qui la déclare lui-même ; \`npm uninstall ${dep}\` la retire si le projet n'en a pas l'usage`
198
+ );
199
+ }
200
+ return sortie;
201
+ }
@@ -0,0 +1,274 @@
1
+ // The circuit of 0.24.0 has no regulator and no technical referent: the circuit makes the mechanical
2
+ // gestures, and any other question goes up to the supervisor with its piece, who takes to the owner
3
+ // what is his. What a project holds of the former circuit is rewritten here, as a codemod of
4
+ // `@turbo/codemod` rewrites what a version changes, and nothing else.
5
+ //
6
+ // - The rubric « Ce qui monte au superviseur » of the project's CLAUDE.md: the paragraph of the
7
+ // template of 0.22.x and 0.23.x (« Le fonctionnel que le régulateur ne tranche jamais seul : <la
8
+ // liste>. Le cadre et l'architecture montent toujours (charte commune). »), its list as the
9
+ // project filled it, becomes the paragraph of the template of 0.24.0, the list kept, wrapped at
10
+ // 100 columns; the rest of the rubric stays. A rubric the project wrote itself that still names
11
+ // a removed role is left to the project (`{ aFaire }`); one that names none is left silently.
12
+ // - `docs/agents/issue-tracker.md`, the project's own since 0.23.0 (`grillhouse init` places it):
13
+ // a file whose bytes are those of the template of 0.21.0 to 0.22.x, or of 0.23.x (their SHA-256
14
+ // in FORMES), becomes the template's; a file the project modified that names a removed role is
15
+ // left to the project.
16
+ // - A copy of a removed role (`.claude/agents/<rôle>.md`, `.claude/skills/<rôle>/`) still in the
17
+ // project is one the migration of the copies (0.23.2-copies-du-cadre.mjs, played before) could
18
+ // not know as the frame's: the project modified it, or no list named it. It is left to the
19
+ // project, with the command that removes it; the migration of the copies leaves it to this one
20
+ // (deRoleSupprime), so that it is named once.
21
+ // - What stays to the project (`restes`, which the update plays after its own update and its
22
+ // regeneration, so that it sees the final state): a tracked file of the project (`git ls-files`,
23
+ // node_modules and the changelogs aside) that still calls or names a script of the frame at its former address under `scripts/`
24
+ // (`node scripts/<script>`, `$RACINE/scripts/<script>`, `file://$1/scripts/<script>`,
25
+ // `path.join(…, '..', 'scripts', '<dossier>', '<script>')`, found by its words, sousScripts; a
26
+ // `scripts/` under a literal directory, a package of the project or the installed package, is
27
+ // another's, `.` and `..` naming none; a tracked path that is no regular file, a submodule or a
28
+ // link, is skipped), where the project holds no such file any more, is named with its file:line
29
+ // and what replaces it: the subcommand (`SOUS_COMMANDES` of scripts/sous-commandes.mjs), the hook
30
+ // (`grillhouse hook <nom>`, `CROCHETS` of scripts/crochets.mjs) or the named entry (the
31
+ // `exports` of the package). A script of the package that has none of them is named with « aucune
32
+ // entrée publiée ne la remplace ». The project's scripts are its own: none is rewritten, and the
33
+ // lines are a report, which never holds back the version the project records, as a codemod
34
+ // reports what it leaves without failing.
35
+ // - No version of the update wrote a setting of a role in `.claude/settings.json` or under
36
+ // `grillhouse` of the package.json (the update of 0.22.x wrote the hooks, the status line and the
37
+ // plugins; the rights of a role are given on the command line of its session, scripts/droits.mjs):
38
+ // the migration writes neither.
39
+ // - A project already in the new form is not written, and a second pass reports only what it left
40
+ // to the project.
41
+ import { execFileSync } from 'node:child_process';
42
+ import { createHash } from 'node:crypto';
43
+ import { existsSync, lstatSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
44
+ import path from 'node:path';
45
+ import { CADRE, PAQUET } from '../cadre.mjs';
46
+ import { CROCHETS } from '../crochets.mjs';
47
+ import { SOUS_COMMANDES } from '../sous-commandes.mjs';
48
+
49
+ export const version = '0.24.0';
50
+ export const nom = 'circuit simplifié';
51
+
52
+ const SUPPRIMES = ['regulateur', 'referent-technique'];
53
+
54
+ /** Whether <f>, a path from a project's root, is in a copy of a removed role, which this migration names. */
55
+ export const deRoleSupprime = f =>
56
+ SUPPRIMES.some(r => f === `.claude/agents/${r}.md` || f.startsWith(`.claude/skills/${r}/`));
57
+ // A removed role named in a text, in its words or by its name.
58
+ const NOMME_UN_SUPPRIME = /r[ée]gulat(?:eur|rice)|referent-technique|r[ée]f[ée]rents? techniques?/i;
59
+
60
+ const RUBRIQUE = 'Ce qui monte au superviseur';
61
+ // The section of the rubric, its title line apart, up to the next section or the end.
62
+ const SECTION = new RegExp(`^(## ${RUBRIQUE}[ \\t]*\\n)([^]*?)(?=^## |(?![^]))`, 'm');
63
+ // The paragraph of the former template, its list captured, as a paragraph of its own.
64
+ const ANCIEN =
65
+ /(^\n*|\n\n)Le\s+fonctionnel\s+que\s+le\s+régulateur\s+ne\s+tranche\s+jamais\s+seul\s+:\s+([^]*?)\.\s+Le\s+cadre\s+et\s+l'architecture\s+montent\s+toujours\s+\(charte\s+commune\)\.(?=\n\n|\n?$)/;
66
+ const NEUF = liste =>
67
+ `Une question qu'aucun geste du circuit ne tranche monte au superviseur, avec sa pièce. Il monte au responsable le fonctionnel, qu'il ne tranche jamais seul : ${liste}. Le cadre et l'architecture montent toujours (charte commune).`;
68
+ const COLONNES = 100;
69
+
70
+ const SUIVI = 'docs/agents/issue-tracker.md';
71
+ // The SHA-256 of the guide of the issue tracker as the template held it: 0.21.0 to 0.22.6 (the
72
+ // copy the update placed), 0.23.0 and 0.23.1 (the template `grillhouse init` places).
73
+ const FORMES = new Set([
74
+ '2b7c70b22001c83009887372f2bc5b04509e8e4dbc14f1e647df2c686b0ba5e0',
75
+ 'a6d6e9ecbb75b82dad9c6adadd310fe1047c4a5f83d4d789718cd921eda6b258',
76
+ ]);
77
+
78
+ const hacher = octets => createHash('sha256').update(octets).digest('hex');
79
+
80
+ // The words of <texte> in lines of at most COLONNES characters.
81
+ function replier(texte) {
82
+ const lignes = [];
83
+ for (const mot of texte.split(/\s+/).filter(Boolean)) {
84
+ const l = lignes.length - 1;
85
+ if (l >= 0 && lignes[l].length + 1 + mot.length <= COLONNES) {
86
+ lignes[l] += ` ${mot}`;
87
+ } else {
88
+ lignes.push(mot);
89
+ }
90
+ }
91
+ return lignes.join('\n');
92
+ }
93
+
94
+ // The rubric of the CLAUDE.md of <projet>.
95
+ function rubrique(projet) {
96
+ const fichier = path.join(projet, 'CLAUDE.md');
97
+ if (!existsSync(fichier)) {
98
+ return [];
99
+ }
100
+ const texte = readFileSync(fichier, 'utf8');
101
+ const s = SECTION.exec(texte);
102
+ if (!s) {
103
+ return [];
104
+ }
105
+ const [tout, titre, corps] = s;
106
+ if (ANCIEN.test(corps)) {
107
+ const neuf = corps.replace(ANCIEN, (_, avant, liste) => `${avant}${replier(NEUF(liste))}`);
108
+ writeFileSync(
109
+ fichier,
110
+ texte.replace(tout, () => `${titre}${neuf}`)
111
+ );
112
+ return [
113
+ `CLAUDE.md : la rubrique « ${RUBRIQUE} » dit le circuit de la 0.24.0 (une question qu'aucun geste ne tranche monte au superviseur, avec sa pièce), sa liste gardée`,
114
+ ];
115
+ }
116
+ if (!NOMME_UN_SUPPRIME.test(corps)) {
117
+ return [];
118
+ }
119
+ return [
120
+ {
121
+ aFaire: `CLAUDE.md : la rubrique « ${RUBRIQUE} », que le projet a écrite, nomme un rôle que la 0.24.0 supprime (le régulateur ou le référent technique) ; à réécrire : une question qu'aucun geste du circuit ne tranche monte au superviseur, avec sa pièce, et il monte au responsable ce que la rubrique nomme`,
122
+ },
123
+ ];
124
+ }
125
+
126
+ // The guide of the issue tracker of <projet>.
127
+ function suivi(projet) {
128
+ const fichier = path.join(projet, SUIVI);
129
+ if (!existsSync(fichier)) {
130
+ return [];
131
+ }
132
+ const octets = readFileSync(fichier);
133
+ if (FORMES.has(hacher(octets))) {
134
+ writeFileSync(fichier, readFileSync(path.join(CADRE, 'templates/projet', SUIVI)));
135
+ return [`${SUIVI} : le guide du gabarit, qui dit le circuit de la 0.24.0`];
136
+ }
137
+ if (!NOMME_UN_SUPPRIME.test(octets.toString('utf8'))) {
138
+ return [];
139
+ }
140
+ return [
141
+ {
142
+ aFaire: `${SUIVI} : modifié par le projet, il nomme un rôle que la 0.24.0 supprime ; le gabarit du paquet (templates/projet/${SUIVI}) dit le circuit neuf`,
143
+ },
144
+ ];
145
+ }
146
+
147
+ // The copies of the removed roles left in <projet>.
148
+ function copies(projet) {
149
+ return SUPPRIMES.flatMap(r => [
150
+ { f: `.claude/agents/${r}.md`, rm: 'git rm' },
151
+ { f: `.claude/skills/${r}`, rm: 'git rm -r' },
152
+ ])
153
+ .filter(({ f }) => existsSync(path.join(projet, f)))
154
+ .map(({ f, rm }) => ({
155
+ aFaire: `${f} : copie d'un rôle que la 0.24.0 supprime (le circuit ne le lance plus), laissée en place ; \`${rm} ${f}\` la retire`,
156
+ }));
157
+ }
158
+
159
+ // What replaces each script of the frame, by its path under `scripts/`: its subcommand, its hook,
160
+ // its named entry; none for a script of the package that has none of them.
161
+ function remplacants() {
162
+ const r = new Map();
163
+ const ajouter = (script, quoi) => r.set(script, [...(r.get(script) ?? []), quoi]);
164
+ for (const [n, s] of Object.entries(SOUS_COMMANDES)) {
165
+ ajouter(s, `\`grillhouse ${n}\``);
166
+ }
167
+ for (const [n, c] of Object.entries(CROCHETS)) {
168
+ ajouter(c.garde, `\`grillhouse hook ${n}\``);
169
+ }
170
+ const { name, exports = {} } = JSON.parse(
171
+ readFileSync(path.join(PAQUET, 'package.json'), 'utf8')
172
+ );
173
+ for (const [entree, cible] of Object.entries(exports)) {
174
+ const d = typeof cible === 'string' ? cible : cible?.default;
175
+ if (typeof d === 'string' && d.startsWith('./scripts/')) {
176
+ ajouter(d.slice('./scripts/'.length), `\`${name}${entree.slice(1)}\``);
177
+ }
178
+ }
179
+ const scripts = path.join(PAQUET, 'scripts');
180
+ for (const e of readdirSync(scripts, { recursive: true, withFileTypes: true })) {
181
+ const s = path.relative(scripts, path.join(e.parentPath, e.name)).split(path.sep).join('/');
182
+ if (e.isFile() && SCRIPT.test(s) && !r.has(s)) {
183
+ r.set(s, []);
184
+ }
185
+ }
186
+ return r;
187
+ }
188
+ const SCRIPT = /\.(?:mjs|sh)$/;
189
+ const SANS = /(^|\/)(?:node_modules\/|CHANGELOG\.md$)/;
190
+ // What separates the words of a line, and the arguments of a call.
191
+ const BLANCS = /[\s'"`();|&<>=,]+/;
192
+ const APPELS = /[();]/;
193
+ const cite = a => a.length >= 2 && /^['"`]$/.test(a[0]) && a.at(-1) === a[0];
194
+ // Whether a piece of a path names a directory: `.` and `..` name none, only a step.
195
+ const dossier = m => m !== '.' && m !== '..';
196
+
197
+ // The paths under `scripts/` that the line <ligne> names at a project's root, each as its pieces
198
+ // after `scripts`: a word cut at its `/` (`scripts/x.mjs`, `./scripts/x.mjs`, `../scripts/x.mjs`,
199
+ // `$RACINE/scripts/x.mjs`, `file://$1/scripts/x.mjs`), whose piece before `scripts` is none, a
200
+ // step (`.`, `..`) or a variable; the arguments of a call (`path.join(RACINE, 'scripts', 'd',
201
+ // 'x.mjs')`, `path.join(dir, '..', 'scripts', 'x.mjs')`), whose argument before `'scripts'` is
202
+ // none, no literal or a step, the literals after it its pieces. A `scripts/` under a literal
203
+ // directory (`packages/a/scripts/`, the installed package) is another's.
204
+ function sousScripts(ligne) {
205
+ const chemins = [];
206
+ for (const mot of ligne.split(BLANCS)) {
207
+ const p = mot.split('/');
208
+ p.forEach((m, i) => {
209
+ if (m === 'scripts' && (i === 0 || !dossier(p[i - 1]) || p[i - 1].startsWith('$'))) {
210
+ chemins.push(p.slice(i + 1));
211
+ }
212
+ });
213
+ }
214
+ for (const appel of ligne.split(APPELS)) {
215
+ const a = appel.split(',').map(x => x.trim());
216
+ a.forEach((x, i) => {
217
+ const avant = a[i - 1];
218
+ const libre = i === 0 || !cite(avant) || !dossier(avant.slice(1, -1));
219
+ if (cite(x) && x.slice(1, -1) === 'scripts' && libre) {
220
+ const suite = a.slice(i + 1);
221
+ const fin = suite.findIndex(y => !cite(y));
222
+ chemins.push(suite.slice(0, fin === -1 ? suite.length : fin).map(y => y.slice(1, -1)));
223
+ }
224
+ });
225
+ }
226
+ return chemins;
227
+ }
228
+
229
+ /** The lines of the tracked files of <projet> that still reach a script of the frame at its former address. */
230
+ export function restes(projet) {
231
+ let suivis;
232
+ try {
233
+ suivis = execFileSync('git', ['-C', projet, 'ls-files', '-z'], { encoding: 'utf8' })
234
+ .split('\0')
235
+ .filter(f => f && !SANS.test(f));
236
+ } catch {
237
+ return [];
238
+ }
239
+ const par = remplacants();
240
+ const sortie = [];
241
+ for (const f of suivis) {
242
+ const absolu = path.join(projet, f);
243
+ // A submodule, a link, a file gone from the disk: no text of the project to read.
244
+ if (!existsSync(absolu) || !lstatSync(absolu).isFile()) {
245
+ continue;
246
+ }
247
+ const texte = readFileSync(absolu, 'utf8');
248
+ if (texte.includes('\0')) {
249
+ continue;
250
+ }
251
+ texte.split('\n').forEach((ligne, i) => {
252
+ const scripts = sousScripts(ligne).flatMap(pieces =>
253
+ pieces.map((_, n) => pieces.slice(0, n + 1).join('/')).filter(s => par.has(s))
254
+ );
255
+ for (const s of new Set(scripts)) {
256
+ const quoi = par.get(s);
257
+ if (existsSync(path.join(projet, 'scripts', s))) {
258
+ continue;
259
+ }
260
+ const suite =
261
+ quoi.length > 0
262
+ ? `${quoi.join(' ou ')} la remplace`
263
+ : 'aucune entrée publiée ne la remplace';
264
+ sortie.push(`${f}:${i + 1} : appelle scripts/${s}, copie du cadre retirée ; ${suite}`);
265
+ }
266
+ });
267
+ }
268
+ return sortie;
269
+ }
270
+
271
+ /** Rewrites in the project at <projet> what the circuit of 0.24.0 changes; returns what it did. */
272
+ export function migrer(projet) {
273
+ return [...rubrique(projet), ...suivi(projet), ...copies(projet)];
274
+ }
@@ -4,21 +4,37 @@
4
4
  //
5
5
  // - A migration exports `version` (the version of the package that brings the change of form),
6
6
  // `nom` and `migrer(projet)`, which brings the project at <projet> to the new form and returns
7
- // the lines of what it changed: none when the project already has it. It is replayable without
8
- // effect: a second pass changes no byte and returns no line.
7
+ // the lines of what it changed: none when the project already has it. A line `{ aFaire }` is
8
+ // what it leaves to the project (a copy of the frame the project modified), which the update
9
+ // writes on its error stream, its exit code non-zero, as `@turbo/codemod` fails on a file it
10
+ // could not transform. It is replayable without effect: a second pass changes no byte and
11
+ // returns no line.
9
12
  // - MIGRATIONS lists them, each imported by its module; the update plays those newer than the
10
13
  // version the project recorded (`grillhouse.version` of its package.json), in the order of their
11
14
  // versions; a project that recorded none plays them all. The package carries no migration newer
12
15
  // than itself once released, so the range ends with the package.
16
+ // - A migration may also export `restes(projet)`: the lines of what stays to the project once the
17
+ // update is done, which the update plays after its own update and its regeneration, for each
18
+ // migration it played, and reports (« reste au projet ») without holding back the version, as a
19
+ // codemod reports what it leaves without failing (restesAuProjet).
13
20
  // - Each migration has its test, tests/unit/migration.<nom>.spec.ts.
14
21
  import semver from 'semver';
15
22
  import * as charteImportee from './migrations/0.23.0-charte-importee.mjs';
16
23
  import * as configVitest from './migrations/0.23.0-config-vitest.mjs';
17
24
  import * as crochetsDeSeance from './migrations/0.23.0-crochets-de-seance.mjs';
18
25
  import * as marcheDuProjet from './migrations/0.23.0-marche-du-projet.mjs';
26
+ import * as copiesDuCadre from './migrations/0.23.2-copies-du-cadre.mjs';
27
+ import * as circuitSimplifie from './migrations/0.24.0-circuit-simplifie.mjs';
19
28
 
20
29
  /** The migrations of the package. */
21
- export const MIGRATIONS = [crochetsDeSeance, configVitest, marcheDuProjet, charteImportee];
30
+ export const MIGRATIONS = [
31
+ crochetsDeSeance,
32
+ configVitest,
33
+ marcheDuProjet,
34
+ charteImportee,
35
+ copiesDuCadre,
36
+ circuitSimplifie,
37
+ ];
22
38
 
23
39
  /**
24
40
  * The migrations of <liste> newer than <depuis> (all of them when it is null), in the order of
@@ -32,12 +48,31 @@ export function migrationsAJouer(depuis, liste = MIGRATIONS) {
32
48
  .map(({ m }) => m);
33
49
  }
34
50
 
51
+ /**
52
+ * What stays to the project at <projet> after the update, for each migration newer than <depuis>
53
+ * that exports `restes` and names something: `{ version, nom, lignes }`.
54
+ */
55
+ export function restesAuProjet(projet, depuis, liste = MIGRATIONS) {
56
+ return migrationsAJouer(depuis, liste)
57
+ .filter(m => typeof m.restes === 'function')
58
+ .map(m => ({ version: m.version, nom: m.nom, lignes: m.restes(projet) }))
59
+ .filter(r => r.lignes.length > 0);
60
+ }
61
+
35
62
  /**
36
63
  * Plays on the project at <projet> each migration newer than <depuis>; returns, for each one that
37
- * changed something, `{ version, nom, lignes }`.
64
+ * changed something or left something to do, `{ version, nom, lignes, aFaire }`.
38
65
  */
39
66
  export function migrer(projet, depuis, liste = MIGRATIONS) {
40
67
  return migrationsAJouer(depuis, liste)
41
- .map(m => ({ version: m.version, nom: m.nom, lignes: m.migrer(projet) }))
42
- .filter(r => r.lignes.length > 0);
68
+ .map(m => {
69
+ const tout = m.migrer(projet);
70
+ return {
71
+ version: m.version,
72
+ nom: m.nom,
73
+ lignes: tout.filter(l => typeof l === 'string'),
74
+ aFaire: tout.filter(l => typeof l !== 'string').map(l => l.aFaire),
75
+ };
76
+ })
77
+ .filter(r => r.lignes.length > 0 || r.aFaire.length > 0);
43
78
  }
@@ -0,0 +1,5 @@
1
+ // The types of ./paquets.mjs as the entry `@roomi-fields/grillhouse/paquets` publishes it, for a
2
+ // project's guard that leaves out the packages out of the frame.
3
+
4
+ /** The names listed in `racine`'s `docs/agents/hors-cadre.txt`: empty without the file. */
5
+ export function horsCadre(racine: string): Set<string>;
@@ -12,6 +12,9 @@
12
12
  //
13
13
  // This module and the ones it imports use Node alone: the update runs them from a source checkout
14
14
  // of the frame that installs no dependency.
15
+ //
16
+ // A project's guard imports `horsCadre` by the package's entry `@roomi-fields/grillhouse/paquets`,
17
+ // its types in ./paquets.d.ts (docs/ARCHITECTURE.md, the package's named entries).
15
18
  import { existsSync, globSync, readFileSync } from 'node:fs';
16
19
  import path from 'node:path';
17
20