@roomi-fields/grillhouse 0.0.0-stage → 0.23.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 (166) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/METACADRE.md +41 -0
  3. package/agents/developpeur.md +10 -0
  4. package/agents/explorateur.md +9 -0
  5. package/agents/questionneur.md +11 -0
  6. package/agents/referent-technique.md +9 -0
  7. package/agents/regulateur.md +11 -0
  8. package/agents/relecteur.md +9 -0
  9. package/agents/testwriter.md +10 -0
  10. package/bin/grillhouse.mjs +125 -0
  11. package/cadre/templates/projet/.claude/settings.json +30 -0
  12. package/cadre/templates/projet/.editorconfig +47 -0
  13. package/cadre/templates/projet/.env.example +57 -0
  14. package/cadre/templates/projet/.eslintrc.cjs +56 -0
  15. package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
  16. package/cadre/templates/projet/.mcp.json +13 -0
  17. package/cadre/templates/projet/.prettierrc +11 -0
  18. package/cadre/templates/projet/CHANGELOG.md +61 -0
  19. package/cadre/templates/projet/CLAUDE.md +46 -0
  20. package/cadre/templates/projet/CONTRIBUTING.md +243 -0
  21. package/cadre/templates/projet/CREDITS.md +41 -0
  22. package/cadre/templates/projet/README.md +152 -0
  23. package/cadre/templates/projet/_gitignore +91 -0
  24. package/cadre/templates/projet/_npmignore +46 -0
  25. package/cadre/templates/projet/deployment/README.md +98 -0
  26. package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
  27. package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
  28. package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
  29. package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
  30. package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
  31. package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
  32. package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
  33. package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
  34. package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
  35. package/cadre/templates/projet/ecosystem.config.cjs +73 -0
  36. package/cadre/templates/projet/package.json +90 -0
  37. package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
  38. package/cadre/templates/projet/src/config.ts +68 -0
  39. package/cadre/templates/projet/src/errors.ts +102 -0
  40. package/cadre/templates/projet/src/index.ts +46 -0
  41. package/cadre/templates/projet/src/services/.gitkeep +8 -0
  42. package/cadre/templates/projet/src/types/index.ts +24 -0
  43. package/cadre/templates/projet/src/utils/logger.ts +106 -0
  44. package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
  45. package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
  46. package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
  47. package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
  48. package/cadre/templates/projet/tsconfig.eslint.json +9 -0
  49. package/cadre/templates/projet/tsconfig.json +41 -0
  50. package/cadre/templates/projet/tsdoc.json +6 -0
  51. package/cadre/templates/projet/vitest.config.ts +44 -0
  52. package/cadre/templates/turbo/paquets.json +29 -0
  53. package/cadre/templates/turbo/sans-paquets.json +28 -0
  54. package/charte.md +107 -0
  55. package/docs/ARCHITECTURE.md +90 -0
  56. package/hooks/crochet.mjs +33 -0
  57. package/hooks/hooks.json +58 -0
  58. package/hooks/register.tsx +558 -0
  59. package/package.json +75 -4
  60. package/scripts/avancement.mjs +244 -0
  61. package/scripts/bilan.mjs +523 -0
  62. package/scripts/branche-de-nuit.mjs +59 -0
  63. package/scripts/cadre.mjs +109 -0
  64. package/scripts/circuit.mjs +4375 -0
  65. package/scripts/code-mort.mjs +371 -0
  66. package/scripts/composants-affectes.mjs +598 -0
  67. package/scripts/consommateurs-arbitres.mjs +192 -0
  68. package/scripts/consommateurs.mjs +310 -0
  69. package/scripts/crochet.mjs +24 -0
  70. package/scripts/crochets.mjs +135 -0
  71. package/scripts/dernier-vert.mjs +208 -0
  72. package/scripts/dette.mjs +181 -0
  73. package/scripts/droits.mjs +354 -0
  74. package/scripts/enveloppe/codes.mjs +17 -0
  75. package/scripts/enveloppe/composant.mjs +11 -0
  76. package/scripts/enveloppe/enveloppe.mjs +628 -0
  77. package/scripts/enveloppe/lancer.sh +311 -0
  78. package/scripts/etapes.mjs +323 -0
  79. package/scripts/etat-du-circuit.mjs +104 -0
  80. package/scripts/frontieres.mjs +456 -0
  81. package/scripts/gardes-du-cadre.mjs +72 -0
  82. package/scripts/greffons.mjs +338 -0
  83. package/scripts/grillhouse-maj.mjs +514 -0
  84. package/scripts/index-interfaces.mjs +293 -0
  85. package/scripts/init.mjs +117 -0
  86. package/scripts/integration/file-attente.mjs +61 -0
  87. package/scripts/integration/file.mjs +494 -0
  88. package/scripts/integration/integrer.mjs +381 -0
  89. package/scripts/integration/rejeu.mjs +139 -0
  90. package/scripts/integration/verdict.mjs +13 -0
  91. package/scripts/interfaces-contre-code.mjs +547 -0
  92. package/scripts/ligne-etat/defaut.sh +110 -0
  93. package/scripts/ligne-etat/ligne-etat.mjs +93 -0
  94. package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
  95. package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
  96. package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
  97. package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
  98. package/scripts/migrations.mjs +43 -0
  99. package/scripts/nuit.sh +269 -0
  100. package/scripts/paquets.mjs +79 -0
  101. package/scripts/poussee-cles.mjs +46 -0
  102. package/scripts/preparation.mjs +84 -0
  103. package/scripts/references-par-les-types.mjs +420 -0
  104. package/scripts/registre.mjs +336 -0
  105. package/scripts/resultats.mjs +580 -0
  106. package/scripts/resumes.mjs +57 -0
  107. package/scripts/retirer-copie.sh +127 -0
  108. package/scripts/session-start.sh +203 -0
  109. package/scripts/sortie.mjs +155 -0
  110. package/scripts/sous-modules.sh +316 -0
  111. package/scripts/structure-guard.sh +27 -0
  112. package/scripts/superviseur.mjs +102 -0
  113. package/scripts/tableau.mjs +1886 -0
  114. package/scripts/tests.mjs +236 -0
  115. package/scripts/ticket.sh +23 -0
  116. package/scripts/turbo-des-paquets.mjs +899 -0
  117. package/scripts/un-commit-par-ticket.mjs +212 -0
  118. package/scripts/verifier.mjs +469 -0
  119. package/scripts/verrou-de-fichier.mjs +130 -0
  120. package/scripts/verrou-de-machine.mjs +211 -0
  121. package/scripts/verrous/commande.mjs +180 -0
  122. package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
  123. package/scripts/verrous/verrou.mjs +165 -0
  124. package/scripts/vitest-config/avancement.mjs +39 -0
  125. package/scripts/vitest-config/avancement.reporter.mjs +56 -0
  126. package/scripts/vitest-config/base-de-passe.mjs +128 -0
  127. package/scripts/vitest-config/base.d.ts +6 -0
  128. package/scripts/vitest-config/base.mjs +87 -0
  129. package/scripts/vitest-config/changements.mjs +173 -0
  130. package/scripts/vitest-config/cibles.mjs +207 -0
  131. package/scripts/vitest-config/duree.reporter.mjs +98 -0
  132. package/scripts/vitest-config/machine.setup.mjs +14 -0
  133. package/scripts/vitest-config/mesure.d.ts +6 -0
  134. package/scripts/vitest-config/mesure.mjs +13 -0
  135. package/scripts/vitest-config/mesure.setup.mjs +12 -0
  136. package/scripts/vitest-config/pression.mjs +90 -0
  137. package/scripts/vitest-config/resultats.mjs +23 -0
  138. package/scripts/vitest-config/resultats.setup.mjs +19 -0
  139. package/scripts/vitest.nuit.config.mjs +26 -0
  140. package/skills/developpeur/SKILL.md +175 -0
  141. package/skills/explorateur/SKILL.md +84 -0
  142. package/skills/grill/SKILL.md +238 -0
  143. package/skills/grill/references/releve-structurel.md +38 -0
  144. package/skills/mesure/SKILL.md +168 -0
  145. package/skills/pitmaster/SKILL.md +382 -0
  146. package/skills/pitmaster/references/cadre.md +119 -0
  147. package/skills/pitmaster/references/consigne-agent.md +308 -0
  148. package/skills/pitmaster/references/erreurs.md +25 -0
  149. package/skills/pitmaster/references/tour.md +124 -0
  150. package/skills/questionneur/SKILL.md +64 -0
  151. package/skills/redacteur/SKILL.md +84 -0
  152. package/skills/redacteur/references/documents-d-un-composant.md +105 -0
  153. package/skills/referent-technique/SKILL.md +145 -0
  154. package/skills/regulateur/SKILL.md +220 -0
  155. package/skills/release/SKILL.md +134 -0
  156. package/skills/release/scripts/update-version.cjs +92 -0
  157. package/skills/relecteur/SKILL.md +152 -0
  158. package/skills/relecteur/references/LICENSE-MIT +30 -0
  159. package/skills/relecteur/references/SOURCE.md +8 -0
  160. package/skills/relecteur/references/claims-check.md +14 -0
  161. package/skills/relecteur/references/deletion-check.md +14 -0
  162. package/skills/relecteur/references/edge-case-hunter.md +80 -0
  163. package/skills/relecteur/references/verification-gap.md +113 -0
  164. package/skills/testwriter/SKILL.md +187 -0
  165. package/types/index.d.ts +56 -0
  166. package/README.md +0 -3
@@ -0,0 +1,899 @@
1
+ #!/usr/bin/env node
2
+ // The `turbo.json` of each package of a repository of packages, generated from its package.json
3
+ // (its dependencies, its build, the paths its tests read and the traces it writes), its
4
+ // configurations of TypeScript, the root's turbo.json and the list of the packages out of the
5
+ // frame; and, in the root's turbo.json, the `dependsOn` of the guard tasks, the `env` of the tests,
6
+ // the variable of the results directory and the exclusion of a submodule's `.git`.
7
+ //
8
+ // npx grillhouse turbo-des-paquets refuses a turbo.json absent or stale, and a
9
+ // declaration of a path read or trace refused (below)
10
+ // npx grillhouse turbo-des-paquets --ecrire writes the turbo.json of each package, and the
11
+ // guards' dependsOn and the tests' env in the root's
12
+ //
13
+ // Turborepo's `^build` waits for the build of every internal package a package declares, its
14
+ // devDependencies included; a package whose tests import a neighbour that depends on it (the
15
+ // shared test material, two stages testing each other) makes a cycle that `turbo run build`
16
+ // refuses. A build waits for its runtime dependencies only, as Bazel separates the deps of a
17
+ // library from those of its test. So each package receives a package configuration
18
+ // (https://turborepo.dev/docs/reference/package-configurations): `extends: ["//"]`, and a `build`
19
+ // whose `dependsOn`, which replaces the root's, holds the `<name>#build` of each internal package
20
+ // its `dependencies`, `peerDependencies` and `optionalDependencies` name that has a `build`
21
+ // script, sorted. The tests keep the root's `build` and `^build`: a test waits for its own package's build, whose
22
+ // built entry it imports, and for everything it imports.
23
+ //
24
+ // A build hashes what it reads, as Turborepo's `inputs` limit what invalidates a task
25
+ // (https://turborepo.dev/docs/reference/configuration#inputs): a test file changed leaves its
26
+ // package's build as it is, and so the builds and tests of every package depending on it. A
27
+ // package with a `tsconfig.build.json` has its `build` hash the root's build inputs, Turborepo's
28
+ // default (`$TURBO_DEFAULT$`) left out, then, sorted: its package.json, its tsconfig.build.json and
29
+ // each configuration it extends inside the package, `<dir>/**` per directory it includes (an
30
+ // `include` entry, its fixed part before a glob; a `files` entry, its directory, since tsc compiles
31
+ // what a listed file imports, its neighbours most often), the same for each configuration a `tsc`
32
+ // of its `build` script compiles (`-p`, `--project`, `-b`, `--build`, else tsconfig.json). tsc's
33
+ // own rules hold: a configuration's `include`
34
+ // and `files` replace its bases', a later base of an `extends` list replacing an earlier one, each
35
+ // path relative to the file that writes it (`${configDir}`, to the configuration compiled). What is
36
+ // unknown leaves the build on Turborepo's default, every file of its package hashed, so that it
37
+ // never gives a stale cache: a tsconfig.build.json absent or not JSON, a command of the `build`
38
+ // script other than `tsc …` (`vite build`, `node copier.mjs`), a base that cannot be read, a
39
+ // configuration setting
40
+ // neither `include` nor `files` (tsc then takes every file), an included path out of the package
41
+ // or at its root.
42
+ //
43
+ // A package out of the frame (`docs/agents/hors-cadre.txt`: a directory under `packages/` or
44
+ // `src/`) stays a workspace, which a neighbour may import by its name, and Turborepo plays none
45
+ // of its tasks: its configuration excludes each task of the root's turbo.json (or turbo.jsonc),
46
+ // `"extends": false`, by which the task does not exist for it. A `^` dependency still reaches an excluded task, so no `dependsOn`
47
+ // written here names a package out of the frame, and a package of the frame that declares one
48
+ // (in its `dependencies`, `devDependencies` or `optionalDependencies`, the fields `^` follows)
49
+ // has each root task whose `dependsOn` holds a `^<task>` written out: its other entries kept, then
50
+ // the `<name>#<task>` of each internal package it declares there, out of the frame left out,
51
+ // sorted. The check refuses a configuration that is not so, which Turborepo would play.
52
+ //
53
+ // What a package's tests read, as Bazel's `data`, and the traces a stage writes, as Bazel's
54
+ // genrule: the test task of a package depends on its code, on what it imports and on the paths it
55
+ // declares only. A package declares in its package.json, each path from the root:
56
+ //
57
+ // "grillhouse": {
58
+ // "lit": ["packages/990-corpus/test/060-encoder/paires-v1"],
59
+ // "traces": { "trace:grammaires": { "lit": ["…/paires-v1"], "ecrit": "packages/060-encoder/trace/grammaires-du-corpus" } }
60
+ // }
61
+ //
62
+ // - `lit`: the paths its tests read, directories or files, data or code, a neighbour's scripts and
63
+ // sources included, as Bazel's `data` takes any file. Its `test:vitest` hashes them
64
+ // (`inputs`: the root's, `$TURBO_EXTENDS$`, or Turborepo's default, `$TURBO_DEFAULT$`, when the
65
+ // root's task sets none, since `$TURBO_EXTENDS$` then extends an empty list; then
66
+ // `$TURBO_ROOT$/<dir>/**` each) and waits, beside the root's `dependsOn`, for each trace whose
67
+ // output one of them overlaps (`<producer>#trace:<name>`); the producer declares nothing for it.
68
+ // - `traces`: each trace the package produces, its command the package's script of the same name,
69
+ // `trace:<name>`. Its task waits for `build` and `^build`, and for the traces its `lit` overlaps;
70
+ // it hashes the package's files and its `lit`, its output left out, and Turborepo caches its
71
+ // output, `ecrit`, a directory inside the package that git ignores. A restore from the cache
72
+ // adds files and removes none: the command empties its directory before writing it.
73
+ // The traces a package's tests wait for are read in Turborepo's plan of its tests
74
+ // (`tracesDuPlan`), by the targeted run (scripts/tests.mjs) and the launcher
75
+ // (scripts/enveloppe/lancer.sh), which produce them before the tests; the envelope shows the
76
+ // directory of a neighbour's trace read-only (scripts/enveloppe/enveloppe.mjs), and the targeted
77
+ // run inside it plays the traces of the agent's package only.
78
+ // - The package holding a declared path of data alone (the shared test material) has its `build`
79
+ // leave it out (`inputs`: the root's, then `!<dir>/**` each): its data no longer moves its build,
80
+ // nor the tests of every package that waits for it. A declared path holding code (a file a build
81
+ // compiles: `.ts`, `.tsx`, `.mts`, `.cts`, `.d.ts` included, whose types tsc checks, `.js`,
82
+ // `.jsx`, `.mjs`, `.cjs`) stays an input of its package's build, as Bazel's `data` leaves a file
83
+ // in the rule that builds it; what a trace writes is no code of it. The builds a package's
84
+ // dependencies give stay as they are: a package reduces them in its package.json.
85
+ //
86
+ // The check refuses a declared path absent that no trace writes; a trace written out of its
87
+ // package, followed by git, or not ignored; a `trace:*` script without
88
+ // declaration, a declaration without script or not named `trace:<name>`. In the root's
89
+ // turbo.json, which is the project's, it refuses what the generator owns: a task of one package
90
+ // (`<package>#<task>`, the root's own `//#…` kept), a `trace:*` task, an input (`inputs` under
91
+ // `$TURBO_ROOT$/`, `globalDependencies`) naming a package directory.
92
+ //
93
+ // A submodule's `.git`, in a copy (a worktree), is a file naming that copy: an input reaching it
94
+ // changes the hash of its task in each new copy, and the cache never serves. Beside each input
95
+ // that can reach the `.git` of a submodule the .gitmodules declares (a pattern of the root under
96
+ // `$TURBO_ROOT$/`, of any task, or a global dependency, whose fixed part is the submodule or holds
97
+ // it, then a glob), the generator writes its exclusion, `!$TURBO_ROOT$/<path>/.git`
98
+ // (`!<path>/.git` for a global dependency), in the root's turbo.json, the rest of the file kept,
99
+ // and in the inputs it writes in a package's turbo.json; the check refuses a root input lacking it.
100
+ //
101
+ // The configuration of Vitest of the package of the shared test material (`matieres` of
102
+ // scripts/consommateurs.mjs) is CommonJS (`vitest.config.cjs`): the envelope shows that package
103
+ // read-only to every role but the testwriter (scripts/enveloppe/enveloppe.mjs), and Vite 5 loads a
104
+ // CommonJS configuration in memory, where it bundles an ESM one into a file beside it
105
+ // (`vitest.config.mjs.timestamp-<n>.mjs`). The check refuses a `vitest.config.mjs` or `.mts` of
106
+ // that package, and a `.js` or `.ts` when its `type` is `module`, as Vite tells ESM from CommonJS.
107
+ //
108
+ // The guard tasks of the root: the frame's controls (`grillhouse:gardes`) and the project's own
109
+ // guards (`gardes`, the root script the frame reserves for them; a root without it plays none),
110
+ // which `npm run verifier` plays beside the tests. They read what the builds write (the API
111
+ // reports and the code-mort check read the `dist/`; a project's guards may read its built form):
112
+ // each waits for the build of each package of the frame that has a `build` script, sorted, a
113
+ // package out of the frame left out (`//#grillhouse:gardes`, `//#gardes` in a repository of
114
+ // packages), or for the root's build (`grillhouse:gardes`, `gardes`) in a repository without
115
+ // packages. The generator writes their `dependsOn` in the root's turbo.json (or turbo.jsonc), and
116
+ // adds a guard task the root lacks (`{ dependsOn, outputs: [] }`); the rest of that file is the
117
+ // project's, its comments and its layout kept (jsonc-parser, the library of the JSON editing of
118
+ // VS Code: each edit touches the value it sets). The check refuses a guard task absent or whose
119
+ // `dependsOn` differs. A repository without a root turbo.json (or turbo.jsonc) has nothing written
120
+ // or refused at its root.
121
+ //
122
+ // The tests of a targeted run: the paths reach Vitest in the variable `GRILLHOUSE_TESTS`
123
+ // (scripts/vitest-config/cibles.mjs, scripts/tests.mjs), which the `env` of the root's
124
+ // `test:vitest` names: it enters the hash of the tests alone, and reaches their process alone
125
+ // (Turborepo's strict mode of the variables). A package's own `test:vitest` sets no `env`, and
126
+ // inherits the root's. The generator adds the variable to the `env` of the root's `test:vitest`,
127
+ // the project's entries kept, and the check refuses a `test:vitest` of the root without it.
128
+ //
129
+ // Four variables pass to every task without entering any hash: the root's `globalPassThroughEnv`
130
+ // names them, Turborepo's strict mode of the variables keeping out of a task every variable its
131
+ // configuration does not name. The results directory of a run (`GRILLHOUSE_RESULTATS`,
132
+ // scripts/resultats.mjs), where the guards write their SARIF, Turborepo its summaries and Vitest
133
+ // its JSON reports; the changes since the last green (`GRILLHOUSE_CHANGEMENTS`,
134
+ // scripts/dernier-vert.mjs), by which the tests of a package Turborepo plays again are those
135
+ // Vitest relates to them, while a package whose inputs did not move replays from the cache; the
136
+ // progress directory of a pass (`GRILLHOUSE_AVANCEMENT`, scripts/avancement.mjs), where Vitest
137
+ // writes the files done and planned of each task, which the board reads; the temporary directory
138
+ // (`TMPDIR`), the base of a pass (scripts/vitest-config/base-de-passe.mjs), where every task writes
139
+ // its temporary files and which goes at the end of the pass. The generator adds each
140
+ // one the root lacks, the project's entries kept, and the check refuses a
141
+ // root without one.
142
+ //
143
+ // The packages are those of `paquets` (scripts/paquets.mjs, the frame's one list of the packages of
144
+ // a repository, each marked out of the frame or not). A repository without `workspaces` has none:
145
+ // no package turbo.json is written or refused. The dependencies are written in the package.json
146
+ // only; this file is their only reader for the tasks' order.
147
+ import { spawnSync } from 'node:child_process';
148
+ import { existsSync, readFileSync, statSync, writeFileSync } from 'node:fs';
149
+ import { createRequire } from 'node:module';
150
+ import path from 'node:path';
151
+ import { pathToFileURL } from 'node:url';
152
+ import { applyEdits, modify, parse } from 'jsonc-parser';
153
+ import { matieres, parcourir } from './consommateurs.mjs';
154
+ import { paquets } from './paquets.mjs';
155
+ import { ecrireSarif, RESULTATS } from './resultats.mjs';
156
+ import { VARIABLE as TESTS_CIBLES } from './vitest-config/cibles.mjs';
157
+ import { AVANCEMENT } from './vitest-config/avancement.mjs';
158
+ import { CHANGEMENTS } from './vitest-config/changements.mjs';
159
+
160
+ export const TURBO = 'turbo.json';
161
+ export const COMMANDE = 'npx grillhouse turbo-des-paquets --ecrire';
162
+ // The fields of a package.json whose packages its code needs at run time.
163
+ const EXECUTION = ['dependencies', 'peerDependencies', 'optionalDependencies'];
164
+ // The fields of a package.json whose packages Turborepo's `^` follows.
165
+ const TOPOLOGIE = ['dependencies', 'devDependencies', 'optionalDependencies'];
166
+ // The root configuration of Turborepo, in the order Turborepo reads it.
167
+ const RACINES = ['turbo.json', 'turbo.jsonc'];
168
+ // The guard tasks of the root: the frame's controls, and the project's own guards.
169
+ export const GARDES = ['grillhouse:gardes', 'gardes'];
170
+ // The layout of what the generator writes in the root's turbo.json.
171
+ const MISE_EN_FORME = { formattingOptions: { insertSpaces: true, tabSize: 2, eol: '\n' } };
172
+ // The test task of a package, and the prefix of a trace task.
173
+ const TESTS = 'test:vitest';
174
+ // The temporary directory of a process, which names the base of its pass.
175
+ const TEMPORAIRE = 'TMPDIR';
176
+ // The variables the root's `globalPassThroughEnv` names, and why, as the check says it.
177
+ const PASSAGES = [RESULTATS, CHANGEMENTS, AVANCEMENT, TEMPORAIRE];
178
+ const POURQUOI = {
179
+ [RESULTATS]: "où les gardes, Turborepo et Vitest écrivent les résultats qu'un circuit lit",
180
+ [CHANGEMENTS]:
181
+ 'les fichiers changés depuis le dernier vert, auxquels Vitest relie les tests que rejoue un paquet',
182
+ [AVANCEMENT]:
183
+ "le dossier d'avancement d'une passe, où Vitest écrit ses fichiers faits et prévus que lit le tableau",
184
+ [TEMPORAIRE]:
185
+ 'la base de la passe, où chaque tâche écrit ses fichiers temporaires, effacée à sa fin',
186
+ };
187
+ const TRACE = 'trace:';
188
+ // The files a build compiles: TypeScript's sources and declarations (tsc's default `include`
189
+ // takes a `.d.ts` into the program, whose types the build checks) and JavaScript (`allowJs`).
190
+ const CODE = /\.(?:[cm]?[jt]s|[jt]sx)$/;
191
+ // The configuration of TypeScript a package's build compiles, and the token tsc reads in a path
192
+ // of `include` or `files` for the directory of the configuration it compiles.
193
+ const TSCONFIG_BUILD = 'tsconfig.build.json';
194
+ const CONFIG_DIR = '${configDir}';
195
+
196
+ // <texte> as JSON, its JSONC comments and trailing commas allowed, as Turborepo reads its
197
+ // configuration; null when it is no JSON.
198
+ const lireJsonc = texte => {
199
+ const erreurs = [];
200
+ const valeur = parse(texte, erreurs, { allowTrailingComma: true });
201
+ return erreurs.length ? null : valeur;
202
+ };
203
+
204
+ // The root configuration of Turborepo at <racine>: its path relative to <racine>, or null.
205
+ const fichierRacine = racine => RACINES.find(n => existsSync(path.join(racine, n))) ?? null;
206
+
207
+ // The root configuration of Turborepo at <racine>, parsed, with its path: { f, conf }, or null.
208
+ function confRacine(racine) {
209
+ const f = fichierRacine(racine);
210
+ const conf = f ? lireJsonc(readFileSync(path.join(racine, f), 'utf8')) : null;
211
+ return conf ? { f, conf } : null;
212
+ }
213
+
214
+ // The tasks of the root configuration of Turborepo at <racine>: Map<name, { dependsOn, inputs }>
215
+ // (`inputs` null when the task has none), the tasks of the root package (`//#…`) and of one
216
+ // package (`<name>#…`) left out; empty without one.
217
+ function tachesRacine(racine) {
218
+ const taches = new Map();
219
+ for (const [nom, t] of Object.entries(confRacine(racine)?.conf.tasks ?? {})) {
220
+ if (nom.includes('#')) continue;
221
+ taches.set(nom, {
222
+ dependsOn: Array.isArray(t?.dependsOn) ? t.dependsOn : [],
223
+ inputs: Array.isArray(t?.inputs) ? t.inputs : null,
224
+ });
225
+ }
226
+ return taches;
227
+ }
228
+
229
+ // The trace tasks of a plan of Turborepo (`turbo run … --dry=json`, parsed), its tasks that
230
+ // <garder> keeps: their task ids (`<producer>#trace:<name>`), sorted.
231
+ export const tracesDuPlan = (plan, garder = () => true) =>
232
+ plan.tasks
233
+ .filter(t => t.task.startsWith(TRACE) && garder(t))
234
+ .map(t => t.taskId)
235
+ .sort();
236
+
237
+ // A path from the root, without its trailing slashes.
238
+ const net = d => String(d ?? '').replace(/\/+$/, '');
239
+ // The fixed part of the pattern <motif>, its segments before the first holding a glob character,
240
+ // and whether it has such a segment: { fixe, glob }.
241
+ function partieFixe(motif) {
242
+ const segments = String(motif).replace(/^\.\//, '').split('/');
243
+ const i = segments.findIndex(s => /[*?[\]{}]/.test(s));
244
+ return { fixe: net((i < 0 ? segments : segments.slice(0, i)).join('/')), glob: i >= 0 };
245
+ }
246
+ // Whether <f> is <d> or under it.
247
+ const sous = (f, d) => f === d || f.startsWith(`${d}/`);
248
+ // Whether one of <a> and <b> holds the other.
249
+ const recouvre = (a, b) => sous(a, b) || sous(b, a);
250
+
251
+ // The paths of the submodules the .gitmodules of <racine> declares, sorted; none without one.
252
+ function sousModules(racine) {
253
+ if (!existsSync(path.join(racine, '.gitmodules'))) return [];
254
+ const r = gitSort(racine, [
255
+ 'config',
256
+ '-f',
257
+ '.gitmodules',
258
+ '--get-regexp',
259
+ '^submodule\\..*\\.path$',
260
+ ]);
261
+ return r.stdout
262
+ .split('\n')
263
+ .map(l => net(l.slice(l.indexOf(' ') + 1).trim()))
264
+ .filter(Boolean)
265
+ .sort();
266
+ }
267
+
268
+ // The exclusion of the `.git` of each submodule of <sm> one of the <chemins> (from the root)
269
+ // holds, each `!<prefixe><path from <depuis>>/.git`: the `.git` of a submodule set in a copy is
270
+ // a file naming that copy.
271
+ const gitsExclus = (chemins, sm, prefixe = '$TURBO_ROOT$/', depuis = '') =>
272
+ sm
273
+ .filter(m => chemins.some(c => sous(m, c)))
274
+ .map(m => `!${prefixe}${depuis ? path.posix.relative(depuis, m) : m}/.git`);
275
+
276
+ // The file of the base <base> that a configuration of TypeScript in <dir> extends, as tsc finds
277
+ // it: a path, `.json` added when it lacks it, or a module resolved from <dir>; null without one.
278
+ function baseTs(base, dir) {
279
+ if (base.startsWith('.') || path.isAbsolute(base)) {
280
+ const f = path.resolve(dir, base);
281
+ return [f, `${f}.json`].find(c => existsSync(c) && statSync(c).isFile()) ?? null;
282
+ }
283
+ const exiger = createRequire(path.join(dir, 'tsconfig.json'));
284
+ for (const c of [base, `${base}.json`, `${base}/tsconfig.json`]) {
285
+ try {
286
+ return exiger.resolve(c);
287
+ } catch {
288
+ // The next form tsc tries.
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+
294
+ // The configuration of TypeScript at <f> (absolute) with its bases, as tsc reads them:
295
+ // { configs, files, include }, `configs` the absolute path of each file read, `files` and
296
+ // `include` the entries that win, [{ chemin, dir }] (the configuration's own, else those of its
297
+ // last base that sets them, `dir` the directory of the file that writes them), undefined when no
298
+ // file sets them; null when one of them cannot be read.
299
+ function configTs(f, vus = []) {
300
+ if (vus.includes(f) || !existsSync(f)) return null;
301
+ const conf = lireJsonc(readFileSync(f, 'utf8'));
302
+ if (conf === null || typeof conf !== 'object' || Array.isArray(conf)) return null;
303
+ const dir = path.dirname(f);
304
+ const lu = { configs: [f], files: undefined, include: undefined };
305
+ for (const base of conf.extends === undefined ? [] : [conf.extends].flat()) {
306
+ const fb = typeof base === 'string' ? baseTs(base, dir) : null;
307
+ const r = fb === null ? null : configTs(fb, [...vus, f]);
308
+ if (r === null) return null;
309
+ lu.configs.push(...r.configs);
310
+ for (const cle of ['files', 'include']) if (r[cle] !== undefined) lu[cle] = r[cle];
311
+ }
312
+ for (const cle of ['files', 'include']) {
313
+ if (conf[cle] === undefined) continue;
314
+ if (!Array.isArray(conf[cle])) return null;
315
+ lu[cle] = conf[cle].map(chemin => ({ chemin: String(chemin), dir }));
316
+ }
317
+ return lu;
318
+ }
319
+
320
+ // What tsc reads in the package at <abs> compiling the configuration <projet> (absolute):
321
+ // { fichiers, dossiers }, the configurations inside the package, and the directory of each
322
+ // included path, each relative to the package; null when unknown: a configuration unread,
323
+ // neither `files` nor `include` set (tsc then takes every file), an included path out of the
324
+ // package or at its root. A listed file gives its directory: tsc compiles what it imports, its
325
+ // neighbours most often.
326
+ function luParTsc(abs, projet) {
327
+ const lu = configTs(projet);
328
+ if (lu === null || (lu.files === undefined && lu.include === undefined)) return null;
329
+ const dedans = p => {
330
+ const r = path.relative(abs, p).split(path.sep).join('/');
331
+ return r === '' || r === '..' || r.startsWith('../') || path.isAbsolute(r) ? null : r;
332
+ };
333
+ const fichiers = lu.configs.map(dedans).filter(r => r && !r.split('/').includes('node_modules'));
334
+ const dossiers = [];
335
+ for (const [cle, entrees] of [
336
+ ['files', lu.files ?? []],
337
+ ['include', lu.include ?? []],
338
+ ]) {
339
+ for (const { chemin, dir } of entrees) {
340
+ const f = chemin.startsWith(CONFIG_DIR)
341
+ ? path.join(path.dirname(projet), chemin.slice(CONFIG_DIR.length))
342
+ : path.resolve(dir, chemin);
343
+ const r = dedans(f);
344
+ if (r === null) return null;
345
+ const { fixe, glob } = partieFixe(r);
346
+ const fichier = cle === 'files' || (existsSync(f) && statSync(f).isFile());
347
+ const d = glob ? fixe : fichier ? path.posix.dirname(r) : r;
348
+ if (d === '' || d === '.') return null;
349
+ dossiers.push(d);
350
+ }
351
+ }
352
+ return { fichiers, dossiers };
353
+ }
354
+
355
+ // The inputs of the build of the package in <dossier> (from <racine>) whose package.json is
356
+ // <pkg>: what its build reads, as Turborepo's `inputs` limit what invalidates a task
357
+ // (https://turborepo.dev/docs/reference/configuration#inputs): its package.json, its
358
+ // tsconfig.build.json and each configuration it extends inside the package, `<dir>/**` per
359
+ // directory it includes, the same for each configuration a `tsc` of its `build` script compiles
360
+ // (`-p`, `--project`, `-b`, `--build`, else tsconfig.json), sorted; then the exclusion of the
361
+ // `.git` of each submodule of <sm> one of these directories holds. Null without a
362
+ // tsconfig.build.json read, when a command of the `build` script (each one apart from the next by
363
+ // `&&`, `||`, `;` or `|`) is not `tsc …` (`npx tsc …`), whose reads are unknown, or when what tsc
364
+ // reads is unknown (`luParTsc`): the build then hashes every file of its package, Turborepo's
365
+ // default, and a command unknown never gives a stale cache.
366
+ function entreesBuild(racine, dossier, pkg, sm) {
367
+ const abs = path.join(racine, dossier);
368
+ const projets = [path.join(abs, TSCONFIG_BUILD)];
369
+ if (!existsSync(projets[0])) return null;
370
+ const fichiers = new Set(['package.json']);
371
+ const versProjet = m => {
372
+ const f = path.resolve(abs, m);
373
+ return existsSync(f) && statSync(f).isDirectory() ? path.join(f, 'tsconfig.json') : f;
374
+ };
375
+ for (const commande of String(pkg.scripts?.build ?? '').split(/&&|\|\||[;|]/)) {
376
+ const mots = commande
377
+ .trim()
378
+ .split(/\s+/)
379
+ .filter(Boolean)
380
+ .map(m => m.replace(/^['"]|['"]$/g, ''));
381
+ if (mots.length === 0) continue;
382
+ const tsc = mots[0] === 'npx' ? 1 : 0;
383
+ if (mots[tsc] !== 'tsc') return null;
384
+ const options = mots.slice(tsc + 1);
385
+ const p = options.findIndex(m => m === '-p' || m === '--project');
386
+ const b = options.findIndex(m => m === '-b' || m === '--build');
387
+ if (p >= 0) {
388
+ projets.push(versProjet(options[p + 1] ?? '.'));
389
+ } else if (b >= 0) {
390
+ const suite = options.slice(b + 1);
391
+ const fin = suite.findIndex(m => m.startsWith('-'));
392
+ const noms = fin < 0 ? suite : suite.slice(0, fin);
393
+ projets.push(...(noms.length ? noms : ['.']).map(versProjet));
394
+ } else {
395
+ projets.push(versProjet('.'));
396
+ }
397
+ }
398
+ const dossiers = new Set();
399
+ for (const projet of new Set(projets)) {
400
+ const lu = luParTsc(abs, projet);
401
+ if (lu === null) return null;
402
+ for (const f of lu.fichiers) fichiers.add(f);
403
+ for (const d of lu.dossiers) dossiers.add(d);
404
+ }
405
+ const lus = [...dossiers];
406
+ return [
407
+ ...[...fichiers, ...lus.map(d => `${d}/**`)].sort(),
408
+ ...gitsExclus(
409
+ lus.map(d => `${dossier}/${d}`),
410
+ sm,
411
+ '',
412
+ dossier
413
+ ),
414
+ ];
415
+ }
416
+
417
+ // The data and traces each package of the frame declares in its package.json:
418
+ // [{ dossier, pkg, lit, traces: [{ tache, id, lit, ecrit }] }], each path from the root.
419
+ function declarations(liste) {
420
+ return liste
421
+ .filter(p => !p.hors)
422
+ .map(({ dossier, pkg }) => {
423
+ const g = pkg.grillhouse ?? {};
424
+ const chemins = l => [...new Set((Array.isArray(l) ? l : []).map(net))].sort();
425
+ return {
426
+ dossier,
427
+ pkg,
428
+ lit: chemins(g.lit),
429
+ traces: Object.entries(g.traces ?? {})
430
+ .map(([tache, t]) => ({
431
+ tache,
432
+ id: `${pkg.name}#${tache}`,
433
+ lit: chemins(t?.lit),
434
+ ecrit: net(t?.ecrit),
435
+ }))
436
+ .sort((a, b) => (a.tache < b.tache ? -1 : 1)),
437
+ };
438
+ });
439
+ }
440
+
441
+ // The text of a package's turbo.json, whose tasks are <taches>.
442
+ const texte = taches =>
443
+ `// Généré par \`${COMMANDE}\` depuis les package.json, le turbo.json racine et docs/agents/hors-cadre.txt ; ne pas éditer.\n${JSON.stringify(
444
+ { $schema: 'https://turborepo.com/schema.json', extends: ['//'], tasks: taches },
445
+ null,
446
+ 2
447
+ )}\n`;
448
+
449
+ // The internal packages the <champs> of <pkg> name, among <noms>, itself left out, sorted.
450
+ const internes = (pkg, champs, noms) =>
451
+ [...new Set(champs.flatMap(c => Object.keys(pkg[c] ?? {})))]
452
+ .filter(n => n !== pkg.name && noms.has(n))
453
+ .sort();
454
+
455
+ // The turbo.json each package of <racine> must hold: Map<path relative to <racine>, text>.
456
+ export function attendus(racine) {
457
+ const liste = paquets(racine);
458
+ const racineTaches = [...tachesRacine(racine)].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
459
+ const deRacine = new Map(racineTaches);
460
+ const hors = new Set(liste.filter(p => p.hors).map(({ pkg }) => pkg.name));
461
+ const duCadre = new Set(liste.filter(p => !p.hors).map(({ pkg }) => pkg.name));
462
+ const construits = new Set(
463
+ liste
464
+ .filter(({ pkg, hors }) => !hors && typeof pkg.scripts?.build === 'string')
465
+ .map(({ pkg }) => pkg.name)
466
+ );
467
+ const decls = new Map(declarations(liste).map(d => [d.dossier, d]));
468
+ const traces = [...decls.values()].flatMap(d => d.traces);
469
+ // The trace tasks whose output one of <lit> overlaps, <soi> left out, sorted.
470
+ const jointes = (lit, soi) =>
471
+ traces
472
+ .filter(t => t.id !== soi && lit.some(l => recouvre(l, t.ecrit)))
473
+ .map(t => t.id)
474
+ .sort();
475
+ // The paths of data alone declared: the ones a trace writes left out, which git ignores, and the
476
+ // ones holding code, which stay inputs of the build of their package.
477
+ const ecrits = traces.map(t => t.ecrit);
478
+ const donnees = [
479
+ ...new Set([...decls.values()].flatMap(d => [...d.lit, ...d.traces.flatMap(t => t.lit)])),
480
+ ].filter(l => !ecrits.some(e => recouvre(l, e)) && !contientCode(racine, l, ecrits));
481
+ // The inputs of the root's <tache> a package's own extend: the root's, or Turborepo's default
482
+ // when the root sets none (`$TURBO_EXTENDS$` then extends an empty list).
483
+ const base = tache => (deRacine.get(tache)?.inputs ? '$TURBO_EXTENDS$' : '$TURBO_DEFAULT$');
484
+ // The inputs of the root's build a package's build keeps when it names its own: Turborepo's
485
+ // default left out, which would hash every file of the package again.
486
+ const racineBuild = (deRacine.get('build')?.inputs ?? []).filter(e => e !== '$TURBO_DEFAULT$');
487
+ const sm = sousModules(racine);
488
+ const out = new Map();
489
+ for (const { dossier, pkg, hors: exclu } of liste) {
490
+ const taches = {};
491
+ if (exclu) {
492
+ for (const [nom] of racineTaches) taches[nom] = { extends: false };
493
+ } else {
494
+ taches.build = {
495
+ dependsOn: internes(pkg, EXECUTION, construits).map(n => `${n}#build`),
496
+ };
497
+ const dedans = donnees
498
+ .filter(d => d !== dossier && sous(d, dossier))
499
+ .map(d => path.posix.relative(dossier, d))
500
+ .sort();
501
+ const propres = entreesBuild(racine, dossier, pkg, sm);
502
+ const sansDonnees = dedans.map(d => `!${d}/**`);
503
+ if (propres) taches.build.inputs = [...racineBuild, ...propres, ...sansDonnees];
504
+ else if (dedans.length) taches.build.inputs = [base('build'), ...sansDonnees];
505
+ const horsVoisin = TOPOLOGIE.some(c => Object.keys(pkg[c] ?? {}).some(n => hors.has(n)));
506
+ const voisins = internes(pkg, TOPOLOGIE, duCadre);
507
+ // <dependances> with each `^<task>` written out when a neighbour is out of the frame.
508
+ const deplier = dependances =>
509
+ horsVoisin
510
+ ? [
511
+ ...dependances.filter(d => !d.startsWith('^')),
512
+ ...dependances
513
+ .filter(d => d.startsWith('^'))
514
+ .flatMap(d => voisins.map(n => `${n}#${d.slice(1)}`))
515
+ .sort(),
516
+ ]
517
+ : dependances;
518
+ if (horsVoisin) {
519
+ for (const [nom, { dependsOn }] of racineTaches) {
520
+ if (nom === 'build' || !dependsOn.some(d => d.startsWith('^'))) continue;
521
+ taches[nom] = { dependsOn: deplier(dependsOn) };
522
+ }
523
+ }
524
+ const decl = decls.get(dossier);
525
+ if (decl.lit.length) {
526
+ taches[TESTS] = {
527
+ dependsOn: [...deplier(deRacine.get(TESTS)?.dependsOn ?? []), ...jointes(decl.lit)],
528
+ inputs: [
529
+ base(TESTS),
530
+ ...decl.lit.map(l => `$TURBO_ROOT$/${l}/**`),
531
+ ...gitsExclus(decl.lit, sm),
532
+ ],
533
+ };
534
+ }
535
+ for (const t of decl.traces) {
536
+ const ecrit = path.posix.relative(dossier, t.ecrit);
537
+ taches[t.tache] = {
538
+ dependsOn: [...deplier(['build', '^build']), ...jointes(t.lit, t.id)],
539
+ inputs: [
540
+ '$TURBO_DEFAULT$',
541
+ ...t.lit.map(l => `$TURBO_ROOT$/${l}/**`),
542
+ ...gitsExclus(t.lit, sm),
543
+ `!${ecrit}/**`,
544
+ ],
545
+ outputs: [`${ecrit}/**`],
546
+ };
547
+ }
548
+ }
549
+ out.set(path.join(dossier, TURBO), texte(taches));
550
+ }
551
+ return out;
552
+ }
553
+
554
+ // The `dependsOn` each guard task of the root of <racine> must hold: Map<task key, dependsOn>.
555
+ export function gardesAttendues(racine) {
556
+ const liste = paquets(racine);
557
+ if (liste.length === 0) return new Map(GARDES.map(g => [g, ['build']]));
558
+ const builds = liste
559
+ .filter(({ pkg, hors }) => !hors && typeof pkg.scripts?.build === 'string')
560
+ .map(({ pkg }) => `${pkg.name}#build`)
561
+ .sort();
562
+ return new Map(GARDES.map(g => [`//#${g}`, builds]));
563
+ }
564
+
565
+ // The guard tasks of the root's turbo.json at <racine> whose `dependsOn` differs from the one
566
+ // expected: [{ cle, attendu, present }] (present: whether the task is in the file), with the file;
567
+ // null without a root configuration, or when it is no JSON.
568
+ function gardesEcartees(racine) {
569
+ const f = fichierRacine(racine);
570
+ if (f === null) return null;
571
+ const texte = readFileSync(path.join(racine, f), 'utf8');
572
+ const conf = lireJsonc(texte);
573
+ if (conf === null) return null;
574
+ const ecarts = [];
575
+ for (const [cle, attendu] of gardesAttendues(racine)) {
576
+ const tache = conf.tasks?.[cle];
577
+ const actuel = tache?.dependsOn;
578
+ if (!Array.isArray(actuel) || actuel.join('\n') !== attendu.join('\n')) {
579
+ ecarts.push({ cle, attendu, present: tache !== undefined });
580
+ }
581
+ }
582
+ return { f, texte, ecarts };
583
+ }
584
+
585
+ // The `env` the `test:vitest` of the root's turbo.json at <racine> must hold, TESTS_CIBLES added
586
+ // to the project's, when it lacks it: { f, attendu }; null when it holds it, without a root
587
+ // configuration or a `test:vitest` in it, and when it is no JSON.
588
+ function testsSansVariable(racine) {
589
+ const r = confRacine(racine);
590
+ const tache = r?.conf.tasks?.[TESTS];
591
+ if (!tache || typeof tache !== 'object') return null;
592
+ const actuel = Array.isArray(tache.env) ? tache.env : [];
593
+ return actuel.includes(TESTS_CIBLES) ? null : { f: r.f, attendu: [...actuel, TESTS_CIBLES] };
594
+ }
595
+
596
+ // The `globalPassThroughEnv` the root's turbo.json at <racine> must hold, each of PASSAGES it
597
+ // lacks added to the project's: { f, attendu, manques }; null when it holds them, without a root
598
+ // configuration, and when it is no JSON.
599
+ function passagesManquants(racine) {
600
+ const r = confRacine(racine);
601
+ if (!r) return null;
602
+ const actuel = Array.isArray(r.conf.globalPassThroughEnv) ? r.conf.globalPassThroughEnv : [];
603
+ const manques = PASSAGES.filter(v => !actuel.includes(v));
604
+ return manques.length ? { f: r.f, attendu: [...actuel, ...manques], manques } : null;
605
+ }
606
+
607
+ // The inputs of the root's turbo.json at <racine> that can reach the `.git` of a submodule its
608
+ // .gitmodules declares (a pattern under `$TURBO_ROOT$/` of a task, or a global dependency, whose
609
+ // fixed part is the submodule or holds it, then a glob) without its exclusion beside them:
610
+ // { f, listes: [{ chemin, attendu, manques: [{ ou, e, m, ligne }] }] }, `attendu` the list with
611
+ // each exclusion `!<path>/.git` added after the first pattern lacking it; null when none lacks it,
612
+ // without a root configuration or a submodule.
613
+ function gitsAbsents(racine) {
614
+ const r = confRacine(racine);
615
+ const sm = r === null ? [] : sousModules(racine);
616
+ if (sm.length === 0) return null;
617
+ const listes = [];
618
+ const examiner = (chemin, entrees, prefixe, ou) => {
619
+ if (!Array.isArray(entrees)) return;
620
+ const attendu = entrees.map(String);
621
+ const manques = [];
622
+ for (const e of entrees.map(String)) {
623
+ if (e.startsWith('!') || !e.startsWith(prefixe)) continue;
624
+ const { fixe, glob } = partieFixe(e.slice(prefixe.length));
625
+ for (const m of sm) {
626
+ const ligne = `!${prefixe}${m}/.git`;
627
+ if (!glob || !(fixe === '' || sous(m, fixe)) || attendu.includes(ligne)) continue;
628
+ attendu.splice(attendu.indexOf(e) + 1, 0, ligne);
629
+ manques.push({ ou, e, m, ligne });
630
+ }
631
+ }
632
+ if (manques.length) listes.push({ chemin, attendu, manques });
633
+ };
634
+ for (const [nom, t] of Object.entries(r.conf.tasks ?? {})) {
635
+ examiner(['tasks', nom, 'inputs'], t?.inputs, '$TURBO_ROOT$/', `l'entrée de \`${nom}\``);
636
+ }
637
+ examiner(['globalDependencies'], r.conf.globalDependencies, '', 'la dépendance globale');
638
+ return listes.length ? { f: r.f, listes } : null;
639
+ }
640
+
641
+ // Writes the turbo.json of each package of <racine>, and in its root's the guards' dependsOn, the
642
+ // tests' env and the exclusion of a submodule's `.git` beside each input reaching it; returns the
643
+ // paths written.
644
+ export function ecrire(racine) {
645
+ const ecrits = [];
646
+ for (const [f, t] of attendus(racine)) {
647
+ const abs = path.join(racine, f);
648
+ if (existsSync(abs) && readFileSync(abs, 'utf8') === t) continue;
649
+ writeFileSync(abs, t);
650
+ ecrits.push(f);
651
+ }
652
+ const racineEcartee = gardesEcartees(racine);
653
+ if (racineEcartee?.ecarts.length) {
654
+ let texte = racineEcartee.texte;
655
+ for (const { cle, attendu, present } of racineEcartee.ecarts) {
656
+ const [chemin, valeur] = present
657
+ ? [['tasks', cle, 'dependsOn'], attendu]
658
+ : [['tasks', cle], { dependsOn: attendu, outputs: [] }];
659
+ texte = applyEdits(texte, modify(texte, chemin, valeur, MISE_EN_FORME));
660
+ }
661
+ writeFileSync(path.join(racine, racineEcartee.f), texte);
662
+ ecrits.push(racineEcartee.f);
663
+ }
664
+ const tests = testsSansVariable(racine);
665
+ if (tests) {
666
+ const abs = path.join(racine, tests.f);
667
+ const texte = readFileSync(abs, 'utf8');
668
+ const chemin = ['tasks', TESTS, 'env'];
669
+ writeFileSync(abs, applyEdits(texte, modify(texte, chemin, tests.attendu, MISE_EN_FORME)));
670
+ if (!ecrits.includes(tests.f)) ecrits.push(tests.f);
671
+ }
672
+ const passage = passagesManquants(racine);
673
+ if (passage) {
674
+ const abs = path.join(racine, passage.f);
675
+ const texte = readFileSync(abs, 'utf8');
676
+ const chemin = ['globalPassThroughEnv'];
677
+ writeFileSync(abs, applyEdits(texte, modify(texte, chemin, passage.attendu, MISE_EN_FORME)));
678
+ if (!ecrits.includes(passage.f)) ecrits.push(passage.f);
679
+ }
680
+ const gits = gitsAbsents(racine);
681
+ if (gits) {
682
+ const abs = path.join(racine, gits.f);
683
+ let texte = readFileSync(abs, 'utf8');
684
+ for (const { chemin, attendu } of gits.listes) {
685
+ texte = applyEdits(texte, modify(texte, chemin, attendu, MISE_EN_FORME));
686
+ }
687
+ writeFileSync(abs, texte);
688
+ if (!ecrits.includes(gits.f)) ecrits.push(gits.f);
689
+ }
690
+ return ecrits;
691
+ }
692
+
693
+ // Whether <chemin> of <racine> (a directory, or a file, from the root) holds a file of code; what
694
+ // lies under one of <exclus> left out, as `parcourir` walks it.
695
+ function contientCode(racine, chemin, exclus) {
696
+ if (exclus.some(e => sous(chemin, e))) return false;
697
+ const rel = p => path.relative(racine, p).split(path.sep).join('/');
698
+ return parcourir(path.join(racine, chemin), p => exclus.some(e => sous(rel(p), e))).some(f =>
699
+ CODE.test(f)
700
+ );
701
+ }
702
+
703
+ const gitSort = (racine, args) => spawnSync('git', args, { cwd: racine, encoding: 'utf8' });
704
+
705
+ // The refusals of the data and traces the packages of <racine> declare: one line per declared
706
+ // path absent that no trace writes, per trace written out of its producer,
707
+ // followed by git or not ignored, per trace script without declaration and declaration without
708
+ // script.
709
+ function declarationsRefusees(racine) {
710
+ const decls = declarations(paquets(racine));
711
+ const ecrits = decls.flatMap(d => d.traces.map(t => t.ecrit));
712
+ const refus = [];
713
+ for (const { dossier, pkg, lit, traces } of decls) {
714
+ const ici = `${dossier}/package.json`;
715
+ const declarees = new Set(traces.map(t => t.tache));
716
+ for (const s of Object.keys(pkg.scripts ?? {}).filter(s => s.startsWith(TRACE))) {
717
+ if (!declarees.has(s)) {
718
+ refus.push(
719
+ `⛔ ${ici} : le script \`${s}\` n'a pas de déclaration dans \`grillhouse.traces\` (les dossiers qu'il lit, celui qu'il écrit).`
720
+ );
721
+ }
722
+ }
723
+ for (const t of traces) {
724
+ if (!t.tache.startsWith(TRACE)) {
725
+ refus.push(`⛔ ${ici} : la trace \`${t.tache}\` se nomme \`${TRACE}<nom>\`.`);
726
+ } else if (typeof pkg.scripts?.[t.tache] !== 'string') {
727
+ refus.push(
728
+ `⛔ ${ici} : la trace \`${t.tache}\` n'a pas de script du même nom, sa commande.`
729
+ );
730
+ }
731
+ if (t.ecrit === dossier || !sous(t.ecrit, dossier)) {
732
+ refus.push(
733
+ `⛔ ${ici} : la trace \`${t.tache}\` écrit \`${t.ecrit}\`, hors de son paquet ; une trace vit dans le paquet qui la produit.`
734
+ );
735
+ } else if (gitSort(racine, ['ls-files', '--', t.ecrit]).stdout.trim()) {
736
+ refus.push(
737
+ `⛔ ${ici} : git suit \`${t.ecrit}\`, que la trace \`${t.tache}\` écrit ; une trace se régénère, git l'ignore (\`git rm -r --cached\`).`
738
+ );
739
+ } else if (gitSort(racine, ['check-ignore', '-q', `${t.ecrit}/x`]).status !== 0) {
740
+ refus.push(
741
+ `⛔ ${ici} : \`${t.ecrit}\`, que la trace \`${t.tache}\` écrit, manque au .gitignore ; une trace se régénère, git l'ignore.`
742
+ );
743
+ }
744
+ }
745
+ for (const l of [...new Set([...lit, ...traces.flatMap(t => t.lit)])].sort()) {
746
+ const sortie = ecrits.some(e => recouvre(l, e));
747
+ if (!sortie && !existsSync(path.join(racine, l))) {
748
+ refus.push(
749
+ `⛔ ${ici} : le chemin lu \`${l}\` n'existe pas, et aucune trace ne l'écrit (\`grillhouse.traces\`).`
750
+ );
751
+ }
752
+ }
753
+ }
754
+ return refus;
755
+ }
756
+
757
+ // The refusals of the configurations of Vitest that Vite loads as ESM in a package of the shared
758
+ // test material of <racine>: one line per such file, as Vite judges it (`.mjs`, `.mts`, or `.js`,
759
+ // `.ts` in a package of `"type": "module"`).
760
+ function configsMatiereRefusees(racine) {
761
+ const refus = [];
762
+ for (const { dir } of matieres(racine)) {
763
+ const { type } = JSON.parse(readFileSync(path.join(dir, 'package.json'), 'utf8'));
764
+ for (const ext of ['mjs', 'mts', ...(type === 'module' ? ['js', 'ts'] : [])]) {
765
+ const f = path.relative(racine, path.join(dir, `vitest.config.${ext}`));
766
+ if (!existsSync(path.join(racine, f))) continue;
767
+ refus.push(
768
+ `⛔ ${f} : la configuration de Vitest du paquet de la matière s'écrit en CommonJS, \`vitest.config.cjs\` ; Vite écrit un fichier à côté d'une configuration ESM pour la charger, et la matière est en lecture seule chez tout rôle autre que le testwriter.`
769
+ );
770
+ }
771
+ }
772
+ return refus;
773
+ }
774
+
775
+ // Whether the pattern <motif> (from the root) can name a file of one of the <dossiers>: its fixed
776
+ // part, before the first glob character, lies in one, or holds one under a glob.
777
+ function visePaquet(motif, dossiers) {
778
+ const { fixe, glob } = partieFixe(motif);
779
+ return dossiers.some(d => sous(fixe, d) || (glob && (fixe === '' || sous(d, fixe))));
780
+ }
781
+
782
+ // The refusals of the root's turbo.json at <racine> that the generator owns: a task of one package
783
+ // (`<package>#<task>`), a trace task, an input (`inputs` of a task, `globalDependencies`) naming a
784
+ // package directory.
785
+ function racineRefusee(racine) {
786
+ const r = confRacine(racine);
787
+ if (r === null) return [];
788
+ const { f, conf } = r;
789
+ const dossiers = paquets(racine).map(p => p.dossier);
790
+ const refus = [];
791
+ const entree = (e, ou) =>
792
+ refus.push(
793
+ `⛔ ${f} : ${ou} \`${e}\` vise un dossier de paquet ; un paquet déclare les dossiers que ses tests lisent dans son package.json (\`grillhouse.lit\`), et \`${COMMANDE}\` écrit son turbo.json.`
794
+ );
795
+ for (const [nom, t] of Object.entries(conf.tasks ?? {})) {
796
+ const diese = nom.indexOf('#');
797
+ if (diese >= 0 && nom.slice(0, diese) !== '//') {
798
+ refus.push(
799
+ `⛔ ${f} : la tâche \`${nom}\` configure un paquet à la main ; son turbo.json se génère depuis son package.json (\`${COMMANDE}\`).`
800
+ );
801
+ } else if (nom.slice(diese + 1).startsWith(TRACE)) {
802
+ refus.push(
803
+ `⛔ ${f} : la tâche \`${nom}\` est une trace ; elle se déclare au package.json du paquet qui la produit (\`grillhouse.traces\`).`
804
+ );
805
+ }
806
+ for (const e of Array.isArray(t?.inputs) ? t.inputs : []) {
807
+ const motif = String(e).replace(/^!/, '');
808
+ if (motif.startsWith('$TURBO_ROOT$/') && visePaquet(motif.slice(13), dossiers)) {
809
+ entree(e, `l'entrée de \`${nom}\``);
810
+ }
811
+ }
812
+ }
813
+ for (const e of Array.isArray(conf.globalDependencies) ? conf.globalDependencies : []) {
814
+ if (visePaquet(String(e).replace(/^!/, ''), dossiers)) entree(e, 'la dépendance globale');
815
+ }
816
+ return refus;
817
+ }
818
+
819
+ // The refusals of <racine>: one line per package turbo.json absent or other than generated, per
820
+ // guard task of the root absent or whose dependsOn differs, for a root `test:vitest` whose `env`
821
+ // lacks TESTS_CIBLES, per variable of PASSAGES the root's `globalPassThroughEnv` lacks, per configuration
822
+ // of a package written by hand in the root's turbo.json, and per declaration of data or trace
823
+ // refused.
824
+ export function verifier(racine) {
825
+ const refus = [
826
+ ...racineRefusee(racine),
827
+ ...declarationsRefusees(racine),
828
+ ...configsMatiereRefusees(racine),
829
+ ];
830
+ const gits = gitsAbsents(racine);
831
+ for (const { ou, e, m, ligne } of (gits?.listes ?? []).flatMap(l => l.manques)) {
832
+ refus.push(
833
+ `⛔ ${gits.f} : ${ou} \`${e}\` couvre le sous-module \`${m}\` sans exclure son \`.git\`, un fichier qui nomme la copie où il est posé : chaque copie neuve change l'empreinte, et le cache ne sert jamais ; \`${COMMANDE}\` ajoute \`${ligne}\` à côté.`
834
+ );
835
+ }
836
+ const tests = testsSansVariable(racine);
837
+ if (tests) {
838
+ refus.push(
839
+ `⛔ ${tests.f} : l'\`env\` de \`${TESTS}\` doit nommer ${TESTS_CIBLES}, qui porte les chemins d'une passe ciblée dans le seul hachage des tests ; \`${COMMANDE}\` l'ajoute.`
840
+ );
841
+ }
842
+ const passage = passagesManquants(racine);
843
+ for (const v of passage?.manques ?? []) {
844
+ refus.push(
845
+ `⛔ ${passage.f} : \`globalPassThroughEnv\` doit nommer ${v}, ${POURQUOI[v]} ; \`${COMMANDE}\` l'ajoute.`
846
+ );
847
+ }
848
+ for (const { cle, attendu, present } of gardesEcartees(racine)?.ecarts ?? []) {
849
+ const f = fichierRacine(racine);
850
+ refus.push(
851
+ present
852
+ ? `⛔ ${f} : la tâche \`${cle}\` doit attendre ${attendu.join(', ') || 'aucune construction'} (la construction de chaque paquet du cadre) ; \`${COMMANDE}\` réécrit son dependsOn.`
853
+ : `⛔ ${f} : la tâche \`${cle}\` manque ; \`${COMMANDE}\` l'ajoute, après la construction de chaque paquet du cadre.`
854
+ );
855
+ }
856
+ for (const [f, t] of attendus(racine)) {
857
+ const abs = path.join(racine, f);
858
+ if (!existsSync(abs)) {
859
+ refus.push(
860
+ `⛔ ${f} manque : \`${COMMANDE}\` l'écrit depuis les package.json, le turbo.json racine et docs/agents/hors-cadre.txt.`
861
+ );
862
+ } else if (readFileSync(abs, 'utf8') !== t) {
863
+ refus.push(
864
+ `⛔ ${f} diffère de ce que donnent les package.json, le turbo.json racine et docs/agents/hors-cadre.txt : \`${COMMANDE}\` le réécrit (une dépendance s'écrit au package.json, un paquet hors cadre à hors-cadre.txt).`
865
+ );
866
+ }
867
+ }
868
+ return refus;
869
+ }
870
+
871
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
872
+ const racine = process.cwd();
873
+ const args = process.argv.slice(2);
874
+ if (args.some(a => a !== '--ecrire')) {
875
+ process.stderr.write(
876
+ 'Usage : npx grillhouse turbo-des-paquets [--ecrire] vérifie, ou écrit, le turbo.json de chaque paquet, et à la racine les dépendances des gardes et l’env des tests\n'
877
+ );
878
+ process.exitCode = 2;
879
+ } else {
880
+ try {
881
+ if (args.includes('--ecrire')) {
882
+ ecrire(racine);
883
+ } else {
884
+ const refus = verifier(racine);
885
+ for (const l of refus) process.stderr.write(`${l}\n`);
886
+ // Each refusal judges the root's configuration: a finding without a file of a component.
887
+ ecrireSarif({
888
+ garde: 'turbo-des-paquets',
889
+ racine,
890
+ constats: refus.map(l => ({ message: l })),
891
+ });
892
+ process.exitCode = refus.length ? 1 : 0;
893
+ }
894
+ } catch (e) {
895
+ process.stderr.write(`${e instanceof Error ? e.message : e}\n`);
896
+ process.exitCode = 1;
897
+ }
898
+ }
899
+ }