@roomi-fields/grillhouse 0.23.1 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/METACADRE.md +1 -1
  3. package/agents/explorateur.md +4 -2
  4. package/agents/questionneur.md +1 -1
  5. package/bin/grillhouse.mjs +3 -58
  6. package/cadre/templates/projet/CLAUDE.md +5 -1
  7. package/cadre/templates/projet/docs/agents/issue-tracker.md +4 -4
  8. package/charte.md +23 -6
  9. package/docs/ARCHITECTURE.md +127 -1
  10. package/hooks/register.tsx +3 -13
  11. package/package.json +13 -1
  12. package/scripts/bilan.mjs +134 -88
  13. package/scripts/circuit.mjs +1547 -2248
  14. package/scripts/composants-affectes.mjs +27 -108
  15. package/scripts/consommateurs-arbitres.mjs +15 -21
  16. package/scripts/consommateurs.d.ts +8 -0
  17. package/scripts/consommateurs.mjs +5 -1
  18. package/scripts/dette.d.ts +23 -0
  19. package/scripts/dette.mjs +76 -13
  20. package/scripts/droits.mjs +38 -44
  21. package/scripts/elements-du-lot.mjs +246 -0
  22. package/scripts/enveloppe/enveloppe.mjs +105 -232
  23. package/scripts/enveloppe/lancer.sh +79 -49
  24. package/scripts/enveloppe/paquets-du-plan.mjs +49 -0
  25. package/scripts/etapes.mjs +148 -213
  26. package/scripts/gardes-du-cadre.mjs +8 -6
  27. package/scripts/grillhouse-maj.mjs +31 -9
  28. package/scripts/index-interfaces.mjs +21 -1
  29. package/scripts/integration/integrer.mjs +2 -2
  30. package/scripts/interfaces-contre-code.mjs +25 -17
  31. package/scripts/migrations/0.23.0-config-vitest.mjs +26 -23
  32. package/scripts/migrations/0.23.2-copies-du-cadre.mjs +201 -0
  33. package/scripts/migrations/0.24.0-circuit-simplifie.mjs +231 -0
  34. package/scripts/migrations.mjs +41 -6
  35. package/scripts/paquets.d.ts +5 -0
  36. package/scripts/paquets.mjs +3 -0
  37. package/scripts/references-par-les-types.mjs +55 -36
  38. package/scripts/registre.mjs +7 -15
  39. package/scripts/resultats.mjs +283 -142
  40. package/scripts/resumes.mjs +1 -1
  41. package/scripts/session-start.sh +1 -2
  42. package/scripts/sources.mjs +28 -0
  43. package/scripts/sous-commandes.mjs +62 -0
  44. package/scripts/tableau.mjs +85 -191
  45. package/scripts/tests.mjs +19 -17
  46. package/scripts/verifier.mjs +5 -4
  47. package/scripts/verrou-de-machine.mjs +131 -24
  48. package/scripts/verrous/verrou.mjs +66 -5
  49. package/skills/developpeur/SKILL.md +60 -50
  50. package/skills/explorateur/SKILL.md +90 -64
  51. package/skills/grill/SKILL.md +1 -1
  52. package/skills/mesure/SKILL.md +2 -1
  53. package/skills/pitmaster/SKILL.md +132 -118
  54. package/skills/pitmaster/references/consigne-agent.md +54 -148
  55. package/skills/questionneur/SKILL.md +18 -18
  56. package/skills/relecteur/SKILL.md +69 -23
  57. package/skills/testwriter/SKILL.md +46 -35
  58. package/types/index.d.ts +1 -3
  59. package/agents/referent-technique.md +0 -9
  60. package/agents/regulateur.md +0 -11
  61. package/scripts/enveloppe/composant.mjs +0 -11
  62. package/scripts/etat-du-circuit.mjs +0 -104
  63. package/skills/referent-technique/SKILL.md +0 -145
  64. package/skills/regulateur/SKILL.md +0 -220
@@ -14,8 +14,8 @@
14
14
  // --ticket is a usage error, named. In order, each step refusing stops the run:
15
15
  // 1. the verdict: the ticket has `ACCEPTÉ` as its last reviewer verdict (`ACCEPTÉ` or
16
16
  // `RENDU` at the start of a comment of the author `relecteur`; another author's is none) and, when labelled `arbitrage`, each arbitrage
17
- // (`estArbitrage`: a « ## Arbitrage » comment, or the regulator's the circuit writes) is settled
18
- // (`arbitrageTranche`: the regulator's, `### Verdict — tranché`, or a « ## Réponse du
17
+ // (`estArbitrage`: a « ## Arbitrage » comment, the supervisor's) is settled
18
+ // (`arbitrageTranche`: `### Verdict — tranché`, or a « ## Réponse du
19
19
  // responsable » comment after it);
20
20
  // 2. the lots: the tests lot touches only what the testwriter writes (`isTesterFile`: test files
21
21
  // and the shared test material), the code lot none of it;
@@ -45,10 +45,10 @@
45
45
  // `faute-mal-formee`, `faute-hors-catalogue`, `faute-non-nommee`, `faute-non-produite`,
46
46
  // `faute-non-listee`), or by the declaration file and TSDoc's message id for a TSDoc message.
47
47
  //
48
- // In the envelope of a component (`GRILLHOUSE_ENVELOPPE`, scripts/enveloppe/composant.mjs), the
49
- // check and `npm run api` read and write that component alone: its neighbours' source is hidden,
50
- // their published parts read-only, and the integration checks every component on the whole
51
- // repository. The component's build comes first, as outside (`npm test -- <its files>` builds it).
48
+ // In the envelope of an agent (`GRILLHOUSE_ENVELOPPE`, scripts/enveloppe/paquets-du-plan.mjs), the
49
+ // check and `npm run api` judge and write the components of its plan alone: the others are
50
+ // read-only there, and the integration checks every component on the whole repository. The
51
+ // component's build comes first, as outside (`npm test -- <its files>` builds it).
52
52
  //
53
53
  // A component without `docs/INTERFACE.md` is reported and skipped. A project that prefers a
54
54
  // bench per component calls `verifierComposant(racine, composant)` from its test.
@@ -58,7 +58,7 @@ import { tmpdir } from 'node:os';
58
58
  import path from 'node:path';
59
59
  import { pathToFileURL } from 'node:url';
60
60
  import { composants, lireSiPresent, parcourir } from './consommateurs.mjs';
61
- import { composantDeLEnveloppe } from './enveloppe/composant.mjs';
61
+ import { paquetsDeLEnveloppe } from './enveloppe/paquets-du-plan.mjs';
62
62
  import { trancher } from './dette.mjs';
63
63
 
64
64
  const require = createRequire(import.meta.url);
@@ -428,7 +428,10 @@ function avecPartie(m, texte, partie) {
428
428
  // the debt (`dette.mjs`) judges, keyed by the component's interface (by the declaration file for a
429
429
  // TSDoc message, its rule TSDoc's message id); a refusal that a command or a build repairs (a
430
430
  // generated part, an API report, a built entry, the marks, the TSDoc configuration) has no key and
431
- // is never inscribed.
431
+ // is never inscribed. The refusals `npm run api` lifts carry their rule without a file, never
432
+ // inscribed either: `rapport-api` (the API report absent or stale) and `partie-generee` (the
433
+ // generated part of the interface), by which the circuit regenerates them at the final pass
434
+ // (scripts/circuit.mjs, the references of the frame).
432
435
  export function verifierComposant(racine, c, { ecrire = false } = {}) {
433
436
  const refus = [];
434
437
  const signaux = [];
@@ -442,6 +445,8 @@ export function verifierComposant(racine, c, { ecrire = false } = {}) {
442
445
  ? { fichier, ...(ligne ? { ligne } : {}), regle, message: `${rel} : ${texte}` }
443
446
  : { message: `${rel} : ${texte}` }
444
447
  );
448
+ // A refusal `npm run api` lifts: its rule, no file.
449
+ const aRegenerer = (texte, regle) => refus.push({ regle, message: `${rel} : ${texte}` });
445
450
  if (!existsSync(iface)) {
446
451
  signaux.push(`${rel} n'a pas d'interface (docs/INTERFACE.md) : rien n'est vérifié.`);
447
452
  return { refus, signaux };
@@ -474,12 +479,14 @@ export function verifierComposant(racine, c, { ecrire = false } = {}) {
474
479
  }
475
480
  const manque = !ecrire && !existsSync(path.join(c.dir, 'docs', 'INTERFACE.api.md'));
476
481
  if (manque) {
477
- refuser(
478
- "son rapport docs/INTERFACE.api.md manque ; npm run api l'écrit, après la construction."
482
+ aRegenerer(
483
+ "son rapport docs/INTERFACE.api.md manque ; npm run api l'écrit, après la construction.",
484
+ 'rapport-api'
479
485
  );
480
486
  } else if (!api.ajour && !ecrire) {
481
- refuser(
482
- "l'API a changé sans son rapport docs/INTERFACE.api.md ; npm run api le régénère, après la construction."
487
+ aRegenerer(
488
+ "l'API a changé sans son rapport docs/INTERFACE.api.md ; npm run api le régénère, après la construction.",
489
+ 'rapport-api'
483
490
  );
484
491
  }
485
492
  const nommees = new Set();
@@ -497,8 +504,9 @@ export function verifierComposant(racine, c, { ecrire = false } = {}) {
497
504
  if (ecrire) {
498
505
  if (attendu !== texte) writeFileSync(iface, attendu);
499
506
  } else if (attendu !== texte) {
500
- refuser(
501
- `la part générée de docs/INTERFACE.md (entre ${DEBUT} et ${FIN}) n'est pas celle du code ; npm run api la refait.`
507
+ aRegenerer(
508
+ `la part générée de docs/INTERFACE.md (entre ${DEBUT} et ${FIN}) n'est pas celle du code ; npm run api la refait.`,
509
+ 'partie-generee'
502
510
  );
503
511
  }
504
512
  }
@@ -522,13 +530,13 @@ function codesContreCatalogue(c, racine, fautes, refuser) {
522
530
  }
523
531
  }
524
532
 
525
- // The components a run reads: every one of <racine>; in the envelope of a component, that one
526
- // alone, its neighbours read by their published form and never written.
533
+ // The components a run reads: every one of <racine>; in an envelope, those its plan names, the
534
+ // others never written; every one when the plan names none of them (a folder of the root, `.`).
527
535
  export function aVerifier(racine, env = process.env) {
528
536
  const tous = composants(racine);
529
- const propre = composantDeLEnveloppe(env);
530
- const sien = propre && tous.filter(c => path.resolve(c.dir) === path.resolve(racine, propre));
531
- return sien?.length ? sien : tous;
537
+ const plan = (paquetsDeLEnveloppe(env) ?? []).map(p => path.resolve(racine, p));
538
+ const siens = tous.filter(c => plan.includes(path.resolve(c.dir)));
539
+ return siens.length ? siens : tous;
532
540
  }
533
541
 
534
542
  if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
@@ -2,22 +2,24 @@
2
2
  // (and `/mesure`), in place of the workspace package `@grillhouse/vitest-config` that the frame's
3
3
  // copied files held.
4
4
  //
5
- // - Each source file of the project (`.ts`, `.mts`, `.cts`, `.js`, `.mjs`, `.cjs`; node_modules,
6
- // .git, dist, coverage, .turbo, .claude, .beads, .codegraph and .rtfm left out) that names
5
+ // - Each source file of the project (scripts/sources.mjs) that names
7
6
  // `@grillhouse/vitest-config` or `@grillhouse/vitest-config/mesure` in quotes names the package's
8
7
  // instead; a root configuration that imports the base by its former path
9
8
  // (`./scripts/vitest-config/base.mjs`, `./packages/grillhouse/scripts/vitest-config/base.mjs`)
10
- // imports it by its name.
9
+ // imports it by its name. `formesAnterieures` gives back what a text so rewritten held before,
10
+ // by which the migration of the copies (0.23.2-copies-du-cadre.mjs) still knows a former copy of
11
+ // the frame by its fingerprint.
11
12
  // - Each package.json of the project (the root's and each workspace's) that declares
12
13
  // `@grillhouse/vitest-config` declares `@roomi-fields/grillhouse` in the same field in its place,
13
14
  // at the range the root declares (else `^<version>` of the package), unless it already declares
14
15
  // it; the root's workspaces lose the former directory of the package (`scripts/vitest-config`,
15
16
  // `packages/grillhouse/scripts/vitest-config`).
16
17
  // - A project already in the new form is not written.
17
- import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
18
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
18
19
  import path from 'node:path';
19
20
  import { VERSION } from '../cadre.mjs';
20
21
  import { paquets } from '../paquets.mjs';
22
+ import { sources } from '../sources.mjs';
21
23
 
22
24
  export const version = '0.23.0';
23
25
  export const nom = 'configuration de base de Vitest';
@@ -27,33 +29,34 @@ const PAQUET = '@roomi-fields/grillhouse';
27
29
  const NOUVEAU = `${PAQUET}/vitest-config`;
28
30
  const ANCIENS_ESPACES = ['scripts/vitest-config', 'packages/grillhouse/scripts/vitest-config'];
29
31
  const CHAMPS = ['dependencies', 'devDependencies', 'optionalDependencies', 'peerDependencies'];
30
- const EXTENSIONS = /\.(?:[mc]?ts|[mc]?js)$/;
31
- const HORS = new Set([
32
- 'node_modules',
33
- '.git',
34
- 'dist',
35
- 'coverage',
36
- '.turbo',
37
- '.claude',
38
- '.beads',
39
- '.codegraph',
40
- '.rtfm',
41
- ]);
42
32
  // A quoted name of the former package, or a former path of the base at the root.
43
33
  const NOMME =
44
34
  /(['"])(?:@grillhouse\/vitest-config(\/mesure)?|\.\/(?:packages\/grillhouse\/)?scripts\/vitest-config\/base\.mjs)\1/g;
45
35
 
46
- function sources(racine, rel = '') {
47
- return readdirSync(path.join(racine, rel), { withFileTypes: true }).flatMap(e => {
48
- const r = rel === '' ? e.name : `${rel}/${e.name}`;
49
- if (e.isDirectory()) return HORS.has(e.name) ? [] : sources(racine, r);
50
- return e.isFile() && EXTENSIONS.test(e.name) ? [r] : [];
51
- });
52
- }
36
+ // A quoted name of the package's base, as this migration writes it.
37
+ const ECRIT = /(['"])@roomi-fields\/grillhouse\/vitest-config(\/mesure)?\1/g;
38
+ const ANCIENS_CHEMINS = [
39
+ './scripts/vitest-config/base.mjs',
40
+ './packages/grillhouse/scripts/vitest-config/base.mjs',
41
+ ];
53
42
 
54
43
  const lireJson = f => JSON.parse(readFileSync(f, 'utf8'));
55
44
  const ecrireJson = (f, v) => writeFileSync(f, `${JSON.stringify(v, null, 2)}\n`);
56
45
 
46
+ /**
47
+ * The texts <texte> may have held before this migration rewrote it: each name it wrote back to the
48
+ * former package's, or each name of the base back to one former path; none when it wrote nothing.
49
+ */
50
+ export function formesAnterieures(texte) {
51
+ if (texte.search(ECRIT) < 0) return [];
52
+ return [
53
+ texte.replace(ECRIT, (_, q, mesure) => `${q}${ANCIEN}${mesure ?? ''}${q}`),
54
+ ...ANCIENS_CHEMINS.map(c =>
55
+ texte.replace(ECRIT, (tout, q, mesure) => (mesure ? tout : `${q}${c}${q}`))
56
+ ),
57
+ ];
58
+ }
59
+
57
60
  /** Brings the project to the package's base configuration of Vitest; returns what it did. */
58
61
  export function migrer(projet) {
59
62
  const lignes = [];
@@ -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,231 @@
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>`, `path.join(…, 'scripts', '<script>')`;
25
+ // a tracked path that is no regular file, a submodule or a link, is skipped),
26
+ // where the project holds no such file any more, is named with its file:line and what replaces
27
+ // it: the subcommand (`SOUS_COMMANDES` of scripts/sous-commandes.mjs), the hook
28
+ // (`grillhouse hook <nom>`, `CROCHETS` of scripts/crochets.mjs) or the named entry (the
29
+ // `exports` of the package). The project's scripts are its own: none is rewritten, and the
30
+ // lines are a report, which never holds back the version the project records, as a codemod
31
+ // reports what it leaves without failing.
32
+ // - No version of the update wrote a setting of a role in `.claude/settings.json` or under
33
+ // `grillhouse` of the package.json (the update of 0.22.x wrote the hooks, the status line and the
34
+ // plugins; the rights of a role are given on the command line of its session, scripts/droits.mjs):
35
+ // the migration writes neither.
36
+ // - A project already in the new form is not written, and a second pass reports only what it left
37
+ // to the project.
38
+ import { execFileSync } from 'node:child_process';
39
+ import { createHash } from 'node:crypto';
40
+ import { existsSync, lstatSync, readFileSync, writeFileSync } from 'node:fs';
41
+ import path from 'node:path';
42
+ import { CADRE, PAQUET } from '../cadre.mjs';
43
+ import { CROCHETS } from '../crochets.mjs';
44
+ import { SOUS_COMMANDES } from '../sous-commandes.mjs';
45
+
46
+ export const version = '0.24.0';
47
+ export const nom = 'circuit simplifié';
48
+
49
+ const SUPPRIMES = ['regulateur', 'referent-technique'];
50
+
51
+ /** Whether <f>, a path from a project's root, is in a copy of a removed role, which this migration names. */
52
+ export const deRoleSupprime = f =>
53
+ SUPPRIMES.some(r => f === `.claude/agents/${r}.md` || f.startsWith(`.claude/skills/${r}/`));
54
+ // A removed role named in a text, in its words or by its name.
55
+ const NOMME_UN_SUPPRIME = /r[ée]gulat(?:eur|rice)|referent-technique|r[ée]f[ée]rents? techniques?/i;
56
+
57
+ const RUBRIQUE = 'Ce qui monte au superviseur';
58
+ // The section of the rubric, its title line apart, up to the next section or the end.
59
+ const SECTION = new RegExp(`^(## ${RUBRIQUE}[ \\t]*\\n)([^]*?)(?=^## |(?![^]))`, 'm');
60
+ // The paragraph of the former template, its list captured, as a paragraph of its own.
61
+ const ANCIEN =
62
+ /(^\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?$)/;
63
+ const NEUF = liste =>
64
+ `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).`;
65
+ const COLONNES = 100;
66
+
67
+ const SUIVI = 'docs/agents/issue-tracker.md';
68
+ // The SHA-256 of the guide of the issue tracker as the template held it: 0.21.0 to 0.22.6 (the
69
+ // copy the update placed), 0.23.0 and 0.23.1 (the template `grillhouse init` places).
70
+ const FORMES = new Set([
71
+ '2b7c70b22001c83009887372f2bc5b04509e8e4dbc14f1e647df2c686b0ba5e0',
72
+ 'a6d6e9ecbb75b82dad9c6adadd310fe1047c4a5f83d4d789718cd921eda6b258',
73
+ ]);
74
+
75
+ const hacher = octets => createHash('sha256').update(octets).digest('hex');
76
+
77
+ // The words of <texte> in lines of at most COLONNES characters.
78
+ function replier(texte) {
79
+ const lignes = [];
80
+ for (const mot of texte.split(/\s+/).filter(Boolean)) {
81
+ const l = lignes.length - 1;
82
+ if (l >= 0 && lignes[l].length + 1 + mot.length <= COLONNES) {
83
+ lignes[l] += ` ${mot}`;
84
+ } else {
85
+ lignes.push(mot);
86
+ }
87
+ }
88
+ return lignes.join('\n');
89
+ }
90
+
91
+ // The rubric of the CLAUDE.md of <projet>.
92
+ function rubrique(projet) {
93
+ const fichier = path.join(projet, 'CLAUDE.md');
94
+ if (!existsSync(fichier)) {
95
+ return [];
96
+ }
97
+ const texte = readFileSync(fichier, 'utf8');
98
+ const s = SECTION.exec(texte);
99
+ if (!s) {
100
+ return [];
101
+ }
102
+ const [tout, titre, corps] = s;
103
+ if (ANCIEN.test(corps)) {
104
+ const neuf = corps.replace(ANCIEN, (_, avant, liste) => `${avant}${replier(NEUF(liste))}`);
105
+ writeFileSync(
106
+ fichier,
107
+ texte.replace(tout, () => `${titre}${neuf}`)
108
+ );
109
+ return [
110
+ `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`,
111
+ ];
112
+ }
113
+ if (!NOMME_UN_SUPPRIME.test(corps)) {
114
+ return [];
115
+ }
116
+ return [
117
+ {
118
+ 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`,
119
+ },
120
+ ];
121
+ }
122
+
123
+ // The guide of the issue tracker of <projet>.
124
+ function suivi(projet) {
125
+ const fichier = path.join(projet, SUIVI);
126
+ if (!existsSync(fichier)) {
127
+ return [];
128
+ }
129
+ const octets = readFileSync(fichier);
130
+ if (FORMES.has(hacher(octets))) {
131
+ writeFileSync(fichier, readFileSync(path.join(CADRE, 'templates/projet', SUIVI)));
132
+ return [`${SUIVI} : le guide du gabarit, qui dit le circuit de la 0.24.0`];
133
+ }
134
+ if (!NOMME_UN_SUPPRIME.test(octets.toString('utf8'))) {
135
+ return [];
136
+ }
137
+ return [
138
+ {
139
+ 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`,
140
+ },
141
+ ];
142
+ }
143
+
144
+ // The copies of the removed roles left in <projet>.
145
+ function copies(projet) {
146
+ return SUPPRIMES.flatMap(r => [
147
+ { f: `.claude/agents/${r}.md`, rm: 'git rm' },
148
+ { f: `.claude/skills/${r}`, rm: 'git rm -r' },
149
+ ])
150
+ .filter(({ f }) => existsSync(path.join(projet, f)))
151
+ .map(({ f, rm }) => ({
152
+ 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`,
153
+ }));
154
+ }
155
+
156
+ // What replaces each script of the frame, by its path under `scripts/`: its subcommand, its hook,
157
+ // its named entry.
158
+ function remplacants() {
159
+ const r = new Map();
160
+ const ajouter = (script, quoi) => r.set(script, [...(r.get(script) ?? []), quoi]);
161
+ for (const [n, s] of Object.entries(SOUS_COMMANDES)) {
162
+ ajouter(s, `\`grillhouse ${n}\``);
163
+ }
164
+ for (const [n, c] of Object.entries(CROCHETS)) {
165
+ ajouter(c.garde, `\`grillhouse hook ${n}\``);
166
+ }
167
+ const { exports = {} } = JSON.parse(readFileSync(path.join(PAQUET, 'package.json'), 'utf8'));
168
+ for (const [entree, cible] of Object.entries(exports)) {
169
+ const d = typeof cible === 'string' ? cible : cible?.default;
170
+ if (typeof d === 'string' && d.startsWith('./scripts/')) {
171
+ ajouter(d.slice('./scripts/'.length), `\`@roomi-fields/grillhouse${entree.slice(1)}\``);
172
+ }
173
+ }
174
+ return r;
175
+ }
176
+ // A script named at its former address: `scripts/<chemin>`, alone or after `./` or `$VAR/`.
177
+ const ADRESSE = /(?<![\w./-])(?:\.\/|\$\{?\w+\}?\/)?scripts\/([\w./-]+?\.(?:mjs|sh))(?![\w.-])/g;
178
+ // The same, as the segments of a path.join: `'scripts', 'dette.mjs'`.
179
+ const SEGMENTS = /(['"])scripts\1((?:\s*,\s*(['"])[\w.-]+\3)+)/g;
180
+ const SANS = /(^|\/)(?:node_modules\/|CHANGELOG\.md$)/;
181
+
182
+ /** The lines of the tracked files of <projet> that still reach a script of the frame at its former address. */
183
+ export function restes(projet) {
184
+ let suivis;
185
+ try {
186
+ suivis = execFileSync('git', ['-C', projet, 'ls-files', '-z'], { encoding: 'utf8' })
187
+ .split('\0')
188
+ .filter(f => f && !SANS.test(f));
189
+ } catch {
190
+ return [];
191
+ }
192
+ const par = remplacants();
193
+ const sortie = [];
194
+ for (const f of suivis) {
195
+ const absolu = path.join(projet, f);
196
+ // A submodule, a link, a file gone from the disk: no text of the project to read.
197
+ if (!existsSync(absolu) || !lstatSync(absolu).isFile()) {
198
+ continue;
199
+ }
200
+ const texte = readFileSync(absolu, 'utf8');
201
+ if (texte.includes('\0')) {
202
+ continue;
203
+ }
204
+ texte.split('\n').forEach((ligne, i) => {
205
+ const scripts = [
206
+ ...[...ligne.matchAll(ADRESSE)].map(m => m[1]),
207
+ ...[...ligne.matchAll(SEGMENTS)].map(m =>
208
+ m[2]
209
+ .split(',')
210
+ .map(s => s.trim().slice(1, -1))
211
+ .filter(Boolean)
212
+ .join('/')
213
+ ),
214
+ ];
215
+ for (const s of new Set(scripts)) {
216
+ if (!par.has(s) || existsSync(path.join(projet, 'scripts', s))) {
217
+ continue;
218
+ }
219
+ sortie.push(
220
+ `${f}:${i + 1} : appelle scripts/${s}, copie du cadre retirée ; ${par.get(s).join(' ou ')} la remplace`
221
+ );
222
+ }
223
+ });
224
+ }
225
+ return sortie;
226
+ }
227
+
228
+ /** Rewrites in the project at <projet> what the circuit of 0.24.0 changes; returns what it did. */
229
+ export function migrer(projet) {
230
+ return [...rubrique(projet), ...suivi(projet), ...copies(projet)];
231
+ }