plugin-eco 0.1.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Vincent Guiberteau
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # plugin-eco
2
+
3
+ Extension VSCode qui note la sobriété énergétique du code **Java, JavaScript et
4
+ TypeScript**, avec une étiquette **A → E** empruntée au DPE des logements.
5
+
6
+ L'analyse est statique et locale : le fichier est parsé à la frappe, les
7
+ patterns énergivores connus sont signalés en annotations inline, et le score du
8
+ fichier s'affiche dans la barre de statut.
9
+
10
+ ```
11
+ ┌─ éditeur ────────────────────────┐ ┌─ panneau ───────────────┐
12
+ │ for (int i..) { │ │ │
13
+ │ for (int j..) { <- imbriquée │ │ ┌───┐ │
14
+ │ s += row[j]; <- String += │ │ │ E │ 28/100 │
15
+ │ } │ │ └───┘ │
16
+ │ } │ │ 4 hautes, 3 moyennes │
17
+ │ │ │ détail par ligne... │
18
+ └──────────────────────────────────┘ └─────────────────────────┘
19
+ barre de statut : Éco: E
20
+ ```
21
+
22
+ ## Utilisation
23
+
24
+ Ouvrir un fichier `.java` : l'analyse démarre seule. Trois commandes dans la
25
+ palette (`Ctrl+Shift+P`) :
26
+
27
+ | Commande | Effet |
28
+ |---|---|
29
+ | `Greencoding: Analyser le fichier` | relance l'analyse du fichier actif |
30
+ | `Greencoding: Ouvrir le rapport détaillé` | ouvre le panneau, score + détail par ligne |
31
+ | `Greencoding: Analyser tout le workspace` | scanne tous les `.java`, classe les fichiers du pire au meilleur |
32
+
33
+ ## Ce qui est détecté
34
+
35
+ **Partout**, quel que soit le langage et l'endroit où le code s'exécute :
36
+
37
+ | Pattern | Sévérité | Pénalité |
38
+ |---|---|---|
39
+ | Boucle imbriquée (complexité ≥ O(n²)) | haute | 15 |
40
+ | Requête SQL sans `LIMIT` | haute | 12 |
41
+ | `await` dans une boucle (appels enchaînés) | haute | 12 |
42
+ | Regex recompilée en boucle | haute | 12 |
43
+
44
+ **Côté serveur** — le coût est payé une fois, par le processus :
45
+
46
+ | Pattern | Sévérité | Pénalité |
47
+ |---|---|---|
48
+ | I/O synchrone dans une fonction (`readFileSync`, `execSync`…) | haute | 12 |
49
+ | I/O bloquant en boucle *(Java)* | haute | 12 |
50
+
51
+ **Côté navigateur** — le coût est payé par l'appareil de chaque visiteur :
52
+
53
+ | Pattern | Sévérité | Pénalité |
54
+ |---|---|---|
55
+ | `setInterval` de moins d'une seconde | haute | 12 |
56
+ | `setInterval` plus espacé | moyenne | 7 |
57
+ | Gestionnaire `scroll`/`resize`/`mousemove` sans limitation de débit | moyenne | 7 |
58
+ | Import global d'une bibliothèque lourde (`lodash`, `moment`…) | moyenne | 7 |
59
+
60
+ **Java seulement** : concaténation `+=` en boucle et `new` en boucle (moyenne, 7
61
+ chacune). Ces deux règles ne s'appliquent **pas** à JavaScript, et c'est
62
+ délibéré : V8 représente les concaténations par des *ropes*, et son
63
+ ramasse-miettes générationnel rend l'allocation à courte durée de vie bon
64
+ marché. Les signaler reviendrait à crier au loup sur du code sain.
65
+
66
+ Une règle restreinte à un côté ne se déclenche **jamais** sur un fichier dont le
67
+ contexte est indéterminé : sans certitude, le plugin se tait.
68
+
69
+ Le score part de 100, chaque détection retranche sa pénalité, et le reste donne
70
+ la lettre : **A** ≥ 90, **B** ≥ 75, **C** ≥ 55, **D** ≥ 35, **E** en dessous.
71
+
72
+ Sur un scan de projet, la note globale est la moyenne des **seuls fichiers qui
73
+ présentent au moins une alerte**. Les fichiers sains n'y entrent pas : sans
74
+ cela, une poignée de modules utilitaires vides suffisait à ramener un projet à
75
+ **A** en noyant le fichier qui pose réellement problème. Le nombre de fichiers
76
+ concernés est affiché à côté de la lettre, et le pire d'entre eux est mis en
77
+ avant — une lettre unique ne peut pas désigner un endroit.
78
+
79
+ ## Développement
80
+
81
+ ```bash
82
+ npm install
83
+ npm test
84
+ ```
85
+
86
+ Puis `F5` dans VSCode pour lancer une fenêtre de test, et ouvrir un des exemples
87
+ de `samples/` : `Example.java` déclenche les six règles Java, `example.ts` montre
88
+ ce qui s'applique hors contexte connu, `example-web.tsx` déclenche les règles
89
+ navigateur.
90
+
91
+ Les tests utilisent `node:test`, sans dépendance supplémentaire, et s'exécutent
92
+ sur le code compilé — donc sur ce qui part réellement dans l'extension. Ils
93
+ couvrent chaque règle (déclenchement et non-déclenchement), les seuils de
94
+ l'étiquette, et le résultat attendu sur `samples/Example.java`.
95
+
96
+ Pour produire l'extension installable :
97
+
98
+ ```bash
99
+ npm run package
100
+ ```
101
+
102
+ ## En intégration continue
103
+
104
+ Le même moteur s'utilise en ligne de commande, pour bloquer une pipeline sous un
105
+ seuil :
106
+
107
+ ```bash
108
+ npx plugin-eco --min C src/
109
+ ```
110
+
111
+ | | |
112
+ |---|---|
113
+ | `--format <text\|json>` | sortie lisible ou exploitable par un script |
114
+ | `--min <A..E>` | note minimale acceptée |
115
+ | code de sortie | `0` conforme · `1` sous le seuil · `2` erreur d'utilisation |
116
+
117
+ Les positions sont rendues au format `fichier:ligne:colonne`, reconnu par la
118
+ plupart des annotateurs de CI et cliquable dans un terminal.
119
+
120
+ ```yaml
121
+ - name: Green check
122
+ run: npx plugin-eco --min C src/
123
+ ```
124
+
125
+ L'analyse est strictement la même que dans l'éditeur : le CLI et l'extension
126
+ partagent le parseur, les règles, l'inférence de contexte et le calcul de score.
127
+ Un verdict qui différerait entre l'IDE et la pipeline ruinerait la confiance dans
128
+ les deux.
129
+
130
+ Le moteur s'utilise aussi comme bibliothèque, pour bâtir un rapport sur mesure :
131
+
132
+ ```js
133
+ const { initParser, parse, collectFindings, computeScore, specFor } = require('plugin-eco');
134
+
135
+ await initParser();
136
+ const findings = collectFindings(parse(source, 'java').rootNode, specFor('java'));
137
+ console.log(computeScore(findings).letter); // 'A' … 'E'
138
+ ```
139
+
140
+ Ce point d'entrée n'expose que le moteur : le code d'intégration VSCode en est
141
+ absent, et rien n'y importe `vscode`.
142
+
143
+ ## Choix techniques
144
+
145
+ **Analyse statique d'abord, mesure à l'exécution ensuite.** Un profileur donne
146
+ des watts réels mais impose de compiler et d'exécuter le code ; l'analyse
147
+ statique tient sous la seconde et tourne pendant la frappe. C'est la condition
148
+ pour que l'information arrive au moment où le développeur peut encore agir.
149
+
150
+ **Une lettre plutôt que des watt-heures.** Une estimation en Wh sur du code non
151
+ exécuté serait une fausse précision. La lettre assume ce qu'elle est — un
152
+ classement relatif — et parle immédiatement. Les Wh et le CO₂ viendront avec la
153
+ mesure à l'exécution, où ils seront mesurés plutôt que devinés.
154
+
155
+ **tree-sitter plutôt qu'une analyse par expressions régulières.** Distinguer une
156
+ boucle imbriquée d'une boucle voisine, ou un `new` dans une boucle d'un `new`
157
+ juste après, demande un arbre syntaxique. tree-sitter le fournit pour de
158
+ nombreux langages avec un seul parseur — Python reste à ajouter.
159
+
160
+ **Un descripteur de langage, pas des conditions dispersées.** Tout ce qui varie
161
+ d'un langage à l'autre — grammaire, noms de nœuds tree-sitter, règles
162
+ applicables — est déclaré dans `src/languages.ts`. Les autres modules n'y font
163
+ aucune référence. Ajouter Python revient à ajouter une entrée.
164
+
165
+ **Le contexte d'exécution se déduit du code, pas des chemins.** Un `setInterval`
166
+ de *polling* coûte une fois sur un serveur et autant de fois qu'il y a de
167
+ visiteurs dans un navigateur : le plugin doit savoir où tourne le fichier. Il le
168
+ lit dans l'arbre déjà parsé — imports de modules Node d'un côté, globales du
169
+ navigateur et JSX de l'autre — plutôt que d'imposer une convention de dossiers,
170
+ qui diffère à chaque framework. Seuls les indices francs comptent, et des
171
+ indices contradictoires donnent « indéterminé » : en rendu côté serveur, un
172
+ fichier tourne réellement des deux côtés.
173
+
174
+ **Deux grammaires pour JS/TS, pas trois.** `tsx` est un sur-ensemble de
175
+ `javascript` et couvre `.js`, `.jsx` et `.tsx`. Mais elle ne peut pas remplacer
176
+ `typescript` pour les `.ts` : elle lit l'assertion `<Type>valeur` comme une
177
+ ouverture JSX et perd la suite du fichier. Mesuré plutôt que supposé — un `.ts`
178
+ contenant une telle assertion voyait ses trois boucles disparaître.
179
+
180
+ **Les grammaires WASM sont copiées dans `out/` au build.** Une extension
181
+ installée n'a pas les `node_modules` de développement sous la main : le script
182
+ `scripts/copy-wasm.js` place le runtime tree-sitter et la grammaire Java dans
183
+ `out/wasm/`, que l'extension résout depuis sa propre racine.
184
+
185
+ ## État
186
+
187
+ Analyse statique de Java, JavaScript et TypeScript, avec un jeu de règles web
188
+ qui distingue le code serveur du code navigateur. Python, la mesure à
189
+ l'exécution (Wh et CO₂) et un portage IntelliJ sont les étapes suivantes.
190
+
191
+ ## Licence
192
+
193
+ MIT — voir [LICENSE](LICENSE).
package/out/cli.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Point d'entrée en ligne de commande — le « green-check » de pipeline.
4
+ *
5
+ * Ce module n'importe **jamais** `vscode`, ni `extension.ts`, ni `webview.ts` :
6
+ * il partage avec l'extension le parseur, les règles, l'inférence de contexte
7
+ * et le calcul de score, rien d'autre. C'est cette frontière qui garantit que
8
+ * l'IDE et la CI rendent le même verdict sur le même fichier — deux moteurs
9
+ * divergents seraient pires que pas de CI du tout.
10
+ */
11
+ import { Score, WorkspaceReport } from './types';
12
+ export interface CliOptions {
13
+ paths: string[];
14
+ format: 'text' | 'json';
15
+ /** Note minimale acceptée ; en dessous, le processus sort en échec. */
16
+ min?: Score['letter'];
17
+ help: boolean;
18
+ }
19
+ export declare const USAGE: string;
20
+ export declare function parseArgs(argv: string[]): CliOptions;
21
+ /** La note atteint-elle le seuil ? 'A' est la meilleure. */
22
+ export declare function meetsThreshold(letter: Score['letter'], min: Score['letter']): boolean;
23
+ /** Parcourt récursivement un chemin et retourne les fichiers analysables. */
24
+ export declare function collectPaths(target: string): string[];
25
+ export declare function analyze(files: string[], baseDir: string): Promise<WorkspaceReport>;
26
+ export declare function renderText(report: WorkspaceReport): string;
27
+ export declare function renderJson(report: WorkspaceReport): string;
28
+ export declare function main(argv: string[]): Promise<number>;
package/out/cli.js ADDED
@@ -0,0 +1,254 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ /**
4
+ * Point d'entrée en ligne de commande — le « green-check » de pipeline.
5
+ *
6
+ * Ce module n'importe **jamais** `vscode`, ni `extension.ts`, ni `webview.ts` :
7
+ * il partage avec l'extension le parseur, les règles, l'inférence de contexte
8
+ * et le calcul de score, rien d'autre. C'est cette frontière qui garantit que
9
+ * l'IDE et la CI rendent le même verdict sur le même fichier — deux moteurs
10
+ * divergents seraient pires que pas de CI du tout.
11
+ */
12
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
13
+ if (k2 === undefined) k2 = k;
14
+ var desc = Object.getOwnPropertyDescriptor(m, k);
15
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
16
+ desc = { enumerable: true, get: function() { return m[k]; } };
17
+ }
18
+ Object.defineProperty(o, k2, desc);
19
+ }) : (function(o, m, k, k2) {
20
+ if (k2 === undefined) k2 = k;
21
+ o[k2] = m[k];
22
+ }));
23
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
24
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
25
+ }) : function(o, v) {
26
+ o["default"] = v;
27
+ });
28
+ var __importStar = (this && this.__importStar) || (function () {
29
+ var ownKeys = function(o) {
30
+ ownKeys = Object.getOwnPropertyNames || function (o) {
31
+ var ar = [];
32
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
33
+ return ar;
34
+ };
35
+ return ownKeys(o);
36
+ };
37
+ return function (mod) {
38
+ if (mod && mod.__esModule) return mod;
39
+ var result = {};
40
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
41
+ __setModuleDefault(result, mod);
42
+ return result;
43
+ };
44
+ })();
45
+ Object.defineProperty(exports, "__esModule", { value: true });
46
+ exports.USAGE = void 0;
47
+ exports.parseArgs = parseArgs;
48
+ exports.meetsThreshold = meetsThreshold;
49
+ exports.collectPaths = collectPaths;
50
+ exports.analyze = analyze;
51
+ exports.renderText = renderText;
52
+ exports.renderJson = renderJson;
53
+ exports.main = main;
54
+ const fs = __importStar(require("fs"));
55
+ const path = __importStar(require("path"));
56
+ const parser_1 = require("./parser");
57
+ const rules_1 = require("./rules");
58
+ const scoring_1 = require("./scoring");
59
+ const context_1 = require("./context");
60
+ const languages_1 = require("./languages");
61
+ const LETTERS = ['A', 'B', 'C', 'D', 'E'];
62
+ exports.USAGE = `Usage : plugin-eco [options] [chemins...]
63
+
64
+ Analyse la consommation énergétique estimée du code Java, JavaScript et
65
+ TypeScript, et rend une étiquette A–E inspirée du DPE.
66
+
67
+ Options
68
+ --format <text|json> Format de sortie (défaut : text)
69
+ --min <A|B|C|D|E> Note minimale acceptée ; en dessous, sortie en échec
70
+ -h, --help Affiche cette aide
71
+
72
+ Chemins
73
+ Fichiers ou dossiers à analyser. Défaut : le dossier courant.
74
+ Ignorés : ${languages_1.EXCLUDED_DIRS.join(', ')}
75
+
76
+ Codes de sortie
77
+ 0 analyse effectuée, note au-dessus du seuil (ou aucun seuil demandé)
78
+ 1 note en dessous du seuil passé à --min
79
+ 2 erreur d'utilisation ou d'exécution
80
+
81
+ Exemple
82
+ plugin-eco --min C --format json src/ > rapport.json`;
83
+ // ---------------------------------------------------------------------------
84
+ // Analyse des arguments
85
+ // ---------------------------------------------------------------------------
86
+ function parseArgs(argv) {
87
+ const options = { paths: [], format: 'text', help: false };
88
+ for (let i = 0; i < argv.length; i++) {
89
+ const arg = argv[i];
90
+ if (arg === '-h' || arg === '--help') {
91
+ options.help = true;
92
+ }
93
+ else if (arg === '--format') {
94
+ const value = argv[++i];
95
+ if (value !== 'text' && value !== 'json') {
96
+ throw new Error(`Format inconnu : ${value ?? '(manquant)'}. Attendu : text ou json.`);
97
+ }
98
+ options.format = value;
99
+ }
100
+ else if (arg === '--min') {
101
+ const value = (argv[++i] ?? '').toUpperCase();
102
+ if (!LETTERS.includes(value)) {
103
+ throw new Error(`Note minimale invalide : ${argv[i] ?? '(manquante)'}. Attendu : A, B, C, D ou E.`);
104
+ }
105
+ options.min = value;
106
+ }
107
+ else if (arg.startsWith('-')) {
108
+ throw new Error(`Option inconnue : ${arg}`);
109
+ }
110
+ else {
111
+ options.paths.push(arg);
112
+ }
113
+ }
114
+ if (options.paths.length === 0)
115
+ options.paths.push('.');
116
+ return options;
117
+ }
118
+ /** La note atteint-elle le seuil ? 'A' est la meilleure. */
119
+ function meetsThreshold(letter, min) {
120
+ return LETTERS.indexOf(letter) <= LETTERS.indexOf(min);
121
+ }
122
+ // ---------------------------------------------------------------------------
123
+ // Découverte des fichiers
124
+ // ---------------------------------------------------------------------------
125
+ /** Parcourt récursivement un chemin et retourne les fichiers analysables. */
126
+ function collectPaths(target) {
127
+ let stat;
128
+ try {
129
+ stat = fs.statSync(target);
130
+ }
131
+ catch {
132
+ throw new Error(`Chemin introuvable : ${target}`);
133
+ }
134
+ if (stat.isFile())
135
+ return (0, languages_1.specForPath)(target) ? [target] : [];
136
+ return fs.readdirSync(target, { withFileTypes: true }).flatMap(entry => {
137
+ if (entry.isDirectory()) {
138
+ return languages_1.EXCLUDED_DIRS.includes(entry.name)
139
+ ? []
140
+ : collectPaths(path.join(target, entry.name));
141
+ }
142
+ const full = path.join(target, entry.name);
143
+ return (0, languages_1.specForPath)(full) ? [full] : [];
144
+ });
145
+ }
146
+ // ---------------------------------------------------------------------------
147
+ // Analyse
148
+ // ---------------------------------------------------------------------------
149
+ async function analyze(files, baseDir) {
150
+ // La racine de l'extension contient out/wasm/, où vivent les grammaires.
151
+ await (0, parser_1.initParser)(path.join(__dirname, '..'));
152
+ const results = [];
153
+ for (const file of files) {
154
+ const spec = (0, languages_1.specForPath)(file);
155
+ if (!spec)
156
+ continue;
157
+ const code = fs.readFileSync(file, 'utf8');
158
+ const tree = (0, parser_1.parseWith)(code, spec);
159
+ const { context } = (0, context_1.inferContext)(tree.rootNode, spec);
160
+ const findings = (0, rules_1.collectFindings)(tree.rootNode, spec, context);
161
+ results.push({
162
+ uri: file,
163
+ fileName: path.relative(baseDir, file).split(path.sep).join('/') || path.basename(file),
164
+ score: (0, scoring_1.computeScore)(findings),
165
+ findings,
166
+ });
167
+ }
168
+ results.sort((a, b) => a.score.value - b.score.value);
169
+ return {
170
+ files: results,
171
+ global: (0, scoring_1.aggregateScore)(results.map(r => r.score)),
172
+ filesWithFindings: results.filter(r => r.findings.length > 0).length,
173
+ scannedAt: new Date().toISOString(),
174
+ };
175
+ }
176
+ // ---------------------------------------------------------------------------
177
+ // Rendu
178
+ // ---------------------------------------------------------------------------
179
+ function renderText(report) {
180
+ const { global, files, filesWithFindings } = report;
181
+ const lines = [];
182
+ lines.push(`Éco : ${global.letter} ${global.value}/100`);
183
+ lines.push(`${global.findingCount.high} alerte(s) haute(s), ${global.findingCount.medium} moyenne(s) — ` +
184
+ `${filesWithFindings} fichier(s) concerné(s) sur ${files.length} analysé(s)`);
185
+ const concerned = files.filter(f => f.findings.length > 0);
186
+ if (concerned.length === 0) {
187
+ lines.push('\nAucune alerte.');
188
+ return lines.join('\n');
189
+ }
190
+ lines.push('\nFichiers, du pire au meilleur :');
191
+ for (const file of concerned) {
192
+ lines.push(`\n ${file.score.letter} ${String(file.score.value).padStart(3)} ${file.fileName}`);
193
+ for (const finding of file.findings) {
194
+ // Format « fichier:ligne:colonne », cliquable dans la plupart des terminaux
195
+ // et reconnu par les annotateurs de CI.
196
+ lines.push(` ${file.fileName}:${finding.startLine + 1}:${finding.startChar + 1} ` +
197
+ `[${finding.severity}] ${finding.message.split(' — ')[0]}`);
198
+ }
199
+ }
200
+ return lines.join('\n');
201
+ }
202
+ function renderJson(report) {
203
+ return JSON.stringify(report, null, 2);
204
+ }
205
+ // ---------------------------------------------------------------------------
206
+ // Point d'entrée
207
+ // ---------------------------------------------------------------------------
208
+ async function main(argv) {
209
+ let options;
210
+ try {
211
+ options = parseArgs(argv);
212
+ }
213
+ catch (err) {
214
+ process.stderr.write(`${err.message}\n\n${exports.USAGE}\n`);
215
+ return 2;
216
+ }
217
+ if (options.help) {
218
+ process.stdout.write(`${exports.USAGE}\n`);
219
+ return 0;
220
+ }
221
+ let files;
222
+ try {
223
+ files = options.paths.flatMap(collectPaths);
224
+ }
225
+ catch (err) {
226
+ process.stderr.write(`${err.message}\n`);
227
+ return 2;
228
+ }
229
+ if (files.length === 0) {
230
+ // Sortie en succès : il n'y a rien à reprocher. Mais l'avertissement part
231
+ // sur stderr, car en CI c'est presque toujours une erreur de configuration.
232
+ process.stderr.write('Aucun fichier analysable trouvé.\n');
233
+ return 0;
234
+ }
235
+ let report;
236
+ try {
237
+ report = await analyze(files, process.cwd());
238
+ }
239
+ catch (err) {
240
+ process.stderr.write(`Échec de l'analyse : ${err.message}\n`);
241
+ return 2;
242
+ }
243
+ process.stdout.write((options.format === 'json' ? renderJson(report) : renderText(report)) + '\n');
244
+ if (options.min && !meetsThreshold(report.global.letter, options.min)) {
245
+ process.stderr.write(`\nNote ${report.global.letter} en dessous du seuil ${options.min}.\n`);
246
+ return 1;
247
+ }
248
+ return 0;
249
+ }
250
+ // Exécution directe uniquement : l'import depuis les tests ne déclenche rien.
251
+ if (require.main === module) {
252
+ main(process.argv.slice(2)).then(code => { process.exitCode = code; }, err => { process.stderr.write(`Erreur inattendue : ${err}\n`); process.exitCode = 2; });
253
+ }
254
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1,31 @@
1
+ import Parser from 'web-tree-sitter';
2
+ import { ExecutionContext } from './types';
3
+ import { LanguageSpec } from './languages';
4
+ /**
5
+ * Inférence du contexte d'exécution d'un fichier JavaScript / TypeScript.
6
+ *
7
+ * Le code se dénonce lui-même : un fichier qui importe `fs` tourne sur un
8
+ * serveur, un fichier qui touche `document` tourne dans un navigateur. On lit
9
+ * ces indices dans l'arbre déjà parsé — pas de convention de chemins, pas de
10
+ * configuration, pas de second moteur d'analyse.
11
+ *
12
+ * Deux principes :
13
+ *
14
+ * - **Seuls les indices francs comptent.** `process.env` est injecté par les
15
+ * bundlers côté client, `Buffer` est polyfillé : ils ne prouvent rien et sont
16
+ * ignorés. Mieux vaut répondre « je ne sais pas » que se tromper.
17
+ * - **Un conflit donne `unknown`.** Un composant qui lit `fs` *et* rend du JSX
18
+ * tourne des deux côtés (rendu côté serveur). C'est un fait, pas une
19
+ * ambiguïté à trancher au hasard.
20
+ */
21
+ export interface ContextResult {
22
+ context: ExecutionContext;
23
+ /** Indices relevés, pour les tests et l'affichage. */
24
+ clientSignals: string[];
25
+ serverSignals: string[];
26
+ }
27
+ /**
28
+ * @param root racine de l'arbre syntaxique
29
+ * @param spec descripteur du langage ; un `fixedContext` court-circuite l'analyse
30
+ */
31
+ export declare function inferContext(root: Parser.SyntaxNode, spec: LanguageSpec): ContextResult;