fad-checker 2.0.0 → 2.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.
Files changed (139) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +147 -25
  3. package/completions/fad-checker.bash +1 -1
  4. package/completions/fad-checker.zsh +14 -1
  5. package/data/eol-mapping.json +5 -5
  6. package/data/license-policy.json +97 -0
  7. package/fad-checker-report/cve-report.doc +630 -0
  8. package/fad-checker-report/cve-report.html +748 -0
  9. package/fad-checker.js +556 -245
  10. package/lib/cache-archive.js +3 -0
  11. package/lib/codecs/codec.interface.js +3 -0
  12. package/lib/codecs/composer/parse.js +3 -0
  13. package/lib/codecs/composer/registry.js +13 -5
  14. package/lib/codecs/composer.codec.js +3 -0
  15. package/lib/codecs/go/parse.js +65 -0
  16. package/lib/codecs/go/registry.js +74 -0
  17. package/lib/codecs/go.codec.js +76 -0
  18. package/lib/codecs/index.js +7 -2
  19. package/lib/codecs/maven/jar-scan.js +199 -0
  20. package/lib/codecs/maven.codec.js +17 -1
  21. package/lib/codecs/npm/collect.js +37 -17
  22. package/lib/codecs/npm/parse.js +117 -10
  23. package/lib/codecs/npm/registry.js +14 -5
  24. package/lib/codecs/npm.codec.js +4 -1
  25. package/lib/codecs/nuget/parse.js +3 -0
  26. package/lib/codecs/nuget/registry.js +11 -4
  27. package/lib/codecs/nuget.codec.js +7 -2
  28. package/lib/codecs/pypi/parse.js +111 -14
  29. package/lib/codecs/pypi/registry.js +24 -5
  30. package/lib/codecs/pypi.codec.js +30 -6
  31. package/lib/codecs/recipes.js +28 -1
  32. package/lib/codecs/ruby/parse.js +42 -0
  33. package/lib/codecs/ruby/registry.js +76 -0
  34. package/lib/codecs/ruby.codec.js +66 -0
  35. package/lib/codecs/select.js +3 -0
  36. package/lib/codecs/yarn.codec.js +3 -0
  37. package/lib/config.js +3 -0
  38. package/lib/core.js +38 -1
  39. package/lib/cpe.js +30 -5
  40. package/lib/csaf-export.js +159 -0
  41. package/lib/cve-download.js +3 -0
  42. package/lib/cve-match.js +27 -6
  43. package/lib/cve-report.js +157 -28
  44. package/lib/dep-record.js +15 -2
  45. package/lib/deps-descriptor.js +113 -0
  46. package/lib/epss.js +115 -0
  47. package/lib/gate.js +45 -0
  48. package/lib/json-export.js +110 -0
  49. package/lib/kev.js +88 -0
  50. package/lib/license-policy.js +110 -0
  51. package/lib/maven-license.js +52 -0
  52. package/lib/maven-repo.js +3 -0
  53. package/lib/maven-version.js +8 -3
  54. package/lib/nvd.js +17 -6
  55. package/lib/osv.js +75 -23
  56. package/lib/outdated.js +10 -2
  57. package/lib/priority.js +90 -0
  58. package/lib/purl.js +77 -0
  59. package/lib/retire.js +80 -13
  60. package/lib/sarif-export.js +134 -0
  61. package/lib/sbom-export.js +153 -0
  62. package/lib/scan-completeness.js +3 -0
  63. package/lib/snyk.js +3 -0
  64. package/lib/suppress.js +113 -0
  65. package/lib/transitive.js +6 -3
  66. package/lib/ui.js +90 -0
  67. package/package.json +49 -2
  68. package/CLAUDE.md +0 -147
  69. package/docs/ARCHITECTURE.md +0 -182
  70. package/docs/USAGE.md +0 -187
  71. package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
  72. package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
  73. package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
  74. package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
  75. package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
  76. package/test/cli-ecosystem.test.js +0 -30
  77. package/test/codec-edge-cases.test.js +0 -131
  78. package/test/codec-integration.test.js +0 -119
  79. package/test/codecs.test.js +0 -92
  80. package/test/composer.test.js +0 -71
  81. package/test/core.test.js +0 -153
  82. package/test/cpe.test.js +0 -166
  83. package/test/cve-download.test.js +0 -39
  84. package/test/cve-match.test.js +0 -217
  85. package/test/cve-report.test.js +0 -180
  86. package/test/dep-record.test.js +0 -31
  87. package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
  88. package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
  89. package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
  90. package/test/fixtures/complex-enterprise/pom.xml +0 -66
  91. package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
  92. package/test/fixtures/csharp-config/packages.config +0 -4
  93. package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
  94. package/test/fixtures/csharp-csproj/app.csproj +0 -7
  95. package/test/fixtures/csharp-lock/packages.lock.json +0 -6
  96. package/test/fixtures/cve-samples/cve-non-java.json +0 -19
  97. package/test/fixtures/cve-samples/cve-product-only.json +0 -31
  98. package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
  99. package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
  100. package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
  101. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
  102. package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
  103. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
  104. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
  105. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
  106. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
  107. package/test/fixtures/monorepo-mixed/pom.xml +0 -29
  108. package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
  109. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
  110. package/test/fixtures/php-app/composer.json +0 -5
  111. package/test/fixtures/php-app/composer.lock +0 -10
  112. package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
  113. package/test/fixtures/polyglot/js/package-lock.json +0 -1
  114. package/test/fixtures/polyglot/js/package.json +0 -1
  115. package/test/fixtures/polyglot/mvn/pom.xml +0 -7
  116. package/test/fixtures/polyglot/php/composer.lock +0 -1
  117. package/test/fixtures/polyglot/py/poetry.lock +0 -3
  118. package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
  119. package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
  120. package/test/fixtures/private-lib-detection/pom.xml +0 -35
  121. package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
  122. package/test/fixtures/python-poetry/poetry.lock +0 -7
  123. package/test/fixtures/python-reqs/requirements.txt +0 -5
  124. package/test/fixtures/python-uv/uv.lock +0 -3
  125. package/test/fixtures/simple/app/pom.xml +0 -28
  126. package/test/fixtures/simple/lib/pom.xml +0 -18
  127. package/test/fixtures/simple/pom.xml +0 -24
  128. package/test/maven-repo.test.js +0 -111
  129. package/test/maven-version.test.js +0 -57
  130. package/test/monorepo.test.js +0 -132
  131. package/test/npm-registry.test.js +0 -64
  132. package/test/npm.test.js +0 -150
  133. package/test/nuget.test.js +0 -66
  134. package/test/osv.test.js +0 -62
  135. package/test/outdated.test.js +0 -101
  136. package/test/pypi.test.js +0 -72
  137. package/test/snyk.test.js +0 -64
  138. package/test/transitive.test.js +0 -305
  139. package/test/webjar.test.js +0 -33
@@ -1,851 +0,0 @@
1
- # Plan A — Socle codec (foundation) Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
-
5
- **Goal:** Extraire toute la logique spécifique-écosystème (Maven + npm/yarn) derrière une interface codec uniforme, généraliser le `depRecord`, et rendre OSV/NVD/CPE/EOL agnostiques — sans changer le comportement observable (sauf le fallback npm assumé), les 96 tests restant verts.
6
-
7
- **Architecture:** Un registre de codecs (`lib/codecs/`) expose un codec par écosystème. Chaque codec implémente une interface commune (`detect`, `collect`, `coordKey`, `formatCoord`, `osvPackageName`, `checkRegistry`, `resolveEolProduct`, `recipe`, `nativeScanners`). L'orchestrateur de `fad-checker.js` boucle sur les codecs détectés au lieu de brancher `if (runMaven)/if (runNpm)`. La logique métier éprouvée (résolution POM/BOM de `lib/core.js`, walker `lib/transitive.js`, parsers `lib/npm/*`) est **déplacée, pas réécrite**.
8
-
9
- **Tech Stack:** Node.js, `node --test`, commander, xml2js. Aucune nouvelle dépendance dans ce plan (smol-toml arrive au Plan C).
10
-
11
- ---
12
-
13
- ## File Structure
14
-
15
- | Fichier | Rôle | Action |
16
- |---|---|---|
17
- | `lib/codecs/codec.interface.js` | Doc du contrat + `assertCodecShape()` (validation) | Créer |
18
- | `lib/codecs/index.js` | Registre : `getCodec`, `allCodecs`, `detectCodecs` | Créer |
19
- | `lib/codecs/maven.codec.js` | Codec Maven (enveloppe `core`, `transitive`, CVE-index) | Créer |
20
- | `lib/codecs/npm.codec.js` | Codec npm (enveloppe `lib/npm/*`, retire.js) | Créer |
21
- | `lib/codecs/yarn.codec.js` | Codec yarn (partage le gros de npm, `ecosystemType="yarn"`) | Créer |
22
- | `lib/dep-record.js` | Builder `makeDepRecord()` + helpers `coordKeyFor`/`namespaceName` | Créer |
23
- | `lib/cve-match.js` | `collectResolvedDeps` produit le depRecord généralisé | Modifier |
24
- | `lib/npm/collect.js` | idem côté npm | Modifier |
25
- | `lib/osv.js` | `osvPkgName`/`queryBatch` délèguent au codec | Modifier |
26
- | `lib/cve-report.js` | `RECIPE_SPECS`/`ECO_LABELS`/`formatCoord` pilotés par codec | Modifier |
27
- | `fad-checker.js` | Orchestration par boucle de codecs ; CLI `--ecosystem` liste + `--no-<id>` | Modifier |
28
- | `test/codecs.test.js` | Contrat d'interface paramétré sur `allCodecs()` | Créer |
29
- | `test/dep-record.test.js` | Builder depRecord | Créer |
30
-
31
- **Invariant transversal :** `npm test` (96 tests) doit rester vert après chaque tâche (sauf Task 9 qui adapte volontairement le test `no-lockfile`).
32
-
33
- ---
34
-
35
- ### Task 1: Builder du depRecord généralisé
36
-
37
- **Files:**
38
- - Create: `lib/dep-record.js`
39
- - Test: `test/dep-record.test.js`
40
-
41
- - [ ] **Step 1: Écrire le test qui échoue**
42
-
43
- ```js
44
- // test/dep-record.test.js
45
- const test = require("node:test");
46
- const assert = require("node:assert");
47
- const { makeDepRecord, coordKeyFor } = require("../lib/dep-record");
48
-
49
- test("maven depRecord builds namespaced coordKey and keeps groupId/artifactId aliases", () => {
50
- const d = makeDepRecord({ ecosystem: "maven", namespace: "org.apache", name: "log4j", version: "2.14.0", manifestPath: "/p/pom.xml", scope: "compile" });
51
- // DÉVIATION ACTÉE : clé Maven BRUTE "g:a" (pas "maven:g:a") — garde transitive.js
52
- // et ~12 tests existants intacts ; collision-free face aux préfixes des autres eco.
53
- assert.strictEqual(d.coordKey, "org.apache:log4j");
54
- assert.strictEqual(d.groupId, "org.apache"); // alias rétro-compat
55
- assert.strictEqual(d.artifactId, "log4j"); // alias rétro-compat
56
- assert.deepStrictEqual(d.versions, ["2.14.0"]);
57
- assert.strictEqual(d.isDev, false);
58
- });
59
-
60
- test("npm depRecord has empty namespace and npm-prefixed coordKey", () => {
61
- const d = makeDepRecord({ ecosystem: "npm", namespace: "", name: "lodash", version: "4.17.20", manifestPath: "/p/package-lock.json", scope: "prod" });
62
- assert.strictEqual(d.coordKey, "npm:lodash");
63
- assert.strictEqual(d.groupId, "");
64
- assert.strictEqual(d.artifactId, "lodash");
65
- });
66
-
67
- test("coordKeyFor composes ecosystem + namespace + name", () => {
68
- assert.strictEqual(coordKeyFor("composer", "guzzlehttp", "guzzle"), "composer:guzzlehttp/guzzle");
69
- assert.strictEqual(coordKeyFor("pypi", "", "requests"), "pypi:requests");
70
- assert.strictEqual(coordKeyFor("nuget", "", "Newtonsoft.Json"), "nuget:newtonsoft.json");
71
- });
72
- ```
73
-
74
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
75
-
76
- Run: `node --test test/dep-record.test.js`
77
- Expected: FAIL — `Cannot find module '../lib/dep-record'`
78
-
79
- - [ ] **Step 3: Implémenter le builder**
80
-
81
- ```js
82
- // lib/dep-record.js
83
- // Contrat de données unifié partagé par tous les codecs.
84
- // `coordKey` est la clé de la Map résolue ; elle ne collisionne jamais entre
85
- // écosystèmes grâce au préfixe `ecosystem:`. `groupId`/`artifactId` sont
86
- // conservés comme alias rétro-compat le temps de migrer tous les consommateurs.
87
-
88
- // Séparateur namespace↔name : ":" pour maven (g:a), "/" pour composer (vendor/pkg),
89
- // "" (concat avec scope @) sinon.
90
- function joinNs(ecosystem, namespace, name) {
91
- if (!namespace) return name;
92
- if (ecosystem === "composer") return `${namespace}/${name}`;
93
- if (ecosystem === "maven") return `${namespace}:${name}`;
94
- // npm scope: namespace="@org" → "@org/name"
95
- return `${namespace}/${name}`;
96
- }
97
-
98
- // NuGet & Composer & PyPI sont case-insensitive : on normalise la clé en lower
99
- // (l'affichage garde la casse d'origine via dep.name).
100
- function normalizeForKey(ecosystem, s) {
101
- if (ecosystem === "nuget" || ecosystem === "composer" || ecosystem === "pypi") return String(s).toLowerCase();
102
- return s;
103
- }
104
-
105
- function coordKeyFor(ecosystem, namespace, name) {
106
- const joined = joinNs(ecosystem, normalizeForKey(ecosystem, namespace || ""), normalizeForKey(ecosystem, name || ""));
107
- return `${ecosystem}:${joined}`;
108
- }
109
-
110
- function makeDepRecord(input) {
111
- const { ecosystem, namespace = "", name, version = null, manifestPath, scope = "compile", isDev = false, ecosystemType } = input;
112
- const concrete = version && !/\$\{/.test(version) ? version : null;
113
- const manifestPaths = manifestPath ? [manifestPath] : [];
114
- return {
115
- ecosystem,
116
- ecosystemType: ecosystemType || ecosystem,
117
- namespace: namespace || "",
118
- name,
119
- version: version || null,
120
- versions: concrete ? [concrete] : [],
121
- coordKey: coordKeyFor(ecosystem, namespace, name),
122
- scope,
123
- isDev: !!isDev,
124
- manifestPaths,
125
- // Alias rétro-compat : CHAMPS DUPLIQUÉS RÉELS (pas des getters — les depRecords
126
- // sont spreadés dans des chemins chauds : cve-match.js:175, scan-completeness.js,
127
- // cve-report.js:1036, snyk.js:105 — un getter serait perdu au spread).
128
- // groupId/artifactId sont des strings jamais réassignées → pas de dérive.
129
- // pomPaths PARTAGE la référence de manifestPaths → les push restent synchrones.
130
- groupId: namespace || "",
131
- artifactId: name,
132
- pomPaths: manifestPaths,
133
- };
134
- }
135
-
136
- module.exports = { makeDepRecord, coordKeyFor };
137
- ```
138
-
139
- - [ ] **Step 4: Lancer le test pour vérifier le succès**
140
-
141
- Run: `node --test test/dep-record.test.js`
142
- Expected: PASS (3 tests)
143
-
144
- - [ ] **Step 5: Commit**
145
-
146
- ```bash
147
- git add lib/dep-record.js test/dep-record.test.js
148
- git commit -m "Add generalized depRecord builder (codec foundation)"
149
- ```
150
-
151
- > ✓ Décision : alias = champs dupliqués réels (vérifié : 4 sites spreadent les depRecords).
152
- > `pomPaths` partage la référence de `manifestPaths`. Un `JSON.parse(JSON.stringify(dep))`
153
- > casserait ce partage de référence, mais aucun chemin chaud ne sérialise/désérialise un
154
- > depRecord (vérifié : aucun `JSON.parse(JSON.stringify(dep))` dans le code).
155
-
156
- ---
157
-
158
- ### Task 2: Interface codec + validation de forme
159
-
160
- **Files:**
161
- - Create: `lib/codecs/codec.interface.js`
162
- - Test: `test/codecs.test.js` (créé ici, étendu en Task 6)
163
-
164
- - [ ] **Step 1: Écrire le test qui échoue**
165
-
166
- ```js
167
- // test/codecs.test.js
168
- const test = require("node:test");
169
- const assert = require("node:assert");
170
- const { assertCodecShape, REQUIRED_KEYS } = require("../lib/codecs/codec.interface");
171
-
172
- test("assertCodecShape accepts a complete codec stub", () => {
173
- const stub = {
174
- id: "x", label: "X", osvEcosystem: "npm",
175
- manifestNames: ["x.json"],
176
- detect: () => false,
177
- collect: async () => ({ deps: new Map(), warnings: [] }),
178
- coordKey: d => `x:${d.name}`,
179
- formatCoord: d => d.name,
180
- osvPackageName: d => d.name,
181
- checkRegistry: async () => ({ outdated: [], deprecated: [] }),
182
- resolveEolProduct: () => null,
183
- recipe: { label: "X", pinSection: "", pinIntro: () => "", snippet: () => "", directSection: "" },
184
- nativeScanners: [],
185
- };
186
- assert.doesNotThrow(() => assertCodecShape(stub));
187
- });
188
-
189
- test("assertCodecShape rejects a codec missing a required method", () => {
190
- assert.throws(() => assertCodecShape({ id: "y" }), /missing|y/i);
191
- });
192
- ```
193
-
194
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
195
-
196
- Run: `node --test test/codecs.test.js`
197
- Expected: FAIL — `Cannot find module '../lib/codecs/codec.interface'`
198
-
199
- - [ ] **Step 3: Implémenter l'interface + validateur**
200
-
201
- ```js
202
- // lib/codecs/codec.interface.js
203
- // Contrat que tout codec doit respecter. Pas de classe imposée : un codec est
204
- // un objet litéral exportant ces clés.
205
- const REQUIRED_KEYS = [
206
- "id", "label", "osvEcosystem", "manifestNames",
207
- "detect", "collect", "coordKey", "formatCoord", "osvPackageName",
208
- "checkRegistry", "resolveEolProduct", "recipe", "nativeScanners",
209
- ];
210
- const FUNCTION_KEYS = ["detect", "collect", "coordKey", "formatCoord", "osvPackageName", "checkRegistry", "resolveEolProduct"];
211
-
212
- function assertCodecShape(codec) {
213
- if (!codec || typeof codec !== "object") throw new Error("codec must be an object");
214
- for (const k of REQUIRED_KEYS) {
215
- if (!(k in codec)) throw new Error(`codec "${codec.id || "?"}" missing required key: ${k}`);
216
- }
217
- for (const k of FUNCTION_KEYS) {
218
- if (typeof codec[k] !== "function") throw new Error(`codec "${codec.id}" key ${k} must be a function`);
219
- }
220
- if (!Array.isArray(codec.manifestNames)) throw new Error(`codec "${codec.id}" manifestNames must be an array`);
221
- if (!Array.isArray(codec.nativeScanners)) throw new Error(`codec "${codec.id}" nativeScanners must be an array`);
222
- return true;
223
- }
224
-
225
- module.exports = { REQUIRED_KEYS, FUNCTION_KEYS, assertCodecShape };
226
- ```
227
-
228
- - [ ] **Step 4: Lancer le test pour vérifier le succès**
229
-
230
- Run: `node --test test/codecs.test.js`
231
- Expected: PASS (2 tests)
232
-
233
- - [ ] **Step 5: Commit**
234
-
235
- ```bash
236
- git add lib/codecs/codec.interface.js test/codecs.test.js
237
- git commit -m "Add codec interface contract + shape validator"
238
- ```
239
-
240
- ---
241
-
242
- ### Task 3: Codec Maven (extraction, pas réécriture)
243
-
244
- **Files:**
245
- - Create: `lib/codecs/maven.codec.js`
246
- - Modify: `lib/cve-match.js:31-95` (`collectResolvedDeps` produit le depRecord généralisé via `makeDepRecord`)
247
-
248
- - [ ] **Step 1: Écrire le test qui échoue**
249
-
250
- ```js
251
- // ajout dans test/codecs.test.js
252
- const path = require("path");
253
- const maven = require("../lib/codecs/maven.codec");
254
-
255
- test("maven codec detects the simple fixture and collects deps with namespaced coordKeys", async () => {
256
- const dir = path.join(__dirname, "fixtures", "simple");
257
- assert.strictEqual(maven.detect(dir), true);
258
- const { deps } = await maven.collect(dir, {});
259
- assert.ok(deps.size > 0);
260
- for (const [k, d] of deps) {
261
- assert.ok(k.startsWith("maven:"), `key ${k} should be maven-namespaced`);
262
- assert.strictEqual(d.ecosystem, "maven");
263
- assert.strictEqual(d.coordKey, k);
264
- }
265
- });
266
- ```
267
-
268
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
269
-
270
- Run: `node --test test/codecs.test.js`
271
- Expected: FAIL — `Cannot find module '../lib/codecs/maven.codec'`
272
-
273
- - [ ] **Step 3a: Migrer `collectResolvedDeps` vers `makeDepRecord`**
274
-
275
- Dans `lib/cve-match.js`, remplacer les deux `out.set(key, {...})` (lignes ~52 et ~86) par des appels à `makeDepRecord`, et le `key` par `coordKey`. Garder la logique de merge identique mais en s'appuyant sur `namespace`/`name`/`versions`/`manifestPaths`. Ajouter en tête : `const { makeDepRecord, coordKeyFor } = require("./dep-record");`
276
-
277
- ```js
278
- // remplace la branche !existing (dep normale) :
279
- const rec = makeDepRecord({ ecosystem: "maven", namespace: g, name: a, version: v || null, manifestPath: pomPath, scope, isDev });
280
- out.set(rec.coordKey, rec);
281
- // la branche existing reste, mais lit existing.versions / existing.manifestPaths / existing.namespace
282
- // et le `key` devient coordKeyFor("maven", g, a)
283
- ```
284
-
285
- Idem pour la branche parent (scope `parent`). Le reste du fichier (transitive, matching) lit déjà `dep.groupId`/`dep.artifactId` → fonctionne via les alias.
286
-
287
- - [ ] **Step 3b: Écrire le codec Maven (enveloppe la logique existante)**
288
-
289
- ```js
290
- // lib/codecs/maven.codec.js
291
- const core = require("../core");
292
- const { collectResolvedDeps, expandWithTransitives, matchDepsAgainstCves } = require("../cve-match");
293
- const { coordKeyFor } = require("../dep-record");
294
-
295
- // Scanner natif : CVE-index local cvelistV5. Reçoit (deps, opts) et renvoie des matches.
296
- const cveIndexScanner = {
297
- id: "cve-index",
298
- async scan(deps, opts) {
299
- const { ensureCveIndex } = require("../cve-download");
300
- const idx = await ensureCveIndex({ force: opts.cveRefresh && !opts.offline, offline: opts.cveOffline || opts.offline, verbose: opts.verbose });
301
- return { matches: matchDepsAgainstCves(deps, idx), meta: { cveDataDate: idx?.meta?.builtAt || null } };
302
- },
303
- };
304
-
305
- module.exports = {
306
- id: "maven",
307
- label: "Maven",
308
- osvEcosystem: "Maven",
309
- manifestNames: ["pom.xml"],
310
- detect(dir) { return core.findPomFiles(dir).length > 0; },
311
-
312
- // collect enveloppe parse + inheritance + collectResolvedDeps existants.
313
- async collect(dir, opts) {
314
- const pomFiles = core.findPomFiles(dir);
315
- const store = core.newMetadataStore();
316
- const propsByPom = {};
317
- for (const pom of pomFiles) { try { await core.parsePom(pom, store); } catch { /* logged by caller */ } }
318
- for (const pom of Object.keys(store.byPath)) { try { await core.getAllInheritedProps(pom, store, propsByPom); } catch { /* logged */ } }
319
- const deps = collectResolvedDeps(store, propsByPom, { ignoreTest: opts.ignoreTest, deps2Exclude: opts.deps2Exclude });
320
- // On expose store/propsByPom pour que l'orchestrateur garde la phase rewrite POM.
321
- return { deps, warnings: [], _maven: { store, propsByPom, pomFiles } };
322
- },
323
-
324
- coordKey(d) { return coordKeyFor("maven", d.namespace || d.groupId, d.name || d.artifactId); },
325
- formatCoord(d) { return `${d.namespace || d.groupId}:${d.name || d.artifactId}`; },
326
- osvPackageName(d) { return `${d.namespace || d.groupId}:${d.name || d.artifactId}`; },
327
-
328
- async checkRegistry(deps, opts) {
329
- const outdated = require("../outdated");
330
- const out = opts.allLibs ? await outdated.checkOutdatedDeps(deps, opts) : [];
331
- const deprecated = outdated.checkObsoleteDeps(deps);
332
- return { outdated: out, deprecated };
333
- },
334
- resolveEolProduct(d) { return require("../outdated").findEolProduct(d); },
335
- recipe: require("./recipes").maven, // extrait de RECIPE_SPECS en Task 7
336
- nativeScanners: [cveIndexScanner],
337
- expandTransitives: expandWithTransitives, // utilisé par l'orchestrateur si --transitive
338
- };
339
- ```
340
-
341
- > Note : `findEolProduct` et `recipes` sont rendus requérables aux Tasks 5 et 7 ; jusque-là, le codec maven peut référencer des stubs. Pour garder le test de Task 3 vert sans dépendances avant, `recipe` peut pointer un objet inline temporaire et `resolveEolProduct` renvoyer `null` — remplacés en Task 5/7. **Décision : on inline un `recipe` minimal et `resolveEolProduct: () => null` ici, finalisés plus tard.**
342
-
343
- - [ ] **Step 4: Lancer toute la suite**
344
-
345
- Run: `npm test`
346
- Expected: PASS — 96 tests existants + nouveaux tests codec. Le comportement de `collectResolvedDeps` est inchangé (clés `maven:g:a` au lieu de `g:a`, mais les consommateurs lisent via alias).
347
-
348
- > ⚠️ Risque clé : des tests existants assertent peut-être des clés `g:a` brutes. Si `node --test test/cve-match.test.js` échoue sur des clés, adapter ces assertions vers `maven:g:a` (c'est le nouveau contrat de clé, attendu).
349
-
350
- - [ ] **Step 5: Commit**
351
-
352
- ```bash
353
- git add lib/codecs/maven.codec.js lib/cve-match.js test/codecs.test.js
354
- git commit -m "Extract Maven codec; collectResolvedDeps emits generalized depRecord"
355
- ```
356
-
357
- ---
358
-
359
- ### Task 4: Codecs npm + yarn (extraction)
360
-
361
- **Files:**
362
- - Create: `lib/codecs/npm.codec.js`, `lib/codecs/yarn.codec.js`
363
- - Modify: `lib/npm/collect.js` (`collectNpmDeps` produit le depRecord généralisé)
364
-
365
- - [ ] **Step 1: Écrire le test qui échoue**
366
-
367
- ```js
368
- // ajout dans test/codecs.test.js
369
- const npm = require("../lib/codecs/npm.codec");
370
-
371
- test("npm codec collects from monorepo-mixed with npm: coordKeys", async () => {
372
- const dir = path.join(__dirname, "fixtures", "monorepo-mixed");
373
- assert.strictEqual(npm.detect(dir), true);
374
- const { deps } = await npm.collect(dir, {});
375
- for (const [k, d] of deps) {
376
- assert.ok(k.startsWith("npm:"), `key ${k} should be npm-namespaced`);
377
- assert.strictEqual(d.ecosystem, "npm");
378
- }
379
- });
380
- ```
381
-
382
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
383
-
384
- Run: `node --test test/codecs.test.js`
385
- Expected: FAIL — `Cannot find module '../lib/codecs/npm.codec'`
386
-
387
- - [ ] **Step 3a: Migrer `collectNpmDeps` vers `makeDepRecord`**
388
-
389
- Dans `lib/npm/collect.js`, remplacer la construction du record (`out.set("npm:"+name, {...})`) par `makeDepRecord({ ecosystem:"npm", namespace: scopeOf(name), name, version, manifestPath, scope, isDev, ecosystemType })`. `namespace` = `@org` si nom scopé `@org/pkg`, sinon `""`. La clé devient `rec.coordKey` (toujours `npm:...`). Conserver `lockType/resolved/integrity` en les assignant après création (`rec.lockType = ...`). Préserver `out.warnings`.
390
-
391
- - [ ] **Step 3b: Écrire les codecs npm et yarn**
392
-
393
- ```js
394
- // lib/codecs/npm.codec.js
395
- const { collectNpmDeps, hasJsManifests } = require("../npm/collect");
396
- const { coordKeyFor } = require("../dep-record");
397
-
398
- const retireScanner = {
399
- id: "retire",
400
- async scan(_deps, opts) {
401
- const { scanWithRetire } = require("../retire");
402
- const matches = await scanWithRetire(opts.src, { verbose: opts.verbose, force: opts.retireRefresh, offline: opts.offline });
403
- return { matches, meta: {} };
404
- },
405
- };
406
-
407
- const base = {
408
- osvEcosystem: "npm",
409
- manifestNames: ["package.json", "package-lock.json", "yarn.lock"],
410
- detect(dir) { return hasJsManifests(dir); },
411
- coordKey(d) { return coordKeyFor("npm", d.namespace || "", d.name || d.artifactId); },
412
- formatCoord(d) { return d.namespace ? `${d.namespace}/${d.name}` : (d.name || d.artifactId); },
413
- osvPackageName(d) { return d.namespace ? `${d.namespace}/${d.name}` : (d.name || d.artifactId); },
414
- async checkRegistry(deps, opts) {
415
- const { checkNpmRegistryDeps } = require("../npm/registry");
416
- const r = await checkNpmRegistryDeps(deps, opts);
417
- return { outdated: r.outdated, deprecated: r.deprecated };
418
- },
419
- resolveEolProduct(d) { return require("../outdated").findEolProduct(d); },
420
- nativeScanners: [retireScanner],
421
- };
422
-
423
- module.exports = {
424
- ...base,
425
- id: "npm",
426
- label: "npm",
427
- recipe: require("./recipes").npm,
428
- // collect renvoie TOUTES les deps JS ; l'orchestrateur ne charge npm.collect qu'une fois.
429
- async collect(dir, opts) {
430
- const deps = collectNpmDeps(dir, opts);
431
- return { deps, warnings: deps.warnings || [] };
432
- },
433
- };
434
- ```
435
-
436
- ```js
437
- // lib/codecs/yarn.codec.js — yarn partage tout sauf l'id/label/recette.
438
- const npm = require("./npm.codec");
439
- module.exports = {
440
- ...npm,
441
- id: "yarn",
442
- label: "Yarn",
443
- recipe: require("./recipes").yarn,
444
- // Pas de collect séparé : npm.collect ramasse déjà yarn.lock (ecosystemType="yarn").
445
- // Le registre des codecs ne fait PAS tourner yarn.collect (évite le double scan) ;
446
- // yarn n'est exposé que pour son label/recette dans le report.
447
- collect: async () => ({ deps: new Map(), warnings: [] }),
448
- };
449
- ```
450
-
451
- > Décision d'orchestration : un seul `collect` JS tourne (celui du codec npm), qui ramasse package-lock ET yarn.lock — chaque dep porte son `ecosystemType`. Le codec yarn n'existe que pour fournir `label`/`recipe` au report. Le registre marque yarn `collectViaSibling: "npm"` pour documenter ça.
452
-
453
- - [ ] **Step 4: Lancer toute la suite**
454
-
455
- Run: `npm test`
456
- Expected: PASS — incl. nouveau test npm codec.
457
-
458
- - [ ] **Step 5: Commit**
459
-
460
- ```bash
461
- git add lib/codecs/npm.codec.js lib/codecs/yarn.codec.js lib/npm/collect.js test/codecs.test.js
462
- git commit -m "Extract npm/yarn codecs; collectNpmDeps emits generalized depRecord"
463
- ```
464
-
465
- ---
466
-
467
- ### Task 5: Registre des codecs
468
-
469
- **Files:**
470
- - Create: `lib/codecs/index.js`
471
- - Test: `test/codecs.test.js` (ajout)
472
-
473
- - [ ] **Step 1: Écrire le test qui échoue**
474
-
475
- ```js
476
- // ajout dans test/codecs.test.js
477
- const { getCodec, allCodecs, detectCodecs } = require("../lib/codecs");
478
-
479
- test("registry exposes maven/npm/yarn and validates their shape", () => {
480
- const ids = allCodecs().map(c => c.id).sort();
481
- assert.deepStrictEqual(ids, ["maven", "npm", "yarn"]);
482
- for (const c of allCodecs()) assertCodecShape(c);
483
- });
484
-
485
- test("detectCodecs finds maven+npm on monorepo-mixed", () => {
486
- const dir = path.join(__dirname, "fixtures", "monorepo-mixed");
487
- const detected = detectCodecs(dir).map(c => c.id);
488
- assert.ok(detected.includes("maven"));
489
- assert.ok(detected.includes("npm"));
490
- });
491
- ```
492
-
493
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
494
-
495
- Run: `node --test test/codecs.test.js`
496
- Expected: FAIL — `Cannot find module '../lib/codecs'`
497
-
498
- - [ ] **Step 3: Implémenter le registre**
499
-
500
- ```js
501
- // lib/codecs/index.js
502
- const { assertCodecShape } = require("./codec.interface");
503
- const maven = require("./maven.codec");
504
- const npm = require("./npm.codec");
505
- const yarn = require("./yarn.codec");
506
-
507
- // Ordre stable pour le report.
508
- const ORDER = ["maven", "npm", "yarn", "nuget", "composer", "pypi"];
509
- const REGISTRY = new Map();
510
- for (const c of [maven, npm, yarn]) { assertCodecShape(c); REGISTRY.set(c.id, c); }
511
-
512
- function getCodec(id) { return REGISTRY.get(id) || null; }
513
- function allCodecs() {
514
- return [...REGISTRY.values()].sort((a, b) => ORDER.indexOf(a.id) - ORDER.indexOf(b.id));
515
- }
516
- // yarn est détecté via npm (même arbre JS) ; on ne le renvoie pas en doublon de détection.
517
- function detectCodecs(dir) {
518
- return allCodecs().filter(c => c.id !== "yarn" && c.detect(dir));
519
- }
520
-
521
- module.exports = { getCodec, allCodecs, detectCodecs, ORDER };
522
- ```
523
-
524
- - [ ] **Step 4: Lancer toute la suite**
525
-
526
- Run: `npm test`
527
- Expected: PASS.
528
-
529
- - [ ] **Step 5: Commit**
530
-
531
- ```bash
532
- git add lib/codecs/index.js test/codecs.test.js
533
- git commit -m "Add codec registry (getCodec/allCodecs/detectCodecs)"
534
- ```
535
-
536
- ---
537
-
538
- ### Task 6: Recettes de fix extraites (`lib/codecs/recipes.js`)
539
-
540
- **Files:**
541
- - Create: `lib/codecs/recipes.js`
542
- - Modify: `lib/cve-report.js` (importer les recettes depuis les codecs au lieu du `RECIPE_SPECS` codé en dur ~664-715)
543
-
544
- - [ ] **Step 1: Écrire le test qui échoue**
545
-
546
- ```js
547
- // ajout dans test/codecs.test.js
548
- const recipes = require("../lib/codecs/recipes");
549
- test("each recipe exposes label + snippet function", () => {
550
- for (const key of ["maven", "npm", "yarn"]) {
551
- assert.strictEqual(typeof recipes[key].label, "string");
552
- assert.strictEqual(typeof recipes[key].snippet, "function");
553
- }
554
- });
555
- ```
556
-
557
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
558
-
559
- Run: `node --test test/codecs.test.js`
560
- Expected: FAIL — `Cannot find module '../lib/codecs/recipes'`
561
-
562
- - [ ] **Step 3: Déplacer `RECIPE_SPECS` de `cve-report.js` vers `recipes.js`**
563
-
564
- Couper l'objet `RECIPE_SPECS` (et les helpers `pomOverridesSnippet`/`npmOverridesSnippet`/`yarnResolutionsSnippet`) de `lib/cve-report.js`, les coller dans `lib/codecs/recipes.js`, exporter `{ maven, npm, yarn }`. Dans `cve-report.js`, remplacer par `const RECIPE_SPECS = require("./codecs/recipes");` (ou mieux : construire depuis `allCodecs()` — fait en Task 7). Mettre à jour les codecs maven/npm/yarn pour pointer `recipe: require("./recipes")[id]` (remplace les stubs de Task 3/4).
565
-
566
- - [ ] **Step 4: Lancer toute la suite**
567
-
568
- Run: `npm test`
569
- Expected: PASS — le report génère les mêmes snippets qu'avant (vérifié par les tests de report existants).
570
-
571
- - [ ] **Step 5: Commit**
572
-
573
- ```bash
574
- git add lib/codecs/recipes.js lib/cve-report.js lib/codecs/maven.codec.js lib/codecs/npm.codec.js lib/codecs/yarn.codec.js test/codecs.test.js
575
- git commit -m "Extract fix recipes into lib/codecs/recipes.js"
576
- ```
577
-
578
- ---
579
-
580
- ### Task 7: Rendre OSV agnostique via le codec
581
-
582
- **Files:**
583
- - Modify: `lib/osv.js:100-102` (`osvPkgName`), `:144-168` (`queryBatch` ecosystem mapping)
584
- - Test: `test/osv.test.js` (ajout)
585
-
586
- - [ ] **Step 1: Écrire le test qui échoue**
587
-
588
- ```js
589
- // ajout dans test/osv.test.js
590
- const { osvEcosystemFor, osvPkgName } = require("../lib/osv");
591
- test("osvEcosystemFor maps codec ids to OSV ecosystem names", () => {
592
- assert.strictEqual(osvEcosystemFor({ ecosystem: "maven" }), "Maven");
593
- assert.strictEqual(osvEcosystemFor({ ecosystem: "npm" }), "npm");
594
- assert.strictEqual(osvEcosystemFor({ ecosystem: "nuget" }), "NuGet");
595
- assert.strictEqual(osvEcosystemFor({ ecosystem: "composer" }), "Packagist");
596
- assert.strictEqual(osvEcosystemFor({ ecosystem: "pypi" }), "PyPI");
597
- });
598
- ```
599
-
600
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
601
-
602
- Run: `node --test test/osv.test.js`
603
- Expected: FAIL — `osvEcosystemFor is not a function`
604
-
605
- - [ ] **Step 3: Remplacer le ternaire codé en dur par une délégation codec**
606
-
607
- Dans `lib/osv.js`, remplacer `osvPkgName` (ligne ~100) et le bloc `const ecosystem = d.ecosystem === "npm" ? "npm" : "Maven"` (ligne ~161) par :
608
-
609
- ```js
610
- const { getCodec } = require("./codecs");
611
- const OSV_ECO = { maven: "Maven", npm: "npm", yarn: "npm", nuget: "NuGet", composer: "Packagist", pypi: "PyPI" };
612
- function osvEcosystemFor(dep) { return OSV_ECO[dep.ecosystem] || "Maven"; }
613
- function osvPkgName(dep) {
614
- const c = getCodec(dep.ecosystem) || getCodec(dep.ecosystemType);
615
- if (c) return c.osvPackageName(dep);
616
- // fallback historique
617
- return dep.ecosystem === "npm" ? dep.artifactId : `${dep.groupId}:${dep.artifactId}`;
618
- }
619
- ```
620
-
621
- Dans `queryBatch`, remplacer les deux lignes par `const ecosystem = osvEcosystemFor(d); const pkgName = osvPkgName(d);`. Exporter `osvEcosystemFor`, `osvPkgName`.
622
-
623
- - [ ] **Step 4: Lancer toute la suite**
624
-
625
- Run: `npm test`
626
- Expected: PASS.
627
-
628
- - [ ] **Step 5: Commit**
629
-
630
- ```bash
631
- git add lib/osv.js test/osv.test.js
632
- git commit -m "OSV: derive ecosystem + package name from codec (agnostic)"
633
- ```
634
-
635
- ---
636
-
637
- ### Task 8: Report — sections et coordonnées pilotées par codec
638
-
639
- **Files:**
640
- - Modify: `lib/cve-report.js` (`formatCoord` ~352-357, groupage `byEco` ~792-803, `ECO_LABELS`/`ECO_MANIFEST_KIND` ~1462-1470)
641
-
642
- - [ ] **Step 1: Écrire le test qui échoue**
643
-
644
- ```js
645
- // ajout dans test/cve-report.test.js (ou nouveau bloc)
646
- const { formatDepCoord } = require("../lib/cve-report");
647
- test("formatDepCoord delegates to codec for npm and maven", () => {
648
- assert.strictEqual(formatDepCoord({ ecosystem: "maven", namespace: "org.apache", name: "log4j" }), "org.apache:log4j");
649
- assert.strictEqual(formatDepCoord({ ecosystem: "npm", namespace: "", name: "lodash" }), "lodash");
650
- });
651
- ```
652
-
653
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
654
-
655
- Run: `node --test test/cve-report.test.js`
656
- Expected: FAIL — `formatDepCoord is not a function`
657
-
658
- - [ ] **Step 3: Centraliser le formatage + générer les labels depuis les codecs**
659
-
660
- Dans `lib/cve-report.js` :
661
- - Ajouter et exporter `function formatDepCoord(dep){ const c = require("./codecs").getCodec(dep.ecosystemType) || require("./codecs").getCodec(dep.ecosystem); return c ? c.formatCoord(dep) : (dep.ecosystem === "npm" ? \`npm:${dep.artifactId}\` : \`${dep.groupId}:${dep.artifactId}\`); }`
662
- - Remplacer les `if (dep?.ecosystem === "npm") return ...` (lignes ~352-357) par un appel à `formatDepCoord`.
663
- - Remplacer `ECO_LABELS`/`ECO_MANIFEST_KIND` codés en dur par une construction depuis `allCodecs()` : `const ECO_LABELS = Object.fromEntries(allCodecs().map(c => [c.id, c.label]))` et `ECO_MANIFEST_KIND` depuis `c.manifestNames[c.manifestNames.length-1]` (le lockfile représentatif) — conserver les libellés actuels via une petite table d'override (`npm → "npm (package-lock)"`, etc.) pour ne pas changer le texte du report.
664
- - Le groupage `byEco` et `ecoOrder` lisent `require("./codecs").ORDER` au lieu de `["maven","npm","yarn"]`.
665
-
666
- - [ ] **Step 4: Lancer toute la suite**
667
-
668
- Run: `npm test`
669
- Expected: PASS — texte du report inchangé.
670
-
671
- - [ ] **Step 5: Commit**
672
-
673
- ```bash
674
- git add lib/cve-report.js test/cve-report.test.js
675
- git commit -m "Report: derive coord formatting + ecosystem labels from codecs"
676
- ```
677
-
678
- ---
679
-
680
- ### Task 9: Orchestrateur par boucle de codecs + CLI liste
681
-
682
- **Files:**
683
- - Modify: `fad-checker.js:250-401` (détection + collecte), `:160-196` (flags CLI)
684
- - Modify: `lib/npm/collect.js` (fallback no-lockfile — changement de contrat assumé)
685
- - Test: `test/cli-ecosystem.test.js` (créé), adaptation de `test/npm-collect.test.js` (no-lockfile)
686
-
687
- - [ ] **Step 1: Écrire le test qui échoue (parsing du flag liste)**
688
-
689
- ```js
690
- // test/cli-ecosystem.test.js
691
- const test = require("node:test");
692
- const assert = require("node:assert");
693
- const { resolveActiveCodecs } = require("../lib/codecs/select");
694
-
695
- test("resolveActiveCodecs parses comma list and 'all'", () => {
696
- const all = ["maven", "npm", "nuget", "composer", "pypi"];
697
- assert.deepStrictEqual(resolveActiveCodecs("maven,pypi", all, {}), ["maven", "pypi"]);
698
- assert.deepStrictEqual(resolveActiveCodecs("all", all, {}), all);
699
- });
700
-
701
- test("resolveActiveCodecs honors --no-<id> flags", () => {
702
- const all = ["maven", "npm", "nuget"];
703
- assert.deepStrictEqual(resolveActiveCodecs("all", all, { noCodecs: ["npm"] }), ["maven", "nuget"]);
704
- });
705
-
706
- test("--no-js aliases to npm+yarn", () => {
707
- const all = ["maven", "npm", "yarn", "nuget"];
708
- assert.deepStrictEqual(resolveActiveCodecs("all", all, { noJs: true }), ["maven", "nuget"]);
709
- });
710
- ```
711
-
712
- - [ ] **Step 2: Lancer le test pour vérifier l'échec**
713
-
714
- Run: `node --test test/cli-ecosystem.test.js`
715
- Expected: FAIL — `Cannot find module '../lib/codecs/select'`
716
-
717
- - [ ] **Step 3a: Implémenter la sélection de codecs**
718
-
719
- ```js
720
- // lib/codecs/select.js
721
- // `requested` : "auto" | "all" | "maven,npm,..." ; `available` : ids détectés (auto) ou tous.
722
- function resolveActiveCodecs(requested, available, flags = {}) {
723
- const { noCodecs = [], noJs = false } = flags;
724
- let active;
725
- const req = String(requested || "auto").toLowerCase();
726
- if (req === "auto") active = [...available];
727
- else if (req === "all") active = [...available];
728
- else active = req.split(",").map(s => s.trim()).filter(Boolean).filter(id => available.includes(id));
729
- const excluded = new Set(noCodecs);
730
- if (noJs) { excluded.add("npm"); excluded.add("yarn"); }
731
- return active.filter(id => !excluded.has(id));
732
- }
733
- module.exports = { resolveActiveCodecs };
734
- ```
735
-
736
- - [ ] **Step 3b: Câbler les flags CLI**
737
-
738
- Dans `fad-checker.js` (bloc commander ~160-196) : remplacer `--ecosystem <auto|maven|npm|both>` par `--ecosystem <list>` (défaut `"auto"`, doc : `auto|all|maven,npm,nuget,composer,pypi`). Ajouter `--no-maven --no-npm --no-yarn --no-nuget --no-composer --no-pypi`. Garder `--no-js`. Construire `flags.noCodecs` depuis les options `--no-*`.
739
-
740
- - [ ] **Step 3c: Réécrire le bloc d'orchestration (250-401) en boucle de codecs**
741
-
742
- ```js
743
- const { detectCodecs, allCodecs, getCodec } = require("./lib/codecs");
744
- const { resolveActiveCodecs } = require("./lib/codecs/select");
745
-
746
- const detected = (options.ecosystem === "auto" || !options.ecosystem)
747
- ? detectCodecs(options.src).map(c => c.id)
748
- : allCodecs().map(c => c.id);
749
- const noCodecs = ["maven","npm","yarn","nuget","composer","pypi"].filter(id => options[`no${id[0].toUpperCase()+id.slice(1)}`] === true);
750
- const activeIds = resolveActiveCodecs(options.ecosystem, detected, { noCodecs, noJs: !options.js });
751
-
752
- const resolved = new Map();
753
- let mavenCtx = null; // {store, propsByPom, pomFiles} pour la phase rewrite POM
754
- const allWarnings = [];
755
- for (const id of activeIds) {
756
- if (id === "yarn") continue; // ramassé par npm.collect
757
- const codec = getCodec(id);
758
- const { deps, warnings, _maven } = await codec.collect(options.src, { ignoreTest: !!options.ignoreTest, deps2Exclude, verbose });
759
- for (const [k, v] of deps) resolved.set(k, v);
760
- if (warnings?.length) allWarnings.push(...warnings);
761
- if (id === "maven") mavenCtx = _maven;
762
- }
763
- ```
764
-
765
- Conserver ensuite la phase rewrite POM (lignes 279-348) en la gardant sous `if (mavenCtx)` et en lisant `mavenCtx.store`/`mavenCtx.propsByPom`/`mavenCtx.pomFiles` au lieu des variables globales. Remplacer dans `runReportFlow` les appels nativeScanners/registry/EOL par une boucle sur `activeIds`/`getCodec(id).nativeScanners` et `getCodec(id).checkRegistry`. Les sections OSV/NVD/CPE restent telles quelles (déjà agnostiques après Task 7).
766
-
767
- > ⚠️ Le dedup cross-section (`eolKeys`/`obsKeys`, lignes 479-484) utilise `${dep.groupId}:${dep.artifactId}`. Remplacer par `dep.coordKey` (déjà unique inter-eco). Idem `depLabel` ligne 568 → `formatDepCoord`.
768
-
769
- - [ ] **Step 3d: Fallback no-lockfile (changement de contrat npm)**
770
-
771
- Dans `lib/npm/collect.js`, là où un `package.json` sans lock pousse aujourd'hui un warning `no-lockfile` **et skippe** : désormais parser `package.json` (dependencies + devDependencies), ne garder que les versions épinglées exactes (pas `^`/`~`/`*`/ranges), créer des depRecords best-effort, et pousser le warning `no-lockfile — résultats partiels`. Les ranges → warning `unresolved-versions`.
772
-
773
- - [ ] **Step 4a: Adapter le test no-lockfile existant**
774
-
775
- Trouver le test qui vérifie le skip total (`test/npm-collect.test.js`, cas `no-lockfile`). Le réécrire pour vérifier : (a) un warning `no-lockfile` est émis, (b) les deps à version épinglée du `package.json` sont bien collectées, (c) les deps en range ne le sont pas.
776
-
777
- - [ ] **Step 4b: Lancer toute la suite**
778
-
779
- Run: `npm test`
780
- Expected: PASS — incl. `cli-ecosystem.test.js` et le test no-lockfile adapté.
781
-
782
- - [ ] **Step 4c: Smoke test bout-en-bout (offline)**
783
-
784
- Run: `node fad-checker.js -s ./test/fixtures/monorepo-mixed --offline`
785
- Expected: le run liste maven + npm, produit le report sans erreur, et `package.json` sans lock du fixture apparaît désormais en best-effort + warning (au lieu de skip total).
786
-
787
- - [ ] **Step 5: Commit**
788
-
789
- ```bash
790
- git add fad-checker.js lib/codecs/select.js lib/npm/collect.js test/cli-ecosystem.test.js test/npm-collect.test.js
791
- git commit -m "Orchestrate pipeline as a codec loop; --ecosystem list + --no-<id>; npm no-lockfile fallback"
792
- ```
793
-
794
- ---
795
-
796
- ### Task 10: Mise à jour docs + completions
797
-
798
- **Files:**
799
- - Modify: `CLAUDE.md`, `docs/ARCHITECTURE.md`, `docs/USAGE.md`, `completions/fad-checker.bash`, `completions/fad-checker.zsh`
800
-
801
- - [ ] **Step 1: Documenter l'abstraction codec**
802
-
803
- Dans `docs/ARCHITECTURE.md` : ajouter une section « Codecs » décrivant l'interface, le registre, et que les services OSV/NVD/CPE/EOL sont agnostiques. Mettre à jour la « Module map ». Dans `CLAUDE.md` : ajouter `lib/codecs/*` à l'arbre, documenter le nouveau `--ecosystem <list>` + `--no-<id>`, et le changement de contrat npm no-lockfile.
804
-
805
- - [ ] **Step 2: Mettre à jour les complétions**
806
-
807
- Ajouter `--no-maven --no-nuget --no-composer --no-pypi` et la nouvelle forme de `--ecosystem` dans les deux fichiers de complétion.
808
-
809
- - [ ] **Step 3: Vérifier**
810
-
811
- Run: `npm test`
812
- Expected: PASS (les docs n'affectent pas les tests ; on relance par sécurité).
813
-
814
- - [ ] **Step 4: Commit**
815
-
816
- ```bash
817
- git add CLAUDE.md docs/ARCHITECTURE.md docs/USAGE.md completions/
818
- git commit -m "Docs + completions: codec abstraction, --ecosystem list, --no-<id>"
819
- ```
820
-
821
- ---
822
-
823
- ## Self-Review (effectuée)
824
-
825
- **Couverture spec :**
826
- - Interface codec → Task 2 ✓ ; depRecord généralisé → Task 1 ✓ ; registre → Task 5 ✓ ;
827
- extraction maven/npm/yarn → Tasks 3-4 ✓ ; OSV agnostique → Task 7 ✓ ;
828
- NVD/CPE agnostiques → déjà agnostiques (par CVE-id), confirmé Task 9 ✓ ;
829
- EOL agnostique → `findEolProduct` déjà dispatch sur ecosystem, exposé via codec Tasks 3-4 ✓ ;
830
- report dynamique → Task 8 ✓ ; CLI liste + `--no-<id>` → Task 9 ✓ ;
831
- changement de contrat npm → Task 9 (3d/4a) ✓ ; docs → Task 10 ✓.
832
- - **Hors de ce plan (volontaire)** : les 3 nouveaux codecs (Plans B/C/D), les fixtures
833
- csharp/php/python, le parser TOML, le `data/eol-mapping.json` enrichi.
834
-
835
- **Cohérence des types :** `coordKey` (string), `makeDepRecord(input)` signature stable,
836
- `collect(dir,opts) → {deps,warnings}`, scanner `{id, scan(deps,opts)→{matches,meta}}`,
837
- `checkRegistry → {outdated,deprecated}`. Noms alignés entre tâches.
838
-
839
- **Point de vigilance reporté à l'exécution :** les getters alias (`groupId`/`artifactId`)
840
- de `makeDepRecord` ne survivent pas au spread/clone. Task 3 Step 4 et Task 9 Step 4c
841
- servent de filet (suite complète + smoke run). Si un chemin clone les records, migrer
842
- ce chemin vers `namespace`/`name`.
843
-
844
- ## Plans suivants (à rédiger après A)
845
-
846
- - **Plan B — composer** : codec `composer`, parse `composer.lock`/`composer.json`,
847
- registre Packagist (`abandoned`), EOL `by_composer_name`, recette, fixtures `php-app/`.
848
- - **Plan C — pypi** : dép `smol-toml`, parse poetry.lock/Pipfile.lock/uv.lock/pdm.lock/
849
- requirements.txt, normalisation PEP 503, registre PyPI (`yanked`), EOL `by_pypi_name`.
850
- - **Plan D — nuget** : parse .csproj/packages.lock.json/packages.config + CPM
851
- (`Directory.Packages.props`), registre NuGet (`deprecation`), EOL `by_nuget_name`/dotnet.