@roomi-fields/grillhouse 0.0.0-stage → 0.23.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (166) hide show
  1. package/.claude-plugin/plugin.json +9 -0
  2. package/METACADRE.md +41 -0
  3. package/agents/developpeur.md +10 -0
  4. package/agents/explorateur.md +9 -0
  5. package/agents/questionneur.md +11 -0
  6. package/agents/referent-technique.md +9 -0
  7. package/agents/regulateur.md +11 -0
  8. package/agents/relecteur.md +9 -0
  9. package/agents/testwriter.md +10 -0
  10. package/bin/grillhouse.mjs +125 -0
  11. package/cadre/templates/projet/.claude/settings.json +30 -0
  12. package/cadre/templates/projet/.editorconfig +47 -0
  13. package/cadre/templates/projet/.env.example +57 -0
  14. package/cadre/templates/projet/.eslintrc.cjs +56 -0
  15. package/cadre/templates/projet/.github/workflows/ci.yml +66 -0
  16. package/cadre/templates/projet/.mcp.json +13 -0
  17. package/cadre/templates/projet/.prettierrc +11 -0
  18. package/cadre/templates/projet/CHANGELOG.md +61 -0
  19. package/cadre/templates/projet/CLAUDE.md +46 -0
  20. package/cadre/templates/projet/CONTRIBUTING.md +243 -0
  21. package/cadre/templates/projet/CREDITS.md +41 -0
  22. package/cadre/templates/projet/README.md +152 -0
  23. package/cadre/templates/projet/_gitignore +91 -0
  24. package/cadre/templates/projet/_npmignore +46 -0
  25. package/cadre/templates/projet/deployment/README.md +98 -0
  26. package/cadre/templates/projet/deployment/docs/01-INSTALL.md +145 -0
  27. package/cadre/templates/projet/deployment/docs/02-CONFIGURATION.md +117 -0
  28. package/cadre/templates/projet/deployment/docs/03-API.md +264 -0
  29. package/cadre/templates/projet/deployment/docs/04-TROUBLESHOOTING.md +262 -0
  30. package/cadre/templates/projet/deployment/scripts/install.ps1 +100 -0
  31. package/cadre/templates/projet/deployment/scripts/start.ps1 +61 -0
  32. package/cadre/templates/projet/deployment/scripts/stop.ps1 +37 -0
  33. package/cadre/templates/projet/deployment/scripts/test.ps1 +118 -0
  34. package/cadre/templates/projet/docs/agents/issue-tracker.md +44 -0
  35. package/cadre/templates/projet/ecosystem.config.cjs +73 -0
  36. package/cadre/templates/projet/package.json +90 -0
  37. package/cadre/templates/projet/pitmaster/SUIVI.md +22 -0
  38. package/cadre/templates/projet/src/config.ts +68 -0
  39. package/cadre/templates/projet/src/errors.ts +102 -0
  40. package/cadre/templates/projet/src/index.ts +46 -0
  41. package/cadre/templates/projet/src/services/.gitkeep +8 -0
  42. package/cadre/templates/projet/src/types/index.ts +24 -0
  43. package/cadre/templates/projet/src/utils/logger.ts +106 -0
  44. package/cadre/templates/projet/tests/integration/.gitkeep +7 -0
  45. package/cadre/templates/projet/tests/unit/config.spec.ts +68 -0
  46. package/cadre/templates/projet/tests/unit/errors.spec.ts +132 -0
  47. package/cadre/templates/projet/tests/unit/types.spec.ts +73 -0
  48. package/cadre/templates/projet/tsconfig.eslint.json +9 -0
  49. package/cadre/templates/projet/tsconfig.json +41 -0
  50. package/cadre/templates/projet/tsdoc.json +6 -0
  51. package/cadre/templates/projet/vitest.config.ts +44 -0
  52. package/cadre/templates/turbo/paquets.json +29 -0
  53. package/cadre/templates/turbo/sans-paquets.json +28 -0
  54. package/charte.md +107 -0
  55. package/docs/ARCHITECTURE.md +90 -0
  56. package/hooks/crochet.mjs +33 -0
  57. package/hooks/hooks.json +58 -0
  58. package/hooks/register.tsx +558 -0
  59. package/package.json +75 -4
  60. package/scripts/avancement.mjs +244 -0
  61. package/scripts/bilan.mjs +523 -0
  62. package/scripts/branche-de-nuit.mjs +59 -0
  63. package/scripts/cadre.mjs +109 -0
  64. package/scripts/circuit.mjs +4375 -0
  65. package/scripts/code-mort.mjs +371 -0
  66. package/scripts/composants-affectes.mjs +598 -0
  67. package/scripts/consommateurs-arbitres.mjs +192 -0
  68. package/scripts/consommateurs.mjs +310 -0
  69. package/scripts/crochet.mjs +24 -0
  70. package/scripts/crochets.mjs +135 -0
  71. package/scripts/dernier-vert.mjs +208 -0
  72. package/scripts/dette.mjs +181 -0
  73. package/scripts/droits.mjs +354 -0
  74. package/scripts/enveloppe/codes.mjs +17 -0
  75. package/scripts/enveloppe/composant.mjs +11 -0
  76. package/scripts/enveloppe/enveloppe.mjs +628 -0
  77. package/scripts/enveloppe/lancer.sh +311 -0
  78. package/scripts/etapes.mjs +323 -0
  79. package/scripts/etat-du-circuit.mjs +104 -0
  80. package/scripts/frontieres.mjs +456 -0
  81. package/scripts/gardes-du-cadre.mjs +72 -0
  82. package/scripts/greffons.mjs +338 -0
  83. package/scripts/grillhouse-maj.mjs +514 -0
  84. package/scripts/index-interfaces.mjs +293 -0
  85. package/scripts/init.mjs +117 -0
  86. package/scripts/integration/file-attente.mjs +61 -0
  87. package/scripts/integration/file.mjs +494 -0
  88. package/scripts/integration/integrer.mjs +381 -0
  89. package/scripts/integration/rejeu.mjs +139 -0
  90. package/scripts/integration/verdict.mjs +13 -0
  91. package/scripts/interfaces-contre-code.mjs +547 -0
  92. package/scripts/ligne-etat/defaut.sh +110 -0
  93. package/scripts/ligne-etat/ligne-etat.mjs +93 -0
  94. package/scripts/migrations/0.23.0-charte-importee.mjs +47 -0
  95. package/scripts/migrations/0.23.0-config-vitest.mjs +105 -0
  96. package/scripts/migrations/0.23.0-crochets-de-seance.mjs +55 -0
  97. package/scripts/migrations/0.23.0-marche-du-projet.mjs +82 -0
  98. package/scripts/migrations.mjs +43 -0
  99. package/scripts/nuit.sh +269 -0
  100. package/scripts/paquets.mjs +79 -0
  101. package/scripts/poussee-cles.mjs +46 -0
  102. package/scripts/preparation.mjs +84 -0
  103. package/scripts/references-par-les-types.mjs +420 -0
  104. package/scripts/registre.mjs +336 -0
  105. package/scripts/resultats.mjs +580 -0
  106. package/scripts/resumes.mjs +57 -0
  107. package/scripts/retirer-copie.sh +127 -0
  108. package/scripts/session-start.sh +203 -0
  109. package/scripts/sortie.mjs +155 -0
  110. package/scripts/sous-modules.sh +316 -0
  111. package/scripts/structure-guard.sh +27 -0
  112. package/scripts/superviseur.mjs +102 -0
  113. package/scripts/tableau.mjs +1886 -0
  114. package/scripts/tests.mjs +236 -0
  115. package/scripts/ticket.sh +23 -0
  116. package/scripts/turbo-des-paquets.mjs +899 -0
  117. package/scripts/un-commit-par-ticket.mjs +212 -0
  118. package/scripts/verifier.mjs +469 -0
  119. package/scripts/verrou-de-fichier.mjs +130 -0
  120. package/scripts/verrou-de-machine.mjs +211 -0
  121. package/scripts/verrous/commande.mjs +180 -0
  122. package/scripts/verrous/ticket-du-sous-agent.mjs +39 -0
  123. package/scripts/verrous/verrou.mjs +165 -0
  124. package/scripts/vitest-config/avancement.mjs +39 -0
  125. package/scripts/vitest-config/avancement.reporter.mjs +56 -0
  126. package/scripts/vitest-config/base-de-passe.mjs +128 -0
  127. package/scripts/vitest-config/base.d.ts +6 -0
  128. package/scripts/vitest-config/base.mjs +87 -0
  129. package/scripts/vitest-config/changements.mjs +173 -0
  130. package/scripts/vitest-config/cibles.mjs +207 -0
  131. package/scripts/vitest-config/duree.reporter.mjs +98 -0
  132. package/scripts/vitest-config/machine.setup.mjs +14 -0
  133. package/scripts/vitest-config/mesure.d.ts +6 -0
  134. package/scripts/vitest-config/mesure.mjs +13 -0
  135. package/scripts/vitest-config/mesure.setup.mjs +12 -0
  136. package/scripts/vitest-config/pression.mjs +90 -0
  137. package/scripts/vitest-config/resultats.mjs +23 -0
  138. package/scripts/vitest-config/resultats.setup.mjs +19 -0
  139. package/scripts/vitest.nuit.config.mjs +26 -0
  140. package/skills/developpeur/SKILL.md +175 -0
  141. package/skills/explorateur/SKILL.md +84 -0
  142. package/skills/grill/SKILL.md +238 -0
  143. package/skills/grill/references/releve-structurel.md +38 -0
  144. package/skills/mesure/SKILL.md +168 -0
  145. package/skills/pitmaster/SKILL.md +382 -0
  146. package/skills/pitmaster/references/cadre.md +119 -0
  147. package/skills/pitmaster/references/consigne-agent.md +308 -0
  148. package/skills/pitmaster/references/erreurs.md +25 -0
  149. package/skills/pitmaster/references/tour.md +124 -0
  150. package/skills/questionneur/SKILL.md +64 -0
  151. package/skills/redacteur/SKILL.md +84 -0
  152. package/skills/redacteur/references/documents-d-un-composant.md +105 -0
  153. package/skills/referent-technique/SKILL.md +145 -0
  154. package/skills/regulateur/SKILL.md +220 -0
  155. package/skills/release/SKILL.md +134 -0
  156. package/skills/release/scripts/update-version.cjs +92 -0
  157. package/skills/relecteur/SKILL.md +152 -0
  158. package/skills/relecteur/references/LICENSE-MIT +30 -0
  159. package/skills/relecteur/references/SOURCE.md +8 -0
  160. package/skills/relecteur/references/claims-check.md +14 -0
  161. package/skills/relecteur/references/deletion-check.md +14 -0
  162. package/skills/relecteur/references/edge-case-hunter.md +80 -0
  163. package/skills/relecteur/references/verification-gap.md +113 -0
  164. package/skills/testwriter/SKILL.md +187 -0
  165. package/types/index.d.ts +56 -0
  166. package/README.md +0 -3
@@ -0,0 +1,547 @@
1
+ #!/usr/bin/env node
2
+ // The written interface of each component against its code.
3
+ //
4
+ // - The public API of a component is reported by API Extractor from its built declarations:
5
+ // `docs/INTERFACE.api.md`, kept in the repository. The check refuses an API that changed
6
+ // without its report. The interface is compared to the built API (the documentation model
7
+ // API Extractor builds from the code), not to the recorded report. A package's entry is its main
8
+ // entry (`types`, or `exports["."].types`); its other entries (test material) are not its API. A
9
+ // module of `src/` has `<types directory>/<module>/index.d.ts`. Types of a neighbour stay
10
+ // references to it.
11
+ // - The contract of each export lives in its TSDoc comment, in the code: a summary, one `@param`
12
+ // per parameter (a destructured parameter by its position, under any name), `@returns` for a
13
+ // function that returns a value (not void, undefined, never or Promise<void>), and one
14
+ // `@faute CODE` per fault it returns, the code being one of the « ## Les fautes » catalogue. The
15
+ // frame's `tsdoc.json` (repository root) declares `@faute` and is read for every component; an
16
+ // unknown or malformed TSDoc tag is refused. Every export is judged on its own rubrics, each
17
+ // overload signature included; the members of a class or interface are not.
18
+ // - `docs/INTERFACE.md` has two parts: the hand-written one, never touched, and the generated one
19
+ // between the marks `<!-- généré: début -->` and `<!-- généré: fin -->`, one entry per export
20
+ // (title, signature, summary, parameters, return, faults; an interface or a class lists its
21
+ // members). The check refuses a generated part that differs from what `--ecrire` writes, and
22
+ // marks that are missing, alone, doubled or out of order. `--ecrire` adds the marks at the end
23
+ // of an interface that has none, writes the part even when a rubric is missing, and then
24
+ // returns the refusals of the check.
25
+ // - The root interface `docs/INTERFACE.md` is not a component's: it needs no marks.
26
+ // - A package whose `exports` has subpaths only (`./test-fixtures`, the shared test material) and
27
+ // no `.` entry has no public API, nor has a package without `exports`, `main`, `types` or
28
+ // `typings` (a package of tools, its commands in `bin`): no built entry, no report, no generated
29
+ // part nor marks are asked of it, and nothing is reported; its « ## Les fautes », if any, is
30
+ // checked against its code.
31
+ // - When the interface has a « ## Les fautes » section, its codes (`PARSE_EXPECTED`…) are exactly
32
+ // the codes with the same prefixes found in the component's production code, and each is named
33
+ // by an `@faute` of the component.
34
+ //
35
+ // npx grillhouse interfaces-contre-code checks every component (run after the build)
36
+ // npm run api writes the API reports and the generated parts
37
+ // (`--ecrire`; run after the build)
38
+ //
39
+ // A missing report, or an API changed without its report, is refused with `npm run api`, the
40
+ // command that writes it; API Extractor's own warning on that report is not relayed (the refusal
41
+ // says what to do).
42
+ //
43
+ // A refusal inscribed in the project's debt (`grillhouse-dette.json`, see `dette.mjs`) passes, keyed
44
+ // by the component's interface and the rubric (`resume`, `param`, `param-orphelin`, `returns`,
45
+ // `faute-mal-formee`, `faute-hors-catalogue`, `faute-non-nommee`, `faute-non-produite`,
46
+ // `faute-non-listee`), or by the declaration file and TSDoc's message id for a TSDoc message.
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).
52
+ //
53
+ // A component without `docs/INTERFACE.md` is reported and skipped. A project that prefers a
54
+ // bench per component calls `verifierComposant(racine, composant)` from its test.
55
+ import { createRequire } from 'node:module';
56
+ import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
57
+ import { tmpdir } from 'node:os';
58
+ import path from 'node:path';
59
+ import { pathToFileURL } from 'node:url';
60
+ import { composants, lireSiPresent, parcourir } from './consommateurs.mjs';
61
+ import { composantDeLEnveloppe } from './enveloppe/composant.mjs';
62
+ import { trancher } from './dette.mjs';
63
+
64
+ const require = createRequire(import.meta.url);
65
+ const SOURCE = /\.(ts|tsx|mts|cts|js|jsx|mjs|cjs|svelte|vue)$/;
66
+ const isTest = rel =>
67
+ /(^|\/)(test|tests|__tests__)\//.test(rel) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(rel);
68
+ const CODE = /\b[A-Z][A-Z0-9]*_[A-Z0-9_]*[A-Z0-9]\b/g;
69
+
70
+ const lireJson = p => JSON.parse(readFileSync(p, 'utf8'));
71
+
72
+ // Whether a component has no public API: its package exports subpaths only (`./test-fixtures`…),
73
+ // no `.` entry, or it declares no entry at all, neither `exports`, `main`, `types` nor `typings`
74
+ // (a package of tools, its commands in `bin`). API Extractor reads one entry point; such a package
75
+ // has none.
76
+ export function sansApi(c) {
77
+ const pj = path.join(c.dir, 'package.json');
78
+ if (!existsSync(pj)) return false;
79
+ const p = lireJson(pj);
80
+ const exp = p.exports;
81
+ if (exp === undefined) return [p.main, p.types, p.typings].every(e => e === undefined);
82
+ if (!exp || typeof exp !== 'object' || Array.isArray(exp)) return false;
83
+ const cles = Object.keys(exp);
84
+ return cles.length > 0 && cles.every(k => k.startsWith('.')) && !cles.includes('.');
85
+ }
86
+
87
+ // The built entry of a component's public API, or null.
88
+ export function entree(racine, c) {
89
+ const pj = path.join(c.dir, 'package.json');
90
+ if (existsSync(pj)) {
91
+ const p = lireJson(pj);
92
+ const point = p.exports?.['.'];
93
+ const types =
94
+ (typeof point === 'object' && (point.types ?? point.import?.types)) || p.types || p.typings;
95
+ return types ? path.join(c.dir, types) : null;
96
+ }
97
+ const racinePj = path.join(racine, 'package.json');
98
+ const types = existsSync(racinePj) ? lireJson(racinePj).types : null;
99
+ if (!types) return null;
100
+ return path.join(racine, path.dirname(types), path.basename(c.dir), 'index.d.ts');
101
+ }
102
+
103
+ // The fault codes an interface text declares in « ## Les fautes » of its hand-written part (null
104
+ // without the section); the generated part and code blocks are not read.
105
+ export function declare(texte) {
106
+ let fautes = null;
107
+ let section = null;
108
+ let bloc = false;
109
+ for (const l of ecrite(texte)) {
110
+ if (/^\s*(```|~~~)/.test(l)) bloc = !bloc;
111
+ const titre = !bloc && /^(#{2,3}) (.+)$/.exec(l);
112
+ if (titre) {
113
+ section = titre[1] === '##' ? titre[2].trim() : section;
114
+ if (section === 'Les fautes' && fautes === null) fautes = new Set();
115
+ continue;
116
+ }
117
+ if (section === 'Les fautes') for (const m of l.matchAll(CODE)) fautes.add(m[0]);
118
+ }
119
+ return { fautes };
120
+ }
121
+
122
+ // The codes with one of <prefixes> found in the production code under <dir>, as `parcourir`
123
+ // walks it.
124
+ function codesDuCode(dir, racine, prefixes) {
125
+ const trouves = new Set();
126
+ const nom = p => path.basename(p);
127
+ const sources = parcourir(
128
+ dir,
129
+ p => ['node_modules', 'dist', 'docs'].includes(nom(p)) || nom(p).startsWith('.')
130
+ ).filter(p => SOURCE.test(nom(p)) && !isTest(path.relative(racine, p)));
131
+ for (const p of sources) {
132
+ for (const m of (lireSiPresent(p) ?? '').matchAll(CODE)) {
133
+ if (prefixes.some(pr => m[0].startsWith(pr))) trouves.add(m[0]);
134
+ }
135
+ }
136
+ return trouves;
137
+ }
138
+
139
+ // Runs API Extractor on one component: whether the repository's report is up to date, the exports
140
+ // of the documentation model built from the code (whether or not the report exists) and the TSDoc
141
+ // messages of its comments.
142
+ function rapportApi(racine, c, entreeDts, ecrire, tsdocConfigFile) {
143
+ const { Extractor, ExtractorConfig } = require('@microsoft/api-extractor');
144
+ const { ApiModel } = require('@microsoft/api-extractor-model');
145
+ const pj = existsSync(path.join(c.dir, 'package.json'))
146
+ ? path.join(c.dir, 'package.json')
147
+ : path.join(racine, 'package.json');
148
+ const tsconfig = [path.join(c.dir, 'tsconfig.json'), path.join(racine, 'tsconfig.json')].find(
149
+ existsSync
150
+ );
151
+ const tmp = mkdtempSync(path.join(tmpdir(), 'api-'));
152
+ const messages = [];
153
+ const tsdoc = new Map();
154
+ try {
155
+ const modele = path.join(tmp, 'modele.api.json');
156
+ const config = ExtractorConfig.prepare({
157
+ configObject: {
158
+ projectFolder: path.dirname(pj),
159
+ mainEntryPointFilePath: entreeDts,
160
+ compiler: tsconfig ? { tsconfigFilePath: tsconfig } : undefined,
161
+ apiReport: {
162
+ enabled: true,
163
+ reportFolder: path.join(c.dir, 'docs'),
164
+ reportTempFolder: tmp,
165
+ reportFileName: 'INTERFACE',
166
+ },
167
+ docModel: { enabled: true, apiJsonFilePath: modele },
168
+ dtsRollup: { enabled: false },
169
+ tsdocMetadata: { enabled: false },
170
+ messages: {
171
+ extractorMessageReporting: { 'ae-missing-release-tag': { logLevel: 'none' } },
172
+ tsdocMessageReporting: { default: { logLevel: 'warning' } },
173
+ },
174
+ },
175
+ packageJsonFullPath: pj,
176
+ tsdocConfigFile,
177
+ });
178
+ const resultat = Extractor.invoke(config, {
179
+ localBuild: ecrire,
180
+ messageCallback: m => {
181
+ const duRapport = String(m.messageId).startsWith('console-api-report');
182
+ if (m.category === 'TSDoc') {
183
+ const fichier = m.sourceFilePath ? path.relative(racine, m.sourceFilePath) : null;
184
+ const lieu = fichier ? `${fichier}:${m.sourceFileLine ?? 1}: ` : '';
185
+ tsdoc.set(`${lieu}${m.text}`, {
186
+ fichier,
187
+ ...(fichier ? { ligne: m.sourceFileLine ?? 1 } : {}),
188
+ regle: m.messageId,
189
+ });
190
+ m.logLevel = 'none';
191
+ } else if ((m.logLevel === 'error' || m.logLevel === 'warning') && (ecrire || !duRapport)) {
192
+ messages.push(m.text);
193
+ }
194
+ m.handled = true;
195
+ },
196
+ });
197
+ const paquet = new ApiModel().loadPackage(modele);
198
+ return {
199
+ ajour: resultat.succeeded && !resultat.apiReportChanged,
200
+ messages,
201
+ tsdoc: [...tsdoc].map(([texte, lieu]) => ({ texte, ...lieu })),
202
+ exports: paquet.entryPoints.flatMap(e => [...e.members]),
203
+ };
204
+ } finally {
205
+ rmSync(tmp, { recursive: true, force: true });
206
+ }
207
+ }
208
+
209
+ // ---- The TSDoc contract of an export ----
210
+
211
+ const RETOUR_VIDE = new Set(['void', 'undefined', 'never', 'Promise<void>']);
212
+ const CODE_SEUL = new RegExp(`^${CODE.source}$`);
213
+
214
+ // The text of a TSDoc node, its paragraphs apart by a blank line, each on one line.
215
+ function texteDoc(noeud) {
216
+ if (!noeud) return '';
217
+ const feuilles = n =>
218
+ n.kind === 'Excerpt' ? n.content.toString() : n.getChildNodes().map(feuilles).join('');
219
+ const paragraphes = noeud.kind === 'Section' ? noeud.getChildNodes() : [noeud];
220
+ return paragraphes
221
+ .map(p => feuilles(p).replace(/\s+/g, ' ').trim())
222
+ .filter(Boolean)
223
+ .join('\n\n');
224
+ }
225
+
226
+ // The parameters of a function as the model gives them. API Extractor lists a destructuring pattern
227
+ // (`{ a }`, `[a]`) as a parameter without type, then the parameter itself under the name `input`,
228
+ // `input2`…; the pattern entry is dropped and the parameter that follows it is marked destructured
229
+ // (documented by its position, under any name). A parameter named `input` after no pattern is a
230
+ // named parameter.
231
+ function parametresDe(item) {
232
+ const sortie = [];
233
+ let motif = false;
234
+ for (const p of item.parameters) {
235
+ if (p.parameterTypeExcerpt.isEmpty && /^[{[]/.test(p.name)) {
236
+ motif = true;
237
+ continue;
238
+ }
239
+ sortie.push({ name: p.name, destructure: motif });
240
+ motif = false;
241
+ }
242
+ return sortie;
243
+ }
244
+
245
+ // A function declared with `function` (API Extractor also models a function-typed constant as a
246
+ // function; a constant is a variable and takes its summary only).
247
+ const estFonction = item =>
248
+ item.kind === 'Function' &&
249
+ /^(?:(?:export|declare|default|async)\s+)*function\b/.test(item.getExcerptWithModifiers().trim());
250
+
251
+ // The rubrics of an export: its summary, its parameters, its return and its faults.
252
+ function contratDe(item) {
253
+ const doc = item.tsdocComment;
254
+ const blocs = doc?.params.blocks ?? [];
255
+ return {
256
+ resume: texteDoc(doc?.summarySection),
257
+ params: blocs.map(b => ({ nom: b.parameterName, texte: texteDoc(b.content) })),
258
+ retour: doc?.returnsBlock ? texteDoc(doc.returnsBlock.content) : null,
259
+ fautes: (doc?.customBlocks ?? [])
260
+ .filter(b => b.blockTag.tagName === '@faute')
261
+ .map(b => texteDoc(b.content)),
262
+ parametres: estFonction(item) ? parametresDe(item) : [],
263
+ rendUneValeur:
264
+ item.kind === 'Function' && !RETOUR_VIDE.has(item.returnTypeExcerpt.text.replace(/\s+/g, '')),
265
+ };
266
+ }
267
+
268
+ // The code an `@faute` text starts with, or null when its first word is not a code.
269
+ const codeDeFaute = texte => {
270
+ const mot = texte.split(/\s+/)[0];
271
+ return CODE_SEUL.test(mot) ? mot : null;
272
+ };
273
+
274
+ // The `@faute` texts <textes> of one element judged against the catalogue <fautes>: refusals
275
+ // (`{ regle, texte }`) are pushed on <refus>, the codes named on <codes>.
276
+ function fautesJugees(nom, textes, fautes, refus, codes) {
277
+ for (const f of textes) {
278
+ const code = codeDeFaute(f);
279
+ if (!code) {
280
+ refus.push({
281
+ regle: 'faute-mal-formee',
282
+ texte: `${nom} : un @faute est mal formé (il commence par un code, ex. @faute CODE).`,
283
+ });
284
+ } else if (!fautes?.has(code)) {
285
+ refus.push({
286
+ regle: 'faute-hors-catalogue',
287
+ texte: `${nom} : le @faute ${code} n'est pas au catalogue « ## Les fautes ».`,
288
+ });
289
+ } else codes.add(code);
290
+ }
291
+ }
292
+
293
+ // The refusals for one export against its contract (`{ regle, texte }`), with the fault codes it
294
+ // and its members name.
295
+ function jugerExport(item, fautes) {
296
+ const refus = [];
297
+ const nom = `\`${item.displayName}\``;
298
+ const k = contratDe(item);
299
+ if (!k.resume) refus.push({ regle: 'resume', texte: `${nom} n'a pas de résumé TSDoc.` });
300
+ const codes = new Set();
301
+ if (estFonction(item)) {
302
+ const noms = new Set(k.parametres.filter(p => !p.destructure).map(p => p.name));
303
+ const prises = new Set();
304
+ k.parametres.forEach((p, i) => {
305
+ const parNom = p.destructure ? -1 : k.params.findIndex(b => b.nom === p.name);
306
+ if (parNom >= 0) prises.add(parNom);
307
+ else if (!p.destructure)
308
+ refus.push({
309
+ regle: 'param',
310
+ texte: `${nom} : le paramètre « ${p.name} » n'a pas de @param.`,
311
+ });
312
+ else if (!k.params[i] || noms.has(k.params[i].nom))
313
+ refus.push({
314
+ regle: 'param',
315
+ texte: `${nom} : le paramètre déstructuré en position ${i + 1} n'a pas son @param à cette position.`,
316
+ });
317
+ else prises.add(i);
318
+ });
319
+ k.params.forEach((b, i) => {
320
+ if (!prises.has(i))
321
+ refus.push({
322
+ regle: 'param-orphelin',
323
+ texte: `${nom} : le @param « ${b.nom} » ne nomme aucun paramètre.`,
324
+ });
325
+ });
326
+ if (k.rendUneValeur && k.retour === null)
327
+ refus.push({ regle: 'returns', texte: `${nom} rend une valeur mais n'a pas de @returns.` });
328
+ }
329
+ fautesJugees(nom, k.fautes, fautes, refus, codes);
330
+ for (const m of item.members ?? []) {
331
+ const km = contratDe(m);
332
+ fautesJugees(`\`${m.displayName}\``, km.fautes, fautes, refus, codes);
333
+ }
334
+ return { refus, codes };
335
+ }
336
+
337
+ // ---- The generated part of docs/INTERFACE.md ----
338
+
339
+ export const DEBUT = '<!-- généré: début -->';
340
+ export const FIN = '<!-- généré: fin -->';
341
+
342
+ // The positions of the two marks in an interface text: { etat: 'ok' | 'absentes' | 'invalides', … }.
343
+ // A mark written in a code block is an example, not a mark.
344
+ export function marques(texte) {
345
+ const lignes = texte.split('\n');
346
+ const debuts = [];
347
+ const fins = [];
348
+ let cloture = null;
349
+ lignes.forEach((l, i) => {
350
+ const f = /^\s*(```|~~~)/.exec(l);
351
+ if (cloture) {
352
+ if (f && f[1] === cloture) cloture = null;
353
+ return;
354
+ }
355
+ if (f) {
356
+ cloture = f[1];
357
+ return;
358
+ }
359
+ if (l.trim() === DEBUT) debuts.push(i);
360
+ if (l.trim() === FIN) fins.push(i);
361
+ });
362
+ if (!debuts.length && !fins.length) return { etat: 'absentes', lignes };
363
+ if (debuts.length === 1 && fins.length === 1 && debuts[0] < fins[0])
364
+ return { etat: 'ok', lignes, debut: debuts[0], fin: fins[0] };
365
+ return { etat: 'invalides', lignes };
366
+ }
367
+
368
+ // The lines of an interface text outside its generated part.
369
+ function ecrite(texte) {
370
+ const m = marques(texte);
371
+ return m.etat === 'ok' ? [...m.lignes.slice(0, m.debut), ...m.lignes.slice(m.fin + 1)] : m.lignes;
372
+ }
373
+
374
+ // The signature of an element on one line; a constant keeps its declaration (`const`, `let`).
375
+ const signature = item => {
376
+ const texte = item.getExcerptWithModifiers().replace(/\s+/g, ' ').replace(/;$/, '').trim();
377
+ if (
378
+ /^(?:(?:export|declare|default|async)\s+)*(?:function|class|interface|type|enum|namespace|const|let|var)\b/.test(
379
+ texte
380
+ )
381
+ )
382
+ return texte;
383
+ if (item.kind === 'Variable')
384
+ return `export declare ${item.isReadonly ? 'const' : 'let'} ${texte}`;
385
+ return item.kind === 'Function' ? `const ${texte}` : texte;
386
+ };
387
+
388
+ // The generated entry of one export: title, summary, signature, parameters, return, faults and,
389
+ // for an interface or a class, its members.
390
+ function entreeDe(item) {
391
+ const k = contratDe(item);
392
+ const out = [`### \`${item.displayName}\``, ''];
393
+ if (k.resume) out.push(k.resume, '');
394
+ out.push('```ts', `${signature(item)};`, '```');
395
+ const params = k.params.filter(b => b.texte);
396
+ if (params.length)
397
+ out.push('', '**Paramètres**', '', ...params.map(b => `- \`${b.nom}\` — ${b.texte}`));
398
+ if (k.retour) out.push('', `**Retour** — ${k.retour}`);
399
+ const codes = [...new Set(k.fautes.map(f => f.replace(/\s+/g, ' ')))];
400
+ if (codes.length)
401
+ out.push('', '**Fautes**', '', ...codes.map(f => `- ${f.replace(/^(\S+)/, '`$1`')}`));
402
+ const membres = [...(item.members ?? [])];
403
+ if (membres.length) {
404
+ out.push('', '**Membres**', '');
405
+ for (const m of membres) {
406
+ const resume = texteDoc(m.tsdocComment?.summarySection);
407
+ out.push(`- \`${signature(m)}\`${resume ? ` — ${resume}` : ''}`);
408
+ }
409
+ }
410
+ return out.join('\n');
411
+ }
412
+
413
+ // The text between the marks for <exports>: empty without export.
414
+ const partieGeneree = exports =>
415
+ exports.length ? `\n${exports.map(entreeDe).join('\n\n')}\n\n` : '\n';
416
+
417
+ // The text of an interface with its generated part replaced; the marks are added at the end of a
418
+ // text that has none.
419
+ function avecPartie(m, texte, partie) {
420
+ if (m.etat === 'absentes')
421
+ return `${texte}${texte.endsWith('\n') ? '' : '\n'}\n${DEBUT}${partie}${FIN}\n`;
422
+ const avant = m.lignes.slice(0, m.debut).join('\n');
423
+ const apres = m.lignes.slice(m.fin + 1).join('\n');
424
+ return `${avant}${m.debut ? '\n' : ''}${DEBUT}${partie}${FIN}\n${apres}`;
425
+ }
426
+
427
+ // The refusals and reports for one component. A refusal is a finding `{ fichier, regle, message }`
428
+ // the debt (`dette.mjs`) judges, keyed by the component's interface (by the declaration file for a
429
+ // TSDoc message, its rule TSDoc's message id); a refusal that a command or a build repairs (a
430
+ // generated part, an API report, a built entry, the marks, the TSDoc configuration) has no key and
431
+ // is never inscribed.
432
+ export function verifierComposant(racine, c, { ecrire = false } = {}) {
433
+ const refus = [];
434
+ const signaux = [];
435
+ const rel = path.relative(racine, c.dir);
436
+ const iface = path.join(c.dir, 'docs', 'INTERFACE.md');
437
+ const relIface = path.relative(racine, iface);
438
+ // A refusal of the component: inscribable when <regle> is given, keyed by <fichier>.
439
+ const refuser = (texte, regle = null, fichier = relIface, ligne = null) =>
440
+ refus.push(
441
+ regle
442
+ ? { fichier, ...(ligne ? { ligne } : {}), regle, message: `${rel} : ${texte}` }
443
+ : { message: `${rel} : ${texte}` }
444
+ );
445
+ if (!existsSync(iface)) {
446
+ signaux.push(`${rel} n'a pas d'interface (docs/INTERFACE.md) : rien n'est vérifié.`);
447
+ return { refus, signaux };
448
+ }
449
+ const texte = readFileSync(iface, 'utf8');
450
+ const { fautes } = declare(texte);
451
+ const m = marques(texte);
452
+ if (sansApi(c)) {
453
+ if (fautes) codesContreCatalogue(c, racine, fautes, refuser);
454
+ return { refus, signaux };
455
+ }
456
+ if (m.etat === 'invalides') {
457
+ refuser(
458
+ `docs/INTERFACE.md doit porter la marque « ${DEBUT} » puis la marque « ${FIN} », chacune une fois ; corrige les marques à la main (npm run api n'écrit rien).`
459
+ );
460
+ }
461
+ const fichierTsdoc = path.join(racine, 'tsdoc.json');
462
+ const dts = entree(racine, c);
463
+ if (!existsSync(fichierTsdoc)) {
464
+ refuser('la configuration TSDoc du cadre (tsdoc.json à la racine) manque.');
465
+ } else if (!dts || !existsSync(dts)) {
466
+ refuser(`son entrée construite (${dts ?? 'types absent'}) manque ; construis d'abord.`);
467
+ } else {
468
+ const { TSDocConfigFile } = require('@microsoft/tsdoc-config');
469
+ const config = TSDocConfigFile.loadFile(fichierTsdoc);
470
+ const api = rapportApi(racine, c, dts, ecrire, config);
471
+ for (const msg of api.messages) signaux.push(`${rel} : ${msg}`);
472
+ for (const t of api.tsdoc) {
473
+ refuser(`TSDoc : ${t.texte}`, t.regle, t.fichier ?? relIface, t.ligne);
474
+ }
475
+ const manque = !ecrire && !existsSync(path.join(c.dir, 'docs', 'INTERFACE.api.md'));
476
+ if (manque) {
477
+ refuser(
478
+ "son rapport docs/INTERFACE.api.md manque ; npm run api l'écrit, après la construction."
479
+ );
480
+ } 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."
483
+ );
484
+ }
485
+ const nommees = new Set();
486
+ for (const item of api.exports) {
487
+ const j = jugerExport(item, fautes);
488
+ for (const r of j.refus) refuser(r.texte, r.regle);
489
+ for (const code of j.codes) nommees.add(code);
490
+ }
491
+ for (const f of fautes ?? []) {
492
+ if (!nommees.has(f))
493
+ refuser(`la faute ${f} du catalogue n'est nommée par aucun @faute.`, 'faute-non-nommee');
494
+ }
495
+ if (m.etat !== 'invalides') {
496
+ const attendu = avecPartie(m, texte, partieGeneree(api.exports));
497
+ if (ecrire) {
498
+ if (attendu !== texte) writeFileSync(iface, attendu);
499
+ } 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.`
502
+ );
503
+ }
504
+ }
505
+ }
506
+ if (fautes) codesContreCatalogue(c, racine, fautes, refuser);
507
+ return { refus, signaux };
508
+ }
509
+
510
+ // The catalogue <fautes> against the codes of the component's production code, refused through
511
+ // <refuser>.
512
+ function codesContreCatalogue(c, racine, fautes, refuser) {
513
+ const prefixes = [...new Set([...fautes].map(f => f.slice(0, f.indexOf('_') + 1)))];
514
+ const dansLeCode = codesDuCode(c.dir, racine, prefixes);
515
+ for (const f of fautes) {
516
+ if (!dansLeCode.has(f))
517
+ refuser(`la faute ${f} est listée, mais le code ne la produit pas.`, 'faute-non-produite');
518
+ }
519
+ for (const f of dansLeCode) {
520
+ if (!fautes.has(f))
521
+ refuser(`le code produit ${f}, que « Les fautes » ne liste pas.`, 'faute-non-listee');
522
+ }
523
+ }
524
+
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.
527
+ export function aVerifier(racine, env = process.env) {
528
+ 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;
532
+ }
533
+
534
+ if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
535
+ const racine = process.cwd();
536
+ const ecrire = process.argv.includes('--ecrire');
537
+ const constats = [];
538
+ for (const c of aVerifier(racine)) {
539
+ const { refus, signaux } = verifierComposant(racine, c, { ecrire });
540
+ for (const s of signaux) process.stderr.write(`⚠️ ${s}\n`);
541
+ constats.push(...refus);
542
+ }
543
+ const { refus, lignes } = trancher(racine, 'interfaces-contre-code', constats);
544
+ for (const r of refus) process.stderr.write(`⛔ ${r.message}\n`);
545
+ for (const l of lignes) process.stderr.write(`${l}\n`);
546
+ process.exitCode = refus.length ? 1 : 0;
547
+ }
@@ -0,0 +1,110 @@
1
+ #!/bin/bash
2
+ # The default left part of the Grillhouse status line, for a person without a status line command
3
+ # of their own: the model, the context used, the OpenRouter credit (cache 30 s) or the Anthropic
4
+ # rate limits over 5 h and 7 days. Reads Claude Code's status line input on stdin.
5
+
6
+ input=$(cat)
7
+
8
+ model=$(echo "$input" | jq -r '.model.display_name // "?"')
9
+ used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
10
+ five_hour=$(echo "$input" | jq -r '.rate_limits.five_hour.used_percentage // empty')
11
+ five_reset=$(echo "$input" | jq -r '.rate_limits.five_hour.resets_at // empty')
12
+ seven_day=$(echo "$input" | jq -r '.rate_limits.seven_day.used_percentage // empty')
13
+ seven_reset=$(echo "$input" | jq -r '.rate_limits.seven_day.resets_at // empty')
14
+
15
+ # --- Model ---
16
+ printf "\033[1;36m%s\033[0m" "$model"
17
+
18
+ # --- Context ---
19
+ if [ -n "$used_pct" ]; then
20
+ ctx_int=$(printf '%.0f' "$used_pct")
21
+ if [ "$ctx_int" -ge 80 ]; then
22
+ c="\033[1;31m"
23
+ elif [ "$ctx_int" -ge 60 ]; then
24
+ c="\033[1;33m"
25
+ else
26
+ c="\033[1;32m"
27
+ fi
28
+ printf " | ctx: ${c}%s%%\033[0m" "$ctx_int"
29
+ fi
30
+
31
+ # --- OpenRouter: crédit restant (cache 30s) ---
32
+ if [ "${ANTHROPIC_BASE_URL:-}" = "https://openrouter.ai/api" ]; then
33
+ CACHE="/tmp/claude-or-credit.json"
34
+ NOW=$(date +%s)
35
+ if [ ! -f "$CACHE" ] || [ $(( NOW - $(jq -r '.ts // 0' "$CACHE" 2>/dev/null || echo 0) )) -ge 30 ]; then
36
+ KEY="${ANTHROPIC_AUTH_TOKEN:-}"
37
+ if [ -n "$KEY" ]; then
38
+ DATA=$(curl -s --max-time 3 "https://openrouter.ai/api/v1/auth/key" \
39
+ -H "Authorization: Bearer $KEY" 2>/dev/null)
40
+ if [ -n "$DATA" ]; then
41
+ USAGE=$(echo "$DATA" | jq -r '.data.usage // 0' 2>/dev/null)
42
+ LIMIT=$(echo "$DATA" | jq -r '.data.limit // 0' 2>/dev/null)
43
+ REMAIN=$(echo "$DATA" | jq -r '.data.limit_remaining // 0' 2>/dev/null)
44
+ printf '{"ts":%s,"usage":%s,"limit":%s,"remaining":%s}\n' \
45
+ "$NOW" "${USAGE:-0}" "${LIMIT:-0}" "${REMAIN:-0}" > "$CACHE" 2>/dev/null
46
+ fi
47
+ fi
48
+ fi
49
+ if [ -f "$CACHE" ]; then
50
+ USAGE=$(jq -r '.usage // 0' "$CACHE" 2>/dev/null)
51
+ LIMIT=$(jq -r '.limit // 0' "$CACHE" 2>/dev/null)
52
+ REMAINING=$(jq -r '.remaining // 0' "$CACHE" 2>/dev/null)
53
+ # Si pas de limite configurée côté OpenRouter, fallback sur fichier budget local
54
+ if [ "$LIMIT" = "0" ] || [ "$LIMIT" = "null" ]; then
55
+ FALLBACK=$(cat ~/.claude/openrouter-budget 2>/dev/null || echo 0)
56
+ LIMIT=${OPENROUTER_BUDGET:-$FALLBACK}
57
+ REMAINING=$(LC_NUMERIC=C awk "BEGIN {printf \"%.2f\", ${LIMIT} - ${USAGE}}" 2>/dev/null || echo "?")
58
+ fi
59
+ LIMIT=${LIMIT:-0}
60
+ if [ "$LIMIT" != "0" ] && [ "$LIMIT" != "null" ]; then
61
+ PCT=$(LC_NUMERIC=C awk "BEGIN {printf \"%.0f\", (${USAGE}/${LIMIT})*100}" 2>/dev/null || echo "?")
62
+ if [ "$PCT" != "?" ] && [ "$PCT" -ge 80 ]; then
63
+ or_c="\033[1;31m"
64
+ elif [ "$PCT" != "?" ] && [ "$PCT" -ge 50 ]; then
65
+ or_c="\033[1;33m"
66
+ else
67
+ or_c="\033[1;32m"
68
+ fi
69
+ printf " | OR: ${or_c}\$%s/%s\$ (%s%%)\033[0m" "$(LC_NUMERIC=C awk "BEGIN {printf \"%.2f\", ${USAGE}}" 2>/dev/null)" "$(LC_NUMERIC=C awk "BEGIN {printf \"%.0f\", ${LIMIT}}" 2>/dev/null)" "$PCT"
70
+ fi
71
+ fi
72
+ fi
73
+
74
+ # --- 5h rate limit + reset (Anthropic) ---
75
+ if [ -n "$five_hour" ]; then
76
+ pct5=$(printf '%.0f' "$five_hour")
77
+ if [ -n "$five_reset" ] && [ "$five_reset" != "null" ]; then
78
+ reset5=$(date -d "@$five_reset" +"%Hh%M" 2>/dev/null || date -r "$five_reset" +"%Hh%M" 2>/dev/null || echo "?")
79
+ else
80
+ reset5="?"
81
+ fi
82
+ if [ "$pct5" -ge 80 ]; then
83
+ c5="\033[1;31m"
84
+ elif [ "$pct5" -ge 50 ]; then
85
+ c5="\033[1;33m"
86
+ else
87
+ c5="\033[1;32m"
88
+ fi
89
+ printf " | 5h: ${c5}%s%%\033[0m \033[0;90m->%s\033[0m" "$pct5" "$reset5"
90
+ fi
91
+
92
+ # --- 7d rate limit + reset (Anthropic) ---
93
+ if [ -n "$seven_day" ]; then
94
+ pct7=$(printf '%.0f' "$seven_day")
95
+ if [ -n "$seven_reset" ] && [ "$seven_reset" != "null" ]; then
96
+ reset7=$(date -d "@$seven_reset" +"%a %Hh%M" 2>/dev/null || date -r "$seven_reset" +"%a %Hh%M" 2>/dev/null || echo "?")
97
+ else
98
+ reset7="?"
99
+ fi
100
+ if [ "$pct7" -ge 80 ]; then
101
+ c7="\033[1;31m"
102
+ elif [ "$pct7" -ge 50 ]; then
103
+ c7="\033[1;33m"
104
+ else
105
+ c7="\033[1;32m"
106
+ fi
107
+ printf " | 7d: ${c7}%s%%\033[0m \033[0;90m->%s\033[0m" "$pct7" "$reset7"
108
+ fi
109
+
110
+ printf "\n"