fad-checker 1.0.5 → 2.0.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 (71) hide show
  1. package/CHANGELOG.md +47 -0
  2. package/CLAUDE.md +28 -9
  3. package/README.md +27 -13
  4. package/completions/fad-checker.bash +4 -1
  5. package/completions/fad-checker.zsh +9 -0
  6. package/data/eol-mapping.json +17 -0
  7. package/docs/ARCHITECTURE.md +35 -7
  8. package/docs/USAGE.md +15 -7
  9. package/docs/superpowers/plans/2026-05-29-codec-composer.md +556 -0
  10. package/docs/superpowers/plans/2026-05-29-codec-foundation.md +851 -0
  11. package/docs/superpowers/plans/2026-05-29-codec-nuget.md +432 -0
  12. package/docs/superpowers/plans/2026-05-29-codec-pypi.md +450 -0
  13. package/docs/superpowers/specs/2026-05-29-codecs-multi-ecosystem-design.md +251 -0
  14. package/fad-checker.js +109 -83
  15. package/lib/codecs/codec.interface.js +27 -0
  16. package/lib/codecs/composer/parse.js +59 -0
  17. package/lib/codecs/composer/registry.js +92 -0
  18. package/lib/codecs/composer.codec.js +93 -0
  19. package/lib/codecs/index.js +37 -0
  20. package/lib/codecs/maven.codec.js +71 -0
  21. package/lib/{npm → codecs/npm}/collect.js +32 -18
  22. package/lib/codecs/npm.codec.js +52 -0
  23. package/lib/codecs/nuget/parse.js +75 -0
  24. package/lib/codecs/nuget/registry.js +88 -0
  25. package/lib/codecs/nuget.codec.js +92 -0
  26. package/lib/codecs/pypi/parse.js +71 -0
  27. package/lib/codecs/pypi/registry.js +89 -0
  28. package/lib/codecs/pypi.codec.js +81 -0
  29. package/lib/codecs/recipes.js +108 -0
  30. package/lib/codecs/select.js +27 -0
  31. package/lib/codecs/yarn.codec.js +19 -0
  32. package/lib/cve-match.js +30 -14
  33. package/lib/cve-report.js +130 -83
  34. package/lib/dep-record.js +60 -0
  35. package/lib/osv.js +33 -19
  36. package/lib/outdated.js +13 -2
  37. package/package.json +3 -2
  38. package/test/cli-ecosystem.test.js +30 -0
  39. package/test/codec-edge-cases.test.js +131 -0
  40. package/test/codec-integration.test.js +119 -0
  41. package/test/codecs.test.js +92 -0
  42. package/test/composer.test.js +71 -0
  43. package/test/cve-match.test.js +40 -0
  44. package/test/cve-report.test.js +9 -0
  45. package/test/dep-record.test.js +31 -0
  46. package/test/fixtures/csharp-config/packages.config +4 -0
  47. package/test/fixtures/csharp-csproj/Directory.Packages.props +5 -0
  48. package/test/fixtures/csharp-csproj/app.csproj +7 -0
  49. package/test/fixtures/csharp-lock/packages.lock.json +6 -0
  50. package/test/fixtures/monorepo-mixed/packages/no-lock/package.json +2 -1
  51. package/test/fixtures/php-app/composer.json +5 -0
  52. package/test/fixtures/php-app/composer.lock +10 -0
  53. package/test/fixtures/polyglot/cs/packages.lock.json +1 -0
  54. package/test/fixtures/polyglot/js/package-lock.json +1 -0
  55. package/test/fixtures/polyglot/js/package.json +1 -0
  56. package/test/fixtures/polyglot/mvn/pom.xml +7 -0
  57. package/test/fixtures/polyglot/php/composer.lock +1 -0
  58. package/test/fixtures/polyglot/py/poetry.lock +3 -0
  59. package/test/fixtures/python-pipenv/Pipfile.lock +1 -0
  60. package/test/fixtures/python-poetry/poetry.lock +7 -0
  61. package/test/fixtures/python-reqs/requirements.txt +5 -0
  62. package/test/fixtures/python-uv/uv.lock +3 -0
  63. package/test/monorepo.test.js +1 -1
  64. package/test/npm-registry.test.js +1 -1
  65. package/test/npm.test.js +8 -4
  66. package/test/nuget.test.js +66 -0
  67. package/test/osv.test.js +62 -0
  68. package/test/pypi.test.js +72 -0
  69. package/test/webjar.test.js +1 -1
  70. /package/lib/{npm → codecs/npm}/parse.js +0 -0
  71. /package/lib/{npm → codecs/npm}/registry.js +0 -0
@@ -0,0 +1,432 @@
1
+ # Plan D — Codec NuGet (C#/.NET) Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: superpowers:executing-plans. Steps use checkbox (`- [ ]`) syntax.
4
+
5
+ **Goal:** Ajouter le codec `nuget` (C#/.NET) en parité — vuln (OSV NuGet), deprecation (NuGet registration), outdated (NuGet), EOL (dotnet/aspnet), recette — sur l'interface codec.
6
+
7
+ **Architecture:** `lib/nuget/parse.js` parse `packages.lock.json` (JSON), `*.csproj` + `Directory.Packages.props` (XML via xml2js), `packages.config` (XML). `lib/nuget/registry.js` interroge la registration NuGet (latest + deprecation). `lib/codecs/nuget.codec.js` assemble. OSV `NuGet` (Plan A) + boucle `checkRegistry` (Plan B) déjà câblés — aucune modif orchestrateur. Noms NuGet case-insensitive (clé en lower, casse d'origine pour l'affichage).
8
+
9
+ **Tech Stack:** Node.js, node --test, `xml2js` (déjà dépendance).
10
+
11
+ ---
12
+
13
+ ## File Structure
14
+
15
+ | Fichier | Rôle | Action |
16
+ |---|---|---|
17
+ | `lib/nuget/parse.js` | packages.lock.json + .csproj (+CPM) + packages.config | Créer |
18
+ | `lib/nuget/registry.js` | NuGet registration → latest + deprecation | Créer |
19
+ | `lib/codecs/nuget.codec.js` | codec nuget | Créer |
20
+ | `lib/codecs/index.js` | enregistrer nuget | Modifier |
21
+ | `lib/codecs/recipes.js` | recette nuget | Modifier |
22
+ | `lib/outdated.js` | findEolProduct → branche nuget | Modifier |
23
+ | `data/eol-mapping.json` | `by_nuget_name` (.NET, aspnet, EF) | Modifier |
24
+ | `test/fixtures/csharp-*/` | lock + csproj+CPM + packages.config | Créer |
25
+ | `test/nuget.test.js` | parsers + codec + CPM + case-insensitive | Créer |
26
+ | `CLAUDE.md`, `docs/ARCHITECTURE.md` | docs | Modifier |
27
+
28
+ **Invariant :** `npm test` reste vert à chaque tâche.
29
+
30
+ ---
31
+
32
+ ### Task 1: Parsers + fixtures
33
+
34
+ - [ ] **Step 1: Fixtures**
35
+
36
+ `test/fixtures/csharp-lock/packages.lock.json` :
37
+ ```json
38
+ { "version": 1, "dependencies": {
39
+ "net6.0": {
40
+ "Newtonsoft.Json": { "type": "Direct", "resolved": "13.0.1" },
41
+ "Serilog": { "type": "Direct", "resolved": "2.12.0" },
42
+ "System.Buffers": { "type": "Transitive", "resolved": "4.5.1" }
43
+ } } }
44
+ ```
45
+
46
+ `test/fixtures/csharp-csproj/app.csproj` :
47
+ ```xml
48
+ <Project Sdk="Microsoft.NET.Sdk">
49
+ <ItemGroup>
50
+ <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
51
+ <PackageReference Include="Floating" Version="1.*" />
52
+ <PackageReference Include="Managed" />
53
+ </ItemGroup>
54
+ </Project>
55
+ ```
56
+ `test/fixtures/csharp-csproj/Directory.Packages.props` :
57
+ ```xml
58
+ <Project>
59
+ <ItemGroup>
60
+ <PackageVersion Include="Managed" Version="6.0.0" />
61
+ </ItemGroup>
62
+ </Project>
63
+ ```
64
+
65
+ `test/fixtures/csharp-config/packages.config` :
66
+ ```xml
67
+ <?xml version="1.0" encoding="utf-8"?>
68
+ <packages>
69
+ <package id="EntityFramework" version="6.4.4" targetFramework="net48" />
70
+ </packages>
71
+ ```
72
+
73
+ - [ ] **Step 2: Test qui échoue** — `test/nuget.test.js`
74
+
75
+ ```js
76
+ const test = require("node:test");
77
+ const assert = require("node:assert");
78
+ const path = require("path");
79
+ const { parsePackagesLockJson, parseCsproj, parsePackagesConfig, parseDirectoryPackagesProps } = require("../lib/nuget/parse");
80
+ const F = n => path.join(__dirname, "fixtures", n);
81
+
82
+ test("parsePackagesLockJson reads resolved versions + Direct/Transitive scope", async () => {
83
+ const r = await parsePackagesLockJson(F("csharp-lock/packages.lock.json"));
84
+ const m = Object.fromEntries(r.deps.map(d => [d.name, d]));
85
+ assert.strictEqual(m["Newtonsoft.Json"].version, "13.0.1");
86
+ assert.strictEqual(m["System.Buffers"].scope, "transitive");
87
+ });
88
+
89
+ test("parseDirectoryPackagesProps returns a name→version map (CPM)", async () => {
90
+ const m = await parseDirectoryPackagesProps(F("csharp-csproj/Directory.Packages.props"));
91
+ assert.strictEqual(m["managed"], "6.0.0"); // keyed lowercase
92
+ });
93
+
94
+ test("parseCsproj: pinned scanned, floating skipped, CPM resolved against props", async () => {
95
+ const cpm = await parseDirectoryPackagesProps(F("csharp-csproj/Directory.Packages.props"));
96
+ const r = await parseCsproj(F("csharp-csproj/app.csproj"), cpm);
97
+ const m = Object.fromEntries(r.deps.map(d => [d.name, d.version]));
98
+ assert.strictEqual(m["Newtonsoft.Json"], "13.0.1");
99
+ assert.strictEqual(m["Managed"], "6.0.0"); // resolved via CPM
100
+ assert.ok(!("Floating" in m)); // "1.*" skipped
101
+ assert.strictEqual(r.skipped, 1);
102
+ });
103
+
104
+ test("parsePackagesConfig reads legacy id/version", async () => {
105
+ const r = await parsePackagesConfig(F("csharp-config/packages.config"));
106
+ assert.strictEqual(r.deps.find(d => d.name === "EntityFramework").version, "6.4.4");
107
+ });
108
+ ```
109
+
110
+ - [ ] **Step 3: Lancer (échec attendu)**.
111
+
112
+ - [ ] **Step 4: Implémenter**
113
+
114
+ ```js
115
+ // lib/nuget/parse.js
116
+ const fs = require("fs");
117
+ const xml2js = require("xml2js");
118
+
119
+ function isConcrete(v) { return /^\d+(\.\d+)*([.\-+]\S+)?$/.test(String(v || "")); } // rejects 1.*, [1.0,2.0)
120
+
121
+ async function parsePackagesLockJson(filePath) {
122
+ const json = JSON.parse(fs.readFileSync(filePath, "utf8"));
123
+ const deps = [];
124
+ const seen = new Set();
125
+ for (const fw of Object.values(json.dependencies || {})) {
126
+ for (const [name, meta] of Object.entries(fw || {})) {
127
+ const version = meta.resolved || null;
128
+ if (!version) continue;
129
+ const key = `${name.toLowerCase()}@${version}`;
130
+ if (seen.has(key)) continue; seen.add(key);
131
+ const scope = (meta.type === "Transitive") ? "transitive" : "prod";
132
+ deps.push({ name, version, scope, isDev: false });
133
+ }
134
+ }
135
+ return { manifestPath: filePath, manifestType: "packages.lock.json", deps };
136
+ }
137
+
138
+ async function parseDirectoryPackagesProps(filePath) {
139
+ const xml = await xml2js.parseStringPromise(fs.readFileSync(filePath, "utf8"));
140
+ const map = {};
141
+ for (const ig of xml.Project?.ItemGroup || []) {
142
+ for (const pv of ig.PackageVersion || []) {
143
+ const id = pv.$?.Include; const v = pv.$?.Version;
144
+ if (id && v) map[id.toLowerCase()] = v;
145
+ }
146
+ }
147
+ return map;
148
+ }
149
+
150
+ async function parseCsproj(filePath, cpm = {}) {
151
+ const xml = await xml2js.parseStringPromise(fs.readFileSync(filePath, "utf8"));
152
+ const deps = [];
153
+ let skipped = 0;
154
+ for (const ig of xml.Project?.ItemGroup || []) {
155
+ for (const pr of ig.PackageReference || []) {
156
+ const name = pr.$?.Include; if (!name) continue;
157
+ // Version may be an attribute or a child element; if absent, use CPM.
158
+ let version = pr.$?.Version || (Array.isArray(pr.Version) ? pr.Version[0] : null) || cpm[name.toLowerCase()] || null;
159
+ if (version && isConcrete(version)) deps.push({ name, version, scope: "prod", isDev: false });
160
+ else skipped++;
161
+ }
162
+ }
163
+ return { manifestPath: filePath, manifestType: "csproj", deps, skipped };
164
+ }
165
+
166
+ async function parsePackagesConfig(filePath) {
167
+ const xml = await xml2js.parseStringPromise(fs.readFileSync(filePath, "utf8"));
168
+ const deps = [];
169
+ for (const p of xml.packages?.package || []) {
170
+ const name = p.$?.id; const version = p.$?.version;
171
+ if (name && version && isConcrete(version)) deps.push({ name, version, scope: "prod", isDev: false });
172
+ }
173
+ return { manifestPath: filePath, manifestType: "packages.config", deps };
174
+ }
175
+
176
+ module.exports = { isConcrete, parsePackagesLockJson, parseDirectoryPackagesProps, parseCsproj, parsePackagesConfig };
177
+ ```
178
+
179
+ - [ ] **Step 5: Lancer (succès)** → PASS (4). **Step 6: Commit** `nuget: parse packages.lock.json + csproj (CPM) + packages.config`.
180
+
181
+ ---
182
+
183
+ ### Task 2: Registre NuGet (latest + deprecation)
184
+
185
+ - [ ] **Step 1: Test qui échoue**
186
+
187
+ ```js
188
+ // ajout test/nuget.test.js
189
+ const { nugetRegistrationToFindings } = require("../lib/nuget/registry");
190
+ test("nugetRegistrationToFindings extracts latest stable + deprecation for version", () => {
191
+ const reg = { items: [ { items: [
192
+ { catalogEntry: { version: "13.0.1", deprecation: { reasons: ["Legacy"], alternatePackage: { id: "NewPkg" } } } },
193
+ { catalogEntry: { version: "13.0.3" } },
194
+ { catalogEntry: { version: "14.0.0-preview" } },
195
+ ] } ] };
196
+ const f = nugetRegistrationToFindings(reg, { version: "13.0.1" });
197
+ assert.strictEqual(f.outdated.latest, "13.0.3"); // preview ignored
198
+ assert.deepStrictEqual(f.deprecated, { reason: "Legacy", replacement: "NewPkg" });
199
+ const f2 = nugetRegistrationToFindings(reg, { version: "13.0.3" });
200
+ assert.strictEqual(f2.deprecated, null);
201
+ assert.strictEqual(f2.outdated, null);
202
+ });
203
+ ```
204
+
205
+ - [ ] **Step 2: Lancer (échec attendu)**.
206
+
207
+ - [ ] **Step 3: Implémenter**
208
+
209
+ ```js
210
+ // lib/nuget/registry.js
211
+ const fs = require("fs");
212
+ const path = require("path");
213
+ const os = require("os");
214
+ let pLimit; try { pLimit = require("p-limit"); } catch { pLimit = () => (fn) => fn(); }
215
+
216
+ const CACHE_DIR = path.join(os.homedir(), ".fad-checker");
217
+ const CACHE_PATH = path.join(CACHE_DIR, "nuget-cache.json");
218
+ const CACHE_MAX_AGE_MS = 24 * 3600 * 1000;
219
+ const REG = "https://api.nuget.org/v3/registration5-gz-semver2";
220
+
221
+ function loadCache() { try { return JSON.parse(fs.readFileSync(CACHE_PATH, "utf8")); } catch { return { entries: {}, meta: {} }; } }
222
+ function saveCache(d) { try { fs.mkdirSync(CACHE_DIR, { recursive: true }); fs.writeFileSync(CACHE_PATH, JSON.stringify(d)); } catch { /* ignore */ } }
223
+ function isStable(v) { return /^\d+(\.\d+)*$/.test(String(v || "")); }
224
+ function cmp(a, b) { const pa = String(a).split(/[.\-+]/).map(n => parseInt(n, 10) || 0); const pb = String(b).split(/[.\-+]/).map(n => parseInt(n, 10) || 0); 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; } return 0; }
225
+
226
+ // Walk a registration index (inline items) → {outdated, deprecated}.
227
+ function nugetRegistrationToFindings(reg, { version }) {
228
+ const entries = [];
229
+ for (const page of reg.items || []) for (const leaf of page.items || []) if (leaf.catalogEntry) entries.push(leaf.catalogEntry);
230
+ const out = { outdated: null, deprecated: null };
231
+ const stable = entries.map(e => e.version).filter(isStable);
232
+ if (stable.length) { const latest = stable.sort(cmp).at(-1); if (latest && cmp(latest, version) > 0) out.outdated = { latest }; }
233
+ const mine = entries.find(e => String(e.version) === String(version));
234
+ if (mine?.deprecation) out.deprecated = { reason: (mine.deprecation.reasons || []).join(", ") || "deprecated", replacement: mine.deprecation.alternatePackage?.id || null };
235
+ return out;
236
+ }
237
+
238
+ async function fetchRegistration(name, { offline }) {
239
+ if (offline) return null;
240
+ try {
241
+ const res = await fetch(`${REG}/${name.toLowerCase()}/index.json`, { headers: { "User-Agent": "fad-checker-nuget" } });
242
+ if (!res.ok) return { error: `HTTP ${res.status}` };
243
+ return await res.json();
244
+ } catch (e) { return { error: e.message }; }
245
+ }
246
+
247
+ async function checkNugetRegistryDeps(deps, opts = {}) {
248
+ const { verbose, offline, allLibs = true, concurrency = 8 } = opts;
249
+ const targets = [...deps.values()].filter(d => d.ecosystem === "nuget" && d.version);
250
+ const result = { deprecated: [], outdated: [] };
251
+ if (!targets.length) return result;
252
+ const cache = loadCache();
253
+ const fresh = cache.meta?.fetchedAt && (Date.now() - cache.meta.fetchedAt) < CACHE_MAX_AGE_MS;
254
+ if (!fresh && !offline) cache.entries = {};
255
+ const limit = pLimit(concurrency);
256
+ await Promise.all(targets.map(t => limit(async () => {
257
+ const key = `${t.name.toLowerCase()}@${t.version}`;
258
+ let ex = cache.entries[key];
259
+ if (!ex) {
260
+ const reg = await fetchRegistration(t.name, { offline });
261
+ if (reg && !reg.error) { const f = nugetRegistrationToFindings(reg, { version: t.version }); ex = { deprecated: f.deprecated, latest: f.outdated?.latest || null }; cache.entries[key] = ex; }
262
+ else ex = { deprecated: null, latest: null };
263
+ }
264
+ if (ex.deprecated) result.deprecated.push({ dep: t, severity: "MEDIUM", replacement: ex.deprecated.replacement, reason: ex.deprecated.reason, source: "nuget" });
265
+ if (allLibs && ex.latest) result.outdated.push({ dep: t, latest: ex.latest, releaseDate: null });
266
+ })));
267
+ cache.meta = { fetchedAt: Date.now() }; saveCache(cache);
268
+ return result;
269
+ }
270
+
271
+ module.exports = { nugetRegistrationToFindings, checkNugetRegistryDeps };
272
+ ```
273
+
274
+ - [ ] **Step 4: Lancer (succès)** → PASS (5). **Step 5: Commit** `nuget: registration registry (latest + deprecation)`.
275
+
276
+ ---
277
+
278
+ ### Task 3: Codec nuget + register + recipe + EOL
279
+
280
+ - [ ] **Step 1: Test qui échoue**
281
+
282
+ ```js
283
+ // ajout test/nuget.test.js
284
+ const nuget = require("../lib/codecs/nuget.codec");
285
+ const { assertCodecShape } = require("../lib/codecs/codec.interface");
286
+ test("nuget codec: shape, detect, collect lockfile, case-insensitive key", async () => {
287
+ assertCodecShape(nuget);
288
+ assert.strictEqual(nuget.detect(F("csharp-lock")), true);
289
+ const { deps } = await nuget.collect(F("csharp-lock"), {});
290
+ const j = deps.get("nuget:newtonsoft.json"); // key lowercased
291
+ assert.ok(j);
292
+ assert.strictEqual(j.name, "Newtonsoft.Json"); // display keeps original case
293
+ assert.strictEqual(nuget.osvPackageName(j), "Newtonsoft.Json");
294
+ });
295
+ test("nuget codec: csproj uses CPM + skips floating with warning", async () => {
296
+ const { deps, warnings } = await nuget.collect(F("csharp-csproj"), {});
297
+ assert.ok(deps.has("nuget:newtonsoft.json"));
298
+ assert.ok(deps.has("nuget:managed")); // resolved via Directory.Packages.props
299
+ assert.ok(!deps.has("nuget:floating"));
300
+ assert.ok(warnings.find(w => w.type === "unresolved-versions" || w.type === "no-lockfile"));
301
+ });
302
+ ```
303
+ Update `test/codecs.test.js` registry id list → `["composer","maven","npm","nuget","pypi","yarn"]`.
304
+
305
+ - [ ] **Step 2: Lancer (échec attendu)**.
306
+
307
+ - [ ] **Step 3a: Codec**
308
+
309
+ ```js
310
+ // lib/codecs/nuget.codec.js
311
+ const fs = require("fs");
312
+ const path = require("path");
313
+ const { makeDepRecord, coordKeyFor } = require("../dep-record");
314
+ const N = require("../nuget/parse");
315
+
316
+ const SKIP = new Set([".git", ".idea", ".vscode", "node_modules", "dist", "build", "out", "bin", "obj", "target", "packages"]);
317
+
318
+ function findNugetDirs(dir) {
319
+ const groups = [];
320
+ const stack = [dir];
321
+ while (stack.length) {
322
+ const cur = stack.pop();
323
+ let entries; try { entries = fs.readdirSync(cur, { withFileTypes: true }); } catch { continue; }
324
+ const files = entries.filter(e => e.isFile()).map(e => e.name);
325
+ const csprojs = files.filter(f => f.toLowerCase().endsWith(".csproj"));
326
+ const has = files.includes("packages.lock.json") || files.includes("packages.config") || csprojs.length;
327
+ if (has) groups.push({ dir: cur, files, csprojs });
328
+ for (const e of entries) if (e.isDirectory() && !SKIP.has(e.name)) stack.push(path.join(cur, e.name));
329
+ }
330
+ return groups;
331
+ }
332
+
333
+ module.exports = {
334
+ id: "nuget",
335
+ label: "NuGet",
336
+ osvEcosystem: "NuGet",
337
+ manifestNames: ["packages.lock.json", "*.csproj", "packages.config"],
338
+
339
+ detect(dir) { return findNugetDirs(dir).length > 0; },
340
+
341
+ async collect(dir, opts = {}) {
342
+ const { ignoreTest, deps2Exclude } = opts;
343
+ const out = new Map();
344
+ const warnings = [];
345
+ const add = (d, manifestPath) => {
346
+ if (deps2Exclude && deps2Exclude.test(d.name)) return;
347
+ out.set(coordKeyFor("nuget", "", d.name), makeDepRecord({ ecosystem: "nuget", namespace: "", name: d.name, version: d.version, manifestPath, scope: d.scope, isDev: d.isDev }));
348
+ };
349
+ for (const g of findNugetDirs(dir)) {
350
+ if (g.files.includes("packages.lock.json")) {
351
+ const fp = path.join(g.dir, "packages.lock.json");
352
+ const { deps } = await N.parsePackagesLockJson(fp);
353
+ for (const d of deps) add(d, fp);
354
+ continue; // lock is authoritative for this dir
355
+ }
356
+ // No lock → best-effort from csproj (+CPM) and packages.config, with warning.
357
+ let cpm = {};
358
+ if (g.files.includes("Directory.Packages.props")) {
359
+ try { cpm = await N.parseDirectoryPackagesProps(path.join(g.dir, "Directory.Packages.props")); } catch { /* ignore */ }
360
+ }
361
+ let pinned = 0, skipped = 0;
362
+ for (const cs of g.csprojs) {
363
+ const fp = path.join(g.dir, cs);
364
+ const { deps, skipped: sk } = await N.parseCsproj(fp, cpm);
365
+ for (const d of deps) { add(d, fp); pinned++; }
366
+ skipped += sk;
367
+ }
368
+ if (g.files.includes("packages.config")) {
369
+ const fp = path.join(g.dir, "packages.config");
370
+ const { deps } = await N.parsePackagesConfig(fp);
371
+ for (const d of deps) { add(d, fp); pinned++; }
372
+ }
373
+ if (g.csprojs.length || g.files.includes("packages.config")) {
374
+ warnings.push({ type: "no-lockfile", manifestPath: g.dir, message: `no packages.lock.json — best-effort: ${pinned} pinned, ${skipped} floating/unresolved skipped (enable RestorePackagesWithLockFile)` });
375
+ }
376
+ }
377
+ return { deps: out, warnings };
378
+ },
379
+
380
+ coordKey(d) { return coordKeyFor("nuget", "", d.name); },
381
+ formatCoord(d) { return d.name; },
382
+ osvPackageName(d) { return d.name; },
383
+
384
+ async checkRegistry(deps, opts = {}) {
385
+ const { checkNugetRegistryDeps } = require("../nuget/registry");
386
+ return checkNugetRegistryDeps(deps, opts);
387
+ },
388
+ resolveEolProduct(d) { return require("../outdated").findEolProduct(d); },
389
+ recipe: require("./recipes").nuget,
390
+ nativeScanners: [],
391
+ };
392
+ ```
393
+
394
+ - [ ] **Step 3b: Register** — `index.js`: add `nuget` to imports + `[…, pypi, nuget]`.
395
+ - [ ] **Step 3c: Recipe** — `recipes.js`:
396
+ ```js
397
+ function dotnetAddSnippet(items) { return items.map(it => `dotnet add package ${it.artifactId} --version ${esc(it.fixVersion)}`).join("\n"); }
398
+ const nuget = { label: "NuGet", pinSection: "A. Update the affected packages", pinIntro: cnt => `Run for the ${cnt} affected package${cnt > 1 ? "s" : ""} (or bump Directory.Packages.props under CPM):`, snippet: dotnetAddSnippet, directSection: "B. Then restore + commit packages.lock.json" };
399
+ module.exports = { …, nuget, dotnetAddSnippet };
400
+ ```
401
+ - [ ] **Step 3d: EOL** — `outdated.js` findEolProduct, add before maven block:
402
+ ```js
403
+ if (dep.ecosystem === "nuget") return EOL_MAPPING.by_nuget_name?.[(dep.name || dep.artifactId || "").toLowerCase()] || null;
404
+ ```
405
+ - [ ] **Step 3e: Mapping** — `eol-mapping.json` add:
406
+ ```json
407
+ "by_nuget_name": {
408
+ "microsoft.aspnetcore.app": { "product": "aspnetcore", "label": "ASP.NET Core" },
409
+ "microsoft.entityframeworkcore": { "product": "efcore", "label": "EF Core" },
410
+ "microsoft.net.sdk": { "product": "dotnet", "label": ".NET" }
411
+ }
412
+ ```
413
+
414
+ - [ ] **Step 4: `npm test`** → PASS. **Step 5: Smoke** `node fad-checker.js -s ./test/fixtures/csharp-lock --offline`. **Step 6: Commit** `nuget: codec + registry wiring + EOL + recipe`.
415
+
416
+ ---
417
+
418
+ ### Task 4: Docs
419
+
420
+ - [ ] module maps (+`lib/nuget/*`, `nuget.codec.js`) + liste écosystèmes (ajouter C#/.NET) + fixtures `csharp-*`. `npm test` + commit.
421
+
422
+ ---
423
+
424
+ ## Self-Review (effectuée)
425
+
426
+ - Vuln nuget → OSV `NuGet` (Plan A) ✓ ; packages.lock.json + csproj(+CPM) + packages.config → Task 1 ✓ ;
427
+ deprecation + outdated → NuGet Task 2 + boucle checkRegistry (Plan B) ✓ ; EOL → Task 3 ✓ ; recette → 3c ✓ ;
428
+ report/CLI → automatiques via le registre ✓.
429
+ - **CPM** : `Directory.Packages.props` lu comme table ; `PackageReference` sans Version résolu contre elle,
430
+ sinon compté en skipped + warning.
431
+ - **Case-insensitive** : clé `nuget:<lower>` (dep-record), `name` garde la casse d'origine pour l'affichage/OSV.
432
+ - **Cohérence types** : checkRegistry → {deprecated[], outdated[]}.