fad-checker 2.0.0 → 2.0.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 (93) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/README.md +58 -10
  3. package/data/eol-mapping.json +5 -5
  4. package/fad-checker.js +289 -203
  5. package/lib/codecs/npm/collect.js +26 -17
  6. package/lib/codecs/npm/parse.js +114 -10
  7. package/lib/codecs/npm/registry.js +4 -3
  8. package/lib/codecs/npm.codec.js +1 -1
  9. package/lib/codecs/nuget.codec.js +4 -2
  10. package/lib/codecs/pypi/parse.js +104 -12
  11. package/lib/codecs/pypi.codec.js +27 -6
  12. package/lib/core.js +35 -1
  13. package/lib/cve-match.js +15 -5
  14. package/lib/deps-descriptor.js +110 -0
  15. package/lib/nvd.js +4 -3
  16. package/lib/osv.js +7 -4
  17. package/lib/outdated.js +7 -2
  18. package/lib/retire.js +77 -13
  19. package/lib/transitive.js +3 -3
  20. package/lib/ui.js +87 -0
  21. package/package.json +3 -2
  22. package/CLAUDE.md +0 -147
  23. package/docs/ARCHITECTURE.md +0 -182
  24. package/docs/USAGE.md +0 -187
  25. package/docs/superpowers/plans/2026-05-29-codec-composer.md +0 -556
  26. package/docs/superpowers/plans/2026-05-29-codec-foundation.md +0 -851
  27. package/docs/superpowers/plans/2026-05-29-codec-nuget.md +0 -432
  28. package/docs/superpowers/plans/2026-05-29-codec-pypi.md +0 -450
  29. package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +0 -251
  30. package/test/cli-ecosystem.test.js +0 -30
  31. package/test/codec-edge-cases.test.js +0 -131
  32. package/test/codec-integration.test.js +0 -119
  33. package/test/codecs.test.js +0 -92
  34. package/test/composer.test.js +0 -71
  35. package/test/core.test.js +0 -153
  36. package/test/cpe.test.js +0 -166
  37. package/test/cve-download.test.js +0 -39
  38. package/test/cve-match.test.js +0 -217
  39. package/test/cve-report.test.js +0 -180
  40. package/test/dep-record.test.js +0 -31
  41. package/test/fixtures/complex-enterprise/api/pom.xml +0 -32
  42. package/test/fixtures/complex-enterprise/build/pom.xml +0 -46
  43. package/test/fixtures/complex-enterprise/dao/pom.xml +0 -37
  44. package/test/fixtures/complex-enterprise/pom.xml +0 -66
  45. package/test/fixtures/complex-enterprise/web/pom.xml +0 -77
  46. package/test/fixtures/csharp-config/packages.config +0 -4
  47. package/test/fixtures/csharp-csproj/Directory.Packages.props +0 -5
  48. package/test/fixtures/csharp-csproj/app.csproj +0 -7
  49. package/test/fixtures/csharp-lock/packages.lock.json +0 -6
  50. package/test/fixtures/cve-samples/cve-non-java.json +0 -19
  51. package/test/fixtures/cve-samples/cve-product-only.json +0 -31
  52. package/test/fixtures/cve-samples/cve-with-packagename.json +0 -37
  53. package/test/fixtures/cve-samples/nvd-log4shell.json +0 -40
  54. package/test/fixtures/cve-samples/nvd-npm-lodash.json +0 -22
  55. package/test/fixtures/monorepo-mixed/libs/common-bom/pom.xml +0 -26
  56. package/test/fixtures/monorepo-mixed/packages/cli/package.json +0 -14
  57. package/test/fixtures/monorepo-mixed/packages/cli/yarn.lock +0 -41
  58. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +0 -10
  59. package/test/fixtures/monorepo-mixed/packages/web-app/package-lock.json +0 -71
  60. package/test/fixtures/monorepo-mixed/packages/web-app/package.json +0 -17
  61. package/test/fixtures/monorepo-mixed/pom.xml +0 -29
  62. package/test/fixtures/monorepo-mixed/services/api/pom.xml +0 -27
  63. package/test/fixtures/monorepo-mixed/services/worker/pom.xml +0 -28
  64. package/test/fixtures/php-app/composer.json +0 -5
  65. package/test/fixtures/php-app/composer.lock +0 -10
  66. package/test/fixtures/polyglot/cs/packages.lock.json +0 -1
  67. package/test/fixtures/polyglot/js/package-lock.json +0 -1
  68. package/test/fixtures/polyglot/js/package.json +0 -1
  69. package/test/fixtures/polyglot/mvn/pom.xml +0 -7
  70. package/test/fixtures/polyglot/php/composer.lock +0 -1
  71. package/test/fixtures/polyglot/py/poetry.lock +0 -3
  72. package/test/fixtures/private-lib-detection/core/pom.xml +0 -26
  73. package/test/fixtures/private-lib-detection/plugin/pom.xml +0 -23
  74. package/test/fixtures/private-lib-detection/pom.xml +0 -35
  75. package/test/fixtures/python-pipenv/Pipfile.lock +0 -1
  76. package/test/fixtures/python-poetry/poetry.lock +0 -7
  77. package/test/fixtures/python-reqs/requirements.txt +0 -5
  78. package/test/fixtures/python-uv/uv.lock +0 -3
  79. package/test/fixtures/simple/app/pom.xml +0 -28
  80. package/test/fixtures/simple/lib/pom.xml +0 -18
  81. package/test/fixtures/simple/pom.xml +0 -24
  82. package/test/maven-repo.test.js +0 -111
  83. package/test/maven-version.test.js +0 -57
  84. package/test/monorepo.test.js +0 -132
  85. package/test/npm-registry.test.js +0 -64
  86. package/test/npm.test.js +0 -150
  87. package/test/nuget.test.js +0 -66
  88. package/test/osv.test.js +0 -62
  89. package/test/outdated.test.js +0 -101
  90. package/test/pypi.test.js +0 -72
  91. package/test/snyk.test.js +0 -64
  92. package/test/transitive.test.js +0 -305
  93. package/test/webjar.test.js +0 -33
@@ -1,556 +0,0 @@
1
- # Plan B — Codec Composer (PHP) Implementation Plan
2
-
3
- > **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.
4
-
5
- **Goal:** Ajouter le codec `composer` (PHP) en parité complète — vuln (OSV/Packagist), abandoned (Packagist), outdated (Packagist), EOL (endoflife.date), recette de fix — sur l'interface codec figée par Plan A.
6
-
7
- **Architecture:** `lib/composer/parse.js` parse `composer.lock` (et `composer.json` en fallback). `lib/composer/registry.js` interroge Packagist (latest stable + champ `abandoned`). `lib/codecs/composer.codec.js` assemble le tout. L'orchestrateur, déjà une boucle de codecs pour la collecte/OSV, gagne une boucle générique `codec.checkRegistry` pour les écosystèmes hors maven/npm. OSV (ecosystem `Packagist`) est déjà câblé (Plan A, Task 7).
8
-
9
- **Tech Stack:** Node.js, node --test. Aucune dépendance nouvelle (composer.lock + composer.json sont du JSON).
10
-
11
- ---
12
-
13
- ## File Structure
14
-
15
- | Fichier | Rôle | Action |
16
- |---|---|---|
17
- | `lib/composer/parse.js` | parse composer.lock (`packages[]`+`packages-dev[]`) + composer.json fallback | Créer |
18
- | `lib/composer/registry.js` | Packagist : latest stable + `abandoned` | Créer |
19
- | `lib/codecs/composer.codec.js` | codec composer | Créer |
20
- | `lib/codecs/index.js` | enregistrer composer | Modifier |
21
- | `lib/codecs/recipes.js` | recette composer | Modifier |
22
- | `lib/outdated.js` | findEolProduct → branche composer ; checkOutdatedDeps → maven-only | Modifier |
23
- | `data/eol-mapping.json` | `by_composer_name` (php, laravel, symfony, drupal…) | Modifier |
24
- | `fad-checker.js` | boucle générique `codec.checkRegistry` (eco hors maven/npm) | Modifier |
25
- | `test/fixtures/php-app/` | composer.json + composer.lock | Créer |
26
- | `test/composer.test.js` | parse + codec + coordKey + fallback | Créer |
27
-
28
- **Invariant :** `npm test` reste vert à chaque tâche.
29
-
30
- ---
31
-
32
- ### Task 1: Parser composer.lock + composer.json
33
-
34
- **Files:**
35
- - Create: `lib/composer/parse.js`, `test/fixtures/php-app/composer.lock`, `test/fixtures/php-app/composer.json`
36
- - Test: `test/composer.test.js`
37
-
38
- - [ ] **Step 1: Créer la fixture**
39
-
40
- `test/fixtures/php-app/composer.json` :
41
- ```json
42
- {
43
- "name": "acme/site",
44
- "require": { "guzzlehttp/guzzle": "^7.0", "monolog/monolog": "2.9.1" },
45
- "require-dev": { "phpunit/phpunit": "^10.0" }
46
- }
47
- ```
48
-
49
- `test/fixtures/php-app/composer.lock` :
50
- ```json
51
- {
52
- "packages": [
53
- { "name": "guzzlehttp/guzzle", "version": "7.4.5" },
54
- { "name": "monolog/monolog", "version": "2.9.1" },
55
- { "name": "symfony/console", "version": "v6.2.10" }
56
- ],
57
- "packages-dev": [
58
- { "name": "phpunit/phpunit", "version": "10.1.0" }
59
- ]
60
- }
61
- ```
62
-
63
- - [ ] **Step 2: Écrire le test qui échoue**
64
-
65
- ```js
66
- // test/composer.test.js
67
- const test = require("node:test");
68
- const assert = require("node:assert");
69
- const path = require("path");
70
- const { parseComposerLock, parseComposerJson } = require("../lib/composer/parse");
71
-
72
- const FIX = path.join(__dirname, "fixtures", "php-app");
73
-
74
- test("parseComposerLock reads prod + dev packages, strips leading v", () => {
75
- const r = parseComposerLock(path.join(FIX, "composer.lock"));
76
- const byName = Object.fromEntries(r.deps.map(d => [d.name, d]));
77
- assert.strictEqual(byName["guzzlehttp/guzzle"].version, "7.4.5");
78
- assert.strictEqual(byName["guzzlehttp/guzzle"].scope, "prod");
79
- assert.strictEqual(byName["symfony/console"].version, "6.2.10"); // "v6.2.10" → "6.2.10"
80
- assert.strictEqual(byName["phpunit/phpunit"].scope, "dev");
81
- assert.strictEqual(byName["phpunit/phpunit"].isDev, true);
82
- });
83
-
84
- test("parseComposerJson reads require + require-dev with pinned-vs-range info", () => {
85
- const r = parseComposerJson(path.join(FIX, "composer.json"));
86
- const byName = Object.fromEntries(r.deps.map(d => [d.name, d]));
87
- assert.strictEqual(byName["monolog/monolog"].version, "2.9.1");
88
- assert.strictEqual(byName["guzzlehttp/guzzle"].version, "^7.0");
89
- assert.strictEqual(byName["phpunit/phpunit"].scope, "dev");
90
- });
91
- ```
92
-
93
- - [ ] **Step 3: Lancer (échec attendu)**
94
-
95
- Run: `node --test test/composer.test.js`
96
- Expected: FAIL — `Cannot find module '../lib/composer/parse'`
97
-
98
- - [ ] **Step 4: Implémenter le parser**
99
-
100
- ```js
101
- // lib/composer/parse.js
102
- const fs = require("fs");
103
-
104
- // Composer versions: "1.2.3", "v1.2.3", "dev-main", "1.0.x-dev". On ne garde la
105
- // version brute que pour affichage ; on normalise le "v" de tête pour OSV/registre.
106
- function normVersion(v) {
107
- if (!v) return null;
108
- return String(v).replace(/^v/, "");
109
- }
110
- function isConcrete(v) {
111
- return !!v && /^\d+(\.\d+)*([.\-+]\S+)?$/.test(String(v).replace(/^v/, ""));
112
- }
113
- // Composer name = "vendor/package" (case-insensitive ; lowercased par convention).
114
- function splitName(full) {
115
- const i = full.indexOf("/");
116
- if (i < 0) return { vendor: "", pkg: full };
117
- return { vendor: full.slice(0, i), pkg: full.slice(i + 1) };
118
- }
119
-
120
- function parseComposerLock(filePath) {
121
- const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
122
- const deps = [];
123
- const push = (arr, scope) => {
124
- for (const p of arr || []) {
125
- if (!p.name) continue;
126
- const { vendor, pkg } = splitName(p.name);
127
- deps.push({ name: p.name, vendor, pkg, version: normVersion(p.version), scope, isDev: scope === "dev" });
128
- }
129
- };
130
- push(json.packages, "prod");
131
- push(json["packages-dev"], "dev");
132
- return { manifestPath: filePath, manifestType: "composer.lock", deps };
133
- }
134
-
135
- function parseComposerJson(filePath) {
136
- const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
137
- const deps = [];
138
- const push = (obj, scope) => {
139
- for (const [name, version] of Object.entries(obj || {})) {
140
- if (name === "php" || name.startsWith("ext-") || name.startsWith("lib-")) continue; // platform reqs
141
- const { vendor, pkg } = splitName(name);
142
- deps.push({ name, vendor, pkg, version: String(version), scope, isDev: scope === "dev" });
143
- }
144
- };
145
- push(json.require, "prod");
146
- push(json["require-dev"], "dev");
147
- return { manifestPath: filePath, manifestType: "composer.json", deps };
148
- }
149
-
150
- module.exports = { parseComposerLock, parseComposerJson, normVersion, isConcrete, splitName };
151
- ```
152
-
153
- - [ ] **Step 5: Lancer (succès)**
154
-
155
- Run: `node --test test/composer.test.js`
156
- Expected: PASS (2 tests)
157
-
158
- - [ ] **Step 6: Commit**
159
-
160
- ```bash
161
- git add lib/composer/parse.js test/composer.test.js test/fixtures/php-app/
162
- git commit -m "composer: parse composer.lock + composer.json"
163
- ```
164
-
165
- ---
166
-
167
- ### Task 2: Registre Packagist (latest + abandoned)
168
-
169
- **Files:**
170
- - Create: `lib/composer/registry.js`
171
- - Test: `test/composer.test.js` (ajout, sans réseau — on teste l'extraction pure)
172
-
173
- - [ ] **Step 1: Écrire le test qui échoue**
174
-
175
- ```js
176
- // ajout test/composer.test.js
177
- const { packagistToFindings } = require("../lib/composer/registry");
178
-
179
- test("packagistToFindings extracts latest stable + abandoned flag", () => {
180
- // Forme du endpoint packagist.org/packages/{vendor}/{pkg}.json (sous-ensemble)
181
- const pkg = {
182
- "abandoned": "psr/log",
183
- "versions": {
184
- "2.9.1": { "version": "2.9.1" },
185
- "3.0.0": { "version": "3.0.0" },
186
- "dev-main": { "version": "dev-main" },
187
- "2.8.0": { "version": "2.8.0" },
188
- },
189
- };
190
- const f = packagistToFindings(pkg, { version: "2.9.1" });
191
- assert.strictEqual(f.outdated.latest, "3.0.0"); // plus haute stable, dev-* ignorée
192
- assert.deepStrictEqual(f.abandoned, { replacement: "psr/log" });
193
-
194
- const f2 = packagistToFindings({ "abandoned": true, "versions": { "1.0.0": {} } }, { version: "1.0.0" });
195
- assert.deepStrictEqual(f2.abandoned, { replacement: null }); // abandoned sans remplaçant
196
- assert.strictEqual(f2.outdated, null); // déjà au latest
197
- });
198
- ```
199
-
200
- - [ ] **Step 2: Lancer (échec attendu)**
201
-
202
- Run: `node --test test/composer.test.js`
203
- Expected: FAIL — `packagistToFindings is not a function`
204
-
205
- - [ ] **Step 3: Implémenter le registre**
206
-
207
- ```js
208
- // lib/composer/registry.js
209
- const fs = require("fs");
210
- const path = require("path");
211
- const os = require("os");
212
- let pLimit; try { pLimit = require("p-limit"); } catch { pLimit = (n) => (fn) => fn(); }
213
-
214
- const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
215
- const CACHE_PATH = path.join(CACHE_DIR, "packagist-cache.json");
216
- const CACHE_MAX_AGE_MS = 24 * 3600 * 1000;
217
- const API = "https://packagist.org/packages";
218
-
219
- function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); } catch { return { entries: {}, meta: {} }; } }
220
- function saveCache(d) { try { fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync(CACHE_PATH, JSON.stringify(d)); } catch { /* ignore */ } }
221
-
222
- function isStable(v) { return /^\d+(\.\d+)*$/.test(String(v || "").replace(/^v/, "")); }
223
- function cmp(a, b) {
224
- const pa = a.replace(/^v/, "").split(/[.\-+]/).map(n => parseInt(n, 10) || 0);
225
- const pb = b.replace(/^v/, "").split(/[.\-+]/).map(n => parseInt(n, 10) || 0);
226
- for (let i = 0; i < Math.max(pa.length, pb.length); i++) { const d = (pa[i] || 0) - (pb[i] || 0); if (d) return d > 0 ? 1 : -1; }
227
- return 0;
228
- }
229
-
230
- // Extrait {abandoned, outdated} de la réponse packagist (package.versions + package.abandoned).
231
- function packagistToFindings(pkg, { version }) {
232
- const out = { abandoned: null, outdated: null };
233
- if (pkg.abandoned === true) out.abandoned = { replacement: null };
234
- else if (typeof pkg.abandoned === "string") out.abandoned = { replacement: pkg.abandoned };
235
- const stable = Object.keys(pkg.versions || {}).map(v => v.replace(/^v/, "")).filter(isStable);
236
- if (stable.length) {
237
- const latest = stable.sort(cmp).at(-1);
238
- if (latest && cmp(latest, String(version).replace(/^v/, "")) > 0) out.outdated = { latest };
239
- }
240
- return out;
241
- }
242
-
243
- async function fetchPackage(name, { offline }) {
244
- if (offline) return null;
245
- try {
246
- const res = await fetch(`${API}/${name}.json`, { headers: { "User-Agent": "fad-checker-packagist" } });
247
- if (!res.ok) return { error: `HTTP ${res.status}` };
248
- const json = await res.json();
249
- return json.package || { error: "no package field" };
250
- } catch (e) { return { error: e.message }; }
251
- }
252
-
253
- // Mirror de checkNpmRegistryDeps : renvoie { deprecated:[], outdated:[] }.
254
- async function checkComposerRegistryDeps(deps, opts = {}) {
255
- const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
256
- const targets = [...deps.values()].filter(d => d.ecosystem === "composer" && d.version);
257
- const result = { deprecated: [], outdated: [] };
258
- if (!targets.length) return result;
259
- const cache = loadCache();
260
- const fresh = cache.meta?.fetchedAt && (Date.now() - cache.meta.fetchedAt) < CACHE_MAX_AGE_MS;
261
- if (!fresh && !offline) cache.entries = {};
262
- const limit = pLimit(concurrency);
263
- await Promise.all(targets.map(t => limit(async () => {
264
- const name = `${t.namespace}/${t.name}`.toLowerCase();
265
- const key = `${name}@${t.version}`;
266
- let ex = cache.entries[key];
267
- if (!ex) {
268
- const pkg = await fetchPackage(name, { offline });
269
- if (pkg && !pkg.error) { const f = packagistToFindings(pkg, { version: t.version }); ex = { abandoned: f.abandoned, latest: f.outdated?.latest || null }; cache.entries[key] = ex; }
270
- else ex = { abandoned: null, latest: null };
271
- }
272
- if (ex.abandoned) result.deprecated.push({ dep: t, severity: "MEDIUM", replacement: ex.abandoned.replacement, reason: "Package marked abandoned on Packagist", source: "packagist" });
273
- if (allLibs && ex.latest) result.outdated.push({ dep: t, latest: ex.latest, releaseDate: null });
274
- })));
275
- cache.meta = { fetchedAt: Date.now() }; saveCache(cache);
276
- return result;
277
- }
278
-
279
- module.exports = { packagistToFindings, checkComposerRegistryDeps };
280
- ```
281
-
282
- - [ ] **Step 4: Lancer (succès)**
283
-
284
- Run: `node --test test/composer.test.js`
285
- Expected: PASS (3 tests)
286
-
287
- - [ ] **Step 5: Commit**
288
-
289
- ```bash
290
- git add lib/composer/registry.js test/composer.test.js
291
- git commit -m "composer: Packagist registry (latest stable + abandoned)"
292
- ```
293
-
294
- ---
295
-
296
- ### Task 3: Le codec composer + enregistrement + recette + EOL
297
-
298
- **Files:**
299
- - Create: `lib/codecs/composer.codec.js`
300
- - Modify: `lib/codecs/index.js`, `lib/codecs/recipes.js`, `lib/outdated.js`, `data/eol-mapping.json`
301
-
302
- - [ ] **Step 1: Écrire le test qui échoue**
303
-
304
- ```js
305
- // ajout test/composer.test.js
306
- const composer = require("../lib/codecs/composer.codec");
307
- const { assertCodecShape } = require("../lib/codecs/codec.interface");
308
-
309
- test("composer codec: shape, detect, collect with composer:vendor/pkg coordKeys", async () => {
310
- assertCodecShape(composer);
311
- assert.strictEqual(composer.detect(FIX), true);
312
- const { deps } = await composer.collect(FIX, {});
313
- const g = deps.get("composer:guzzlehttp/guzzle");
314
- assert.ok(g, "guzzle should be collected under composer:guzzlehttp/guzzle");
315
- assert.strictEqual(g.ecosystem, "composer");
316
- assert.strictEqual(g.namespace, "guzzlehttp");
317
- assert.strictEqual(g.name, "guzzle");
318
- assert.strictEqual(composer.osvPackageName(g), "guzzlehttp/guzzle");
319
- assert.strictEqual(composer.formatCoord(g), "guzzlehttp/guzzle");
320
- });
321
-
322
- test("composer collect falls back to composer.json (pinned only) with warning when no lock", async () => {
323
- const os2 = require("os"); const fs2 = require("fs"); const p2 = require("path");
324
- const dir = fs2.mkdtempSync(p2.join(os2.tmpdir(), "composer-nolock-"));
325
- fs2.writeFileSync(p2.join(dir, "composer.json"), JSON.stringify({ name: "x/y", require: { "a/pinned": "1.2.3", "b/range": "^2.0" } }));
326
- const { deps, warnings } = await composer.collect(dir, {});
327
- assert.ok(deps.has("composer:a/pinned"));
328
- assert.ok(!deps.has("composer:b/range"));
329
- assert.ok(warnings.find(w => w.type === "no-lockfile"));
330
- });
331
- ```
332
-
333
- - [ ] **Step 2: Lancer (échec attendu)**
334
-
335
- Run: `node --test test/composer.test.js`
336
- Expected: FAIL — `Cannot find module '../lib/codecs/composer.codec'`
337
-
338
- - [ ] **Step 3a: Écrire le codec**
339
-
340
- ```js
341
- // lib/codecs/composer.codec.js
342
- const fs = require("fs");
343
- const path = require("path");
344
- const { makeDepRecord, coordKeyFor } = require("../dep-record");
345
- const { parseComposerLock, parseComposerJson, isConcrete } = require("../composer/parse");
346
-
347
- const SKIP = new Set(["vendor", ".git", ".idea", ".vscode", "node_modules", "dist", "build", "out"]);
348
-
349
- function findComposerManifests(dir) {
350
- const groups = [];
351
- const stack = [dir];
352
- while (stack.length) {
353
- const cur = stack.pop();
354
- let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
355
- const names = new Set(entries.filter(e => e.isFile()).map(e => e.name));
356
- if (names.has("composer.json") || names.has("composer.lock")) {
357
- groups.push({
358
- dir: cur,
359
- composerJson: names.has("composer.json") ? path.join(cur, "composer.json") : null,
360
- composerLock: names.has("composer.lock") ? path.join(cur, "composer.lock") : null,
361
- });
362
- }
363
- for (const e of entries) if (e.isDirectory() && !SKIP.has(e.name)) stack.push(path.join(cur, e.name));
364
- }
365
- return groups;
366
- }
367
-
368
- module.exports = {
369
- id: "composer",
370
- label: "Composer",
371
- osvEcosystem: "Packagist",
372
- manifestNames: ["composer.json", "composer.lock"],
373
-
374
- detect(dir) { return findComposerManifests(dir).length > 0; },
375
-
376
- async collect(dir, opts = {}) {
377
- const { ignoreTest, deps2Exclude } = opts;
378
- const out = new Map();
379
- const warnings = [];
380
- for (const g of findComposerManifests(dir)) {
381
- if (g.composerLock) {
382
- const { deps } = parseComposerLock(g.composerLock);
383
- for (const d of deps) {
384
- if (ignoreTest && d.isDev) continue;
385
- if (deps2Exclude && deps2Exclude.test(d.name)) continue;
386
- out.set(coordKeyFor("composer", d.vendor, d.pkg),
387
- makeDepRecord({ ecosystem: "composer", namespace: d.vendor, name: d.pkg, version: d.version, manifestPath: g.composerLock, scope: d.scope, isDev: d.isDev }));
388
- }
389
- } else if (g.composerJson) {
390
- // no lock → best-effort: pinned exact versions only + warning
391
- const { deps } = parseComposerJson(g.composerJson);
392
- let pinned = 0, ranges = 0;
393
- for (const d of deps) {
394
- if (ignoreTest && d.isDev) continue;
395
- if (deps2Exclude && deps2Exclude.test(d.name)) continue;
396
- if (isConcrete(d.version)) {
397
- out.set(coordKeyFor("composer", d.vendor, d.pkg),
398
- makeDepRecord({ ecosystem: "composer", namespace: d.vendor, name: d.pkg, version: d.version.replace(/^v/, ""), manifestPath: g.composerJson, scope: d.scope, isDev: d.isDev }));
399
- pinned++;
400
- } else ranges++;
401
- }
402
- warnings.push({ type: "no-lockfile", manifestPath: g.composerJson, message: `composer.json without composer.lock — best-effort: ${pinned} pinned, ${ranges} range(s) skipped (run "composer install")` });
403
- }
404
- }
405
- return { deps: out, warnings };
406
- },
407
-
408
- coordKey(d) { return coordKeyFor("composer", d.namespace, d.name); },
409
- formatCoord(d) { return `${d.namespace}/${d.name}`; },
410
- osvPackageName(d) { return `${d.namespace}/${d.name}`; },
411
-
412
- async checkRegistry(deps, opts = {}) {
413
- const { checkComposerRegistryDeps } = require("../composer/registry");
414
- return checkComposerRegistryDeps(deps, opts);
415
- },
416
- resolveEolProduct(d) { return require("../outdated").findEolProduct(d); },
417
- recipe: require("./recipes").composer,
418
- nativeScanners: [],
419
- };
420
- ```
421
-
422
- - [ ] **Step 3b: Enregistrer le codec** — `lib/codecs/index.js`
423
-
424
- ```js
425
- const composer = require("./composer.codec");
426
- // …
427
- for (const c of [maven, npm, yarn, composer]) { assertCodecShape(c); REGISTRY.set(c.id, c); }
428
- ```
429
-
430
- - [ ] **Step 3c: Recette composer** — ajouter dans `lib/codecs/recipes.js`
431
-
432
- ```js
433
- function composerRequireSnippet(items) {
434
- return items.map(it => `composer require ${it.groupId ? it.groupId + "/" : ""}${it.artifactId}:^${it.fixVersion}`).join("\n");
435
- }
436
- const composer = {
437
- label: "Composer",
438
- pinSection: "A. Update the abandoned/vulnerable packages",
439
- pinIntro: cnt => `Run for the ${cnt} affected package${cnt > 1 ? "s" : ""}:`,
440
- snippet: composerRequireSnippet,
441
- directSection: "B. Then commit the updated composer.lock",
442
- };
443
- module.exports = { maven, npm, yarn, composer, /* …existing exports… */ };
444
- ```
445
-
446
- - [ ] **Step 3d: EOL composer** — `lib/outdated.js` `findEolProduct`, ajouter AVANT le bloc maven (`const key = ...`) :
447
-
448
- ```js
449
- if (dep.ecosystem === "composer") {
450
- const full = `${dep.namespace || dep.groupId}/${dep.name || dep.artifactId}`.toLowerCase();
451
- return EOL_MAPPING.by_composer_name?.[full] || null;
452
- }
453
- ```
454
-
455
- - [ ] **Step 3e: `by_composer_name`** — `data/eol-mapping.json`, ajouter une clé top-level :
456
-
457
- ```json
458
- "by_composer_name": {
459
- "laravel/framework": { "product": "laravel", "label": "Laravel" },
460
- "symfony/symfony": { "product": "symfony", "label": "Symfony" },
461
- "symfony/console": { "product": "symfony", "label": "Symfony" },
462
- "drupal/core": { "product": "drupal", "label": "Drupal" }
463
- }
464
- ```
465
-
466
- - [ ] **Step 4: Lancer (succès)**
467
-
468
- Run: `node --test test/composer.test.js`
469
- Expected: PASS (5 tests).
470
-
471
- - [ ] **Step 5: Commit**
472
-
473
- ```bash
474
- git add lib/codecs/composer.codec.js lib/codecs/index.js lib/codecs/recipes.js lib/outdated.js data/eol-mapping.json test/composer.test.js
475
- git commit -m "composer: codec + registry wiring + EOL mapping + recipe"
476
- ```
477
-
478
- ---
479
-
480
- ### Task 4: Câbler checkRegistry générique dans l'orchestrateur + fix Maven outdated
481
-
482
- **Files:**
483
- - Modify: `lib/outdated.js` (`checkOutdatedDeps` filtre maven-only), `fad-checker.js` (boucle `codec.checkRegistry` pour eco hors maven/npm)
484
-
485
- - [ ] **Step 1: Fix du filtre Maven outdated** — `lib/outdated.js:248`
486
-
487
- ```js
488
- const list = [...resolvedDeps.values()].filter(d => d.version && !/\$\{|SNAPSHOT/i.test(d.version) && d.ecosystem === "maven");
489
- ```
490
- (était `d.ecosystem !== "npm"` — laissait fuir composer/pypi/nuget vers Maven Central.)
491
-
492
- - [ ] **Step 2: Boucle checkRegistry** — `fad-checker.js`, juste après le bloc npm-registry (étape 4a), ajouter :
493
-
494
- ```js
495
- // 4a-bis. Per-codec registry for non-maven/npm ecosystems (composer/pypi/nuget).
496
- // maven + npm are already covered above (outdated.js + npm registry).
497
- for (const id of activeIds) {
498
- if (id === "maven" || id === "npm" || id === "yarn") continue;
499
- const codec = getCodec(id);
500
- if (!codec?.checkRegistry) continue;
501
- try {
502
- const reg = await codec.checkRegistry(resolved, { verbose, offline, allLibs: options.allLibs });
503
- obsoleteResults = obsoleteResults.concat(reg.deprecated || []);
504
- outdatedResults = outdatedResults.concat(reg.outdated || []);
505
- } catch (err) { console.warn(chalk.yellow(`⚠️ ${id} registry check skipped:`), err.message); }
506
- }
507
- ```
508
-
509
- > Placement : APRÈS la section `4a` (npm registry) et AVANT le dedup cross-section
510
- > `eolKeys/obsKeys` (qui utilise `dep.coordKey`/`groupId:artifactId` — déjà compatible).
511
- > Vérifier que `outdatedResults`/`obsoleteResults` sont `let` (ils le sont).
512
-
513
- - [ ] **Step 3: Lancer la suite complète**
514
-
515
- Run: `npm test`
516
- Expected: PASS (tous + composer).
517
-
518
- - [ ] **Step 4: Smoke test bout-en-bout (offline)**
519
-
520
- Run: `node fad-checker.js -s ./test/fixtures/php-app --offline --report-output /tmp/fad-php`
521
- Expected: détecte composer, liste les 4 packages, génère le report sans erreur, aucune requête Maven Central pour les deps composer.
522
-
523
- - [ ] **Step 5: Commit**
524
-
525
- ```bash
526
- git add lib/outdated.js fad-checker.js
527
- git commit -m "Wire per-codec checkRegistry for non-maven/npm; Maven outdated maven-only"
528
- ```
529
-
530
- ---
531
-
532
- ### Task 5: Docs
533
-
534
- **Files:** `CLAUDE.md`, `docs/ARCHITECTURE.md`
535
-
536
- - [ ] **Step 1** : ajouter `lib/composer/*` + `composer.codec.js` aux cartes de modules ; mentionner Composer (PHP) dans la liste des écosystèmes supportés (point 2/3 de l'intro CLAUDE.md) ; ajouter `php-app/` aux fixtures.
537
- - [ ] **Step 2** : `npm test` (sanity) puis commit.
538
-
539
- ```bash
540
- git add CLAUDE.md docs/ARCHITECTURE.md
541
- git commit -m "Docs: Composer (PHP) codec"
542
- ```
543
-
544
- ---
545
-
546
- ## Self-Review (effectuée)
547
-
548
- - Vuln composer → OSV `Packagist` déjà câblé (Plan A Task 7) ✓ ; parse lock+json+fallback → Task 1 ✓ ;
549
- abandoned+outdated → Packagist Task 2 + wiring Task 4 ✓ ; EOL → findEolProduct + mapping Task 3 ✓ ;
550
- recette → Task 3c ✓ ; report (sections/labels/ordre) → déjà piloté par codec via le registre (Plan A Task 8),
551
- composer apparaît dès son enregistrement ✓ ; CLI `--no-composer`/`--ecosystem composer` → déjà géré par
552
- select.js + les flags `--no-<id>` de Plan A ✓.
553
- - **Bug corrigé** : `checkOutdatedDeps` filtrait `!== "npm"` → aurait interrogé Maven Central pour des
554
- deps composer. Task 4 Step 1 le restreint à maven.
555
- - **Cohérence types** : depRecord composer via makeDepRecord ; checkRegistry → {deprecated[], outdated[]}
556
- (même forme que npm) ; deprecated entry {dep,severity,replacement,reason,source}.