diff --git a/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.fr.md b/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.fr.md new file mode 100644 index 00000000..c4b753eb --- /dev/null +++ b/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.fr.md @@ -0,0 +1,419 @@ +# FirstClassErrors — Analyse de la couverture de code + +🌍 **Langues :** +🇬🇧 [English](./2026-07-30-coverage-analysis.md) | đŸ‡«đŸ‡· Français (ce fichier) + +**Date :** 2026-07-30 +**RĂ©vision analysĂ©e :** `da6e7ee` (tĂȘte de `main` au moment de l'analyse ; `main` a avancĂ© depuis) +**Source :** projet SonarCloud `reefact_first-class-errors`, analyse du 2026-07-30 07:51 UTC +**PĂ©rimĂštre :** tous les composants que SonarCloud rapporte pour la solution — 614 composants, +20 668 ncloc, dont 182 fichiers portent un trou de couverture. +**Statut :** consultatif. ConformĂ©ment Ă  la convention du dĂ©pĂŽt (ADR-0004), cette analyse produit des +recommandations, jamais des blocages ; les deux ADR candidates qu'elle nomme sont des propositions que +`@reefact` accepte ou rejette. + +**Depuis cette analyse :** JustDummies a Ă©tĂ© extrait dans +[`Reefact/just-dummies`](https://github.com/Reefact/just-dummies) le 2026-08-07 (ADR-0069), emportant +ses sources, ses records de dĂ©cision et sa documentation de workflow hors de ce dĂ©pĂŽt. Les chiffres +JustDummies ci-dessous sont conservĂ©s comme trace de ce qui a Ă©tĂ© mesurĂ© le 2026-07-30 ; les +rĂ©fĂ©rences au matĂ©riel dĂ©placĂ© sont nommĂ©es mais ne sont plus des liens. + +**MĂ©thode.** Les mesures par fichier ont Ă©tĂ© tirĂ©es de `api/measures/component_tree` de SonarCloud (les +deux pages, 614 composants), et les compteurs de passages et de branchements par ligne de +`api/sources/lines` pour les 182 fichiers portant un trou. Les totaux reconstruits correspondent +**exactement** aux chiffres publiĂ©s par Sonar — 840 lignes et 1 303 conditions non couvertes — la +classification ci-dessous porte donc sur la population entiĂšre et non sur un Ă©chantillon. Chaque ligne +non couverte a Ă©tĂ© rattachĂ©e au membre qui la contient par analyse des sources locales ; chaque +branchement manquant a Ă©tĂ© classĂ© d'aprĂšs la forme de sa propre expression. + +**Non vĂ©rifiĂ© localement.** `dotnet build` et `dotnet test` **n'ont pas** Ă©tĂ© exĂ©cutĂ©s pour cette +analyse. Tous les chiffres proviennent de l'analyse SonarCloud du 2026-07-30, elle-mĂȘme produite par +`dotnet test 
 --settings coverage.runsettings` dans [`sonar.yml`](../workflows/sonar.fr.md). + +> **Terminologie.** Dans tout ce document, un *branchement* (que SonarCloud appelle *condition*, et la +> littĂ©rature anglophone *branch*) est un chemin conditionnel dans le code — chaque issue possible d'un +> `if`, d'un `&&`, d'un `||`, d'un `??`, d'un `case` ou d'un ternaire. Une ligne +> `if (x is null) { throw new ArgumentNullException(nameof(x)); }` compte pour une ligne et deux +> branchements ; un test qui ne passe jamais `null` couvre la ligne sans couvrir le branchement. Rien +> ici ne concerne les branches Git. + +--- + +## 1. RĂ©sumĂ© + +La solution est Ă  **86,6 % de couverture globale** — 91,1 % en lignes, 80,2 % en branchements — avec +**2 143 unitĂ©s non couvertes** : 840 lignes et 1 303 branchements. Ces 2 143 unitĂ©s ne sont pas +2 143 tests manquants. Elles se rĂ©partissent en cinq catĂ©gories disjointes, et une seule d'entre elles +se rĂšgle en Ă©crivant davantage de tests. + +Trois constats commandent toutes les recommandations ci-dessous. + +1. **Le dĂ©ficit est un dĂ©ficit de branchements, et il n'est pas lĂ  oĂč le pourcentage le suggĂšre.** La + couverture en lignes est dĂ©jĂ  Ă  91,1 % ; les branchements reprĂ©sentent 61 % des unitĂ©s non + couvertes. **JustDummies et les deux projets d'analyzers dĂ©tiennent 1 084 des 2 143 unitĂ©s**, dont + 859 branchements. Pendant ce temps, les deux projets aux pires pourcentages — + `FirstClassErrors.Cli` Ă  53,9 % et `FirstClassErrors.GenDoc.Worker` Ă  0 % — sont majoritairement du + code que la CI exerce rĂ©ellement, simplement pas sous l'instrument de couverture. Prioriser par le + pourcentage commencerait exactement au mauvais endroit. + +2. **`JustDummies.Xunit` n'est pas dans le dĂ©nominateur du tout.** Un package NuGet publiĂ© est classĂ© + par Sonar comme du code de test et sa couverture ne compte jamais, dans aucun sens. Tout objectif + « 100 % de la solution » l'exclut silencieusement aujourd'hui. Voir + [§5](#5-un-angle-mort-de-mesure--justdummiesxunit). + +3. **Le levier le moins cher est dĂ©jĂ  Ă©crit dans ce dĂ©pĂŽt.** JustDummies possĂšde un + `NullArgumentGuardConventionTests` rĂ©flexif qui invoque chaque membre avec `null` et vĂ©rifie + l'`ArgumentNullException` produite. Aucun autre projet n'a d'Ă©quivalent — d'oĂč les 30 gardes null + parmi les 49 trous de branchement de la bibliothĂšque cƓur, dont 24 dans le seul + `OutcomeTaskExtensions.cs`. + +Le quality gate est **vert** et ne mesure que le code *neuf* (88,3 % pour une barre Ă  80 %). Rien +n'Ă©choue. Chaque chiffre de ce document est donc un choix, pas une exigence. + +## 2. Chiffres clĂ©s + +| MĂ©trique | Valeur | +|---|---| +| Couverture globale | **86,6 %** | +| Couverture en lignes | 91,1 % — 840 non couvertes sur 9 449 lignes Ă  couvrir | +| Couverture en branchements | 80,2 % — 1 303 non couverts sur 6 572 conditions Ă  couvrir | +| Total des unitĂ©s manquantes | **2 143** (840 lignes + 1 303 branchements) | +| Quality gate | Vert — `new_coverage` 88,3 % pour un seuil Ă  80 % | +| Taille analysĂ©e | 20 668 ncloc sur 614 composants ; 182 fichiers portent un trou | + +Évolution de la couverture sur la pĂ©riode, d'aprĂšs l'historique des mĂ©triques SonarCloud : + +| Date | 09/07 | 12/07 | 19/07 | 26/07 | 27/07 | 28/07 | 29/07 | 30/07 | +|---|---|---|---|---|---|---|---|---| +| Couverture | 78,1 % | 79,3 % | 79,3 % | 82,8 % | 84,2 % | 86,1 % | 86,6 % | 86,6 % | + +## 3. OĂč se situe le dĂ©ficit + +Par projet, par taille de dĂ©ficit dĂ©croissante. `LTC` = lignes Ă  couvrir, `CTC` = conditions Ă  couvrir. + +| Projet | Couverture | LTC | CTC | Lignes n.c. | Branch. n.c. | UnitĂ©s | +|---|---:|---:|---:|---:|---:|---:| +| `JustDummies` | 90,4 % | 3 229 | 2 745 | 167 | 404 | **571** | +| `JustDummies.Analyzers` | 88,0 % | 1 451 | 1 671 | 50 | 324 | **374** | +| `FirstClassErrors.Cli` | 53,9 % | 502 | 248 | 227 | 119 | **346** | +| `FirstClassErrors.GenDoc` | 84,2 % | 1 414 | 542 | 161 | 148 | **309** | +| `FirstClassErrors.Usage` | 67,1 % | 351 | 72 | 81 | 58 | 139 | +| `FirstClassErrors.Analyzers` | 89,7 % | 858 | 494 | 8 | 131 | 139 | +| `FirstClassErrors.RequestBinder.Usage` | 75,1 % | 352 | 98 | 55 | 57 | 112 | +| `FirstClassErrors` | 94,7 % | 701 | 482 | 14 | 49 | 63 | +| `FirstClassErrors.RequestBinder` | 93,2 % | 483 | 176 | 33 | 12 | 45 | +| `FirstClassErrors.GenDoc.Worker` | 0,0 % | 43 | 0 | 43 | 0 | 43 | +| `FirstClassErrors.Testing` | 98,2 % | 65 | 44 | 1 | 1 | 2 | + +`FirstClassErrors.RequestBinder.Benchmarks` est absent parce qu'il est dĂ©jĂ  exclu de la couverture par +`sonar.coverage.exclusions` dans [`sonar.yml`](../workflows/sonar.fr.md) — un banc de mesure jamais +publiĂ© et jamais testĂ© unitairement. Son code passe malgrĂ© tout sous SonarAnalyzer. + +Les douze fichiers portant les plus gros dĂ©ficits : + +| # | Fichier | Lignes n.c. | Branch. n.c. | UnitĂ©s | Couverture | +|---:|---|---:|---:|---:|---:| +| 1 | `FirstClassErrors.GenDoc/SolutionErrorDocumentationGenerator.cs` | 143 | 83 | 226 | 50,0 % | +| 2 | `JustDummies/Any.Combine.cs` | 0 | 75 | 75 | 76,6 % | +| 3 | `JustDummies/WideIntervalSpec.cs` | 16 | 46 | 62 | 80,9 % | +| 4 | `JustDummies/DecimalIntervalSpec.cs` | 19 | 42 | 61 | 83,1 % | +| 5 | `JustDummies/RegexParser.cs` | 17 | 41 | 58 | 90,8 % | +| 6 | `FirstClassErrors.Usage/Model/Temperature.cs` | 35 | 22 | 57 | 0,0 % | +| 7 | `JustDummies.Analyzers/ScalarConstraintState.cs` | 1 | 56 | 57 | 74,3 % | +| 8 | `FirstClassErrors.Cli/RendererLoader.cs` | 27 | 21 | 48 | 7,7 % | +| 9 | `FirstClassErrors.GenDoc.Worker/Program.cs` | 43 | 0 | 43 | 0,0 % | +| 10 | `FirstClassErrors.Usage/Utils/DocumentationFormatter.cs` | 21 | 19 | 40 | 46,7 % | +| 11 | `JustDummies.Analyzers/RejectedConstantArgumentAnalyzer.cs` | 14 | 26 | 40 | 86,5 % | +| 12 | `FirstClassErrors.Cli/CatalogSnapshotSource.cs` | 26 | 12 | 38 | 0,0 % | + +## 4. Les cinq natures de dĂ©ficit + +Chacune des 2 143 unitĂ©s a Ă©tĂ© classĂ©e d'aprĂšs sa propre ligne de source. Les catĂ©gories sont +disjointes et leur somme fait le total. + +| Nature | Lignes n.c. | Branch. n.c. | UnitĂ©s | Part | +|---|---:|---:|---:|---:| +| **V1** — exercĂ© par la CI, invisible Ă  l'instrument | 186 | 83 | 269 | 12,6 % | +| **V2** — code d'exemple et de dĂ©monstration | 136 | 115 | 251 | 11,7 % | +| **V3** — un test le ferme aujourd'hui | 340 | 993 | **1 333** | **62,2 %** | +| **V4** — exige une couture avant qu'un test puisse l'atteindre | 176 | 88 | 264 | 12,3 % | +| **V5** — pratiquement inatteignable | 2 | 24 | 26 | 1,2 % | +| **Total** | 840 | 1 303 | 2 143 | 100 % | + +### V1 — exercĂ© par la CI, invisible Ă  l'instrument (269 unitĂ©s) + +Le lancement de processus MSBuild dans `SolutionErrorDocumentationGenerator` (226 unitĂ©s) et le point +d'entrĂ©e de `GenDoc.Worker` (43 unitĂ©s). Les zones non couvertes sont prĂ©cisĂ©ment les chemins qui +lancent des processus : `DotNetBuild`, `DotNetGetProperty`, les branchements de timeout et de kill de +`RunProcess`, et l'invocation du sous-processus dans `RunWorker`. + +Ce code n'est pas non testĂ©. [`canary.yml`](../../../../.github/workflows/canary.yml) lance le vrai +`fce.dll generate` sur un vrai projet, capture les diagnostics du worker et *vĂ©rifie* deux choses : que +le catalogue Ă©mis contient des codes d'erreur, et que la banniĂšre du worker annonce bien le runtime le +plus rĂ©cent (`Documenting 
 on .NET .`). +[`gendoc-docs.yml`](../../../../.github/workflows/gendoc-docs.yml) lance le mĂȘme binaire pour +rĂ©gĂ©nĂ©rer le catalogue commitĂ©. C'est une *meilleure* vĂ©rification du comportement MSBuild et du +roll-forward que n'importe quel mock. Ces chemins sont non couverts parce que `dotnet test` ne les +lance jamais — une propriĂ©tĂ© de l'instrument, pas du banc de test. + +Une rĂ©serve : le canary ne s'exĂ©cute que lorsqu'une prĂ©version de .NET est disponible, et passe son +tour sinon ; ce n'est donc pas une garantie Ă  chaque commit. `gendoc-docs` n'a pas cette condition. + +### V2 — code d'exemple et de dĂ©monstration (251 unitĂ©s) + +`FirstClassErrors.Usage` et `FirstClassErrors.RequestBinder.Usage`. L'intention est dĂ©jĂ  consignĂ©e dans +le code : `Usage/Model/Amount.cs` porte un `SuppressMessage` justifiant que les opĂ©rateurs de +comparaison sont hors pĂ©rimĂštre parce qu'ils « ajouteraient de la surface non testĂ©e Ă  un type que les +tests n'exercent qu'indirectement ». Le pĂ©rimĂštre de couverture n'a simplement jamais Ă©tĂ© alignĂ© sur +cette intention affichĂ©e. + +### V3 — un test le ferme aujourd'hui (1 333 unitĂ©s) + +Aucun refactoring, aucune couture, aucune dĂ©cision de politique — seulement des tests qui n'existent +pas encore. 993 de ces unitĂ©s sont des branchements. C'est la seule catĂ©gorie oĂč Ă©crire des tests est +la rĂ©ponse, et elle est dĂ©taillĂ©e en [§6](#6-de-quoi-sont-faites-les-1-333-unitĂ©s-actionnables). + +### V4 — exige une couture avant qu'un test puisse l'atteindre (264 unitĂ©s) + +La branche de commandes `renderer` et `config` de la CLI Ă©crit directement dans `Console.Out` et +appelle `Assembly.LoadFrom`, tandis que les commandes `generate` et `catalog` passent par `IOutputSink`, +`IErrorDocumentationGenerator` et `ICatalogSnapshotSource` et se situent entre 80 % et 97 %. + +Les commandes non testĂ©es sont exactement celles qui n'ont jamais adoptĂ© la couture que le projet +possĂšde dĂ©jĂ . La liste : `RendererLoader`, `RendererListCommand`, `RendererAddCommand`, +`RendererRemoveCommand`, `ConfigShowCommand`, `ConsoleGenerationLogger`, `CatalogSnapshotSource`, +`CatalogSourceResolver`, `RendererCatalog`, `SolutionErrorDocumentationGeneratorAdapter` — ce dernier +Ă©tant le cĂŽtĂ© production de la couture mĂȘme que les tests pilotent avec des doublures — plus +`Cli/Program.cs`, qui est du cĂąblage Spectre. + +### V5 — pratiquement inatteignable (26 unitĂ©s) + +Gardes dĂ©fensives contre des Ă©tats que le compilateur ne peut pas produire : vĂ©rifications Roslyn +`is not ` sur des types d'opĂ©ration et de symbole, et bras `default:` de `switch` exhaustifs. +**Ce chiffre est un plancher, pas un total** — c'est seulement ce qui Ă©tait dĂ©montrable par la syntaxe +seule ; le nombre rĂ©el de branchements dĂ©fensifs inatteignables est plus Ă©levĂ©. Les poursuivre coĂ»te de +la correction, car la seule façon de « couvrir » une telle garde est de supprimer une garde qui est lĂ  +volontairement. + +## 5. Un angle mort de mesure : `JustDummies.Xunit` + +`JustDummies.Xunit/ReproducibleAttribute.cs` — 60 lignes de code d'un **package NuGet publiĂ©** — est +classĂ© par SonarCloud sous le qualifieur `UTS` (source de test unitaire), et non comme du code +principal. Le SonarScanner pour .NET considĂšre un projet comme un projet de test dĂšs qu'il rĂ©fĂ©rence un +framework de test, et ce package rĂ©fĂ©rence `xunit.v3.extensibility.core` parce que c'est exactement ce +qu'il est : l'adaptateur xUnit. + +La consĂ©quence n'est *pas* qu'il est non testĂ© — `JustDummies.Xunit.UnitTests` existe et l'exerce, y +compris via une couture `InternalsVisibleTo` ajoutĂ©e dĂ©libĂ©rĂ©ment pour que la rĂšgle « ne rapporter +qu'en cas d'Ă©chec » puisse ĂȘtre prouvĂ©e sans qu'un test doive Ă©chouer pour de vrai. La consĂ©quence est +que sa couverture **ne compte jamais**, dans aucun sens : une rĂ©gression qui la ferait tomber Ă  zĂ©ro +dĂ©placerait le chiffre publiĂ© de 0,0 point, et le travail dĂ©jĂ  fait pour le tester ne rapporte rien. + +Tout objectif « 100 % de la solution » laisse ce package silencieusement dehors. Le corriger suppose de +forcer la classification — `sonar.test.exclusions`, ou un `SonarQubeTestProject=false` explicite sur ce +seul projet. + +## 6. De quoi sont faites les 1 333 unitĂ©s actionnables + +Voici la catĂ©gorie **V3** ouverte — les unitĂ©s qu'un test peut fermer aujourd'hui. + +| Motif | Lignes n.c. | Branch. n.c. | UnitĂ©s | Forme du correctif | +|---|---:|---:|---:|---| +| ChaĂźnes de gardes et dispatch des analyzers | 56 | 431 | **487** | Extraits de code nĂ©gatifs via l'`AnalyzerTestHarness` existant | +| Moteurs de spec JustDummies (intervalle, chaĂźne, regex) | 73 | 205 | **278** | Cas limites et d'Ă©puisement ; `DescribeExhaustion` et `Cardinality` ne sont jamais atteints | +| Reste de la surface `Any` | 34 | 68 | 102 | Constructeurs de contrainte morts sur certains types scalaires — `MultipleOf` sur `AnySByte`, `LessThan` sur `AnyUInt16`, 
 | +| Renderers et versioning GenDoc | 18 | 65 | 83 | Cas limites des renderers et libellĂ©s de diff de catalogue ; la couture existe et est dĂ©jĂ  testĂ©e | +| CLI, la partie dĂ©jĂ  cousue | 51 | 31 | 82 | Davantage de cas via les doublures qu'utilisent dĂ©jĂ  `GenerateCommand` et les commandes de catalogue | +| Interfaces d'introspection `Any` jamais appelĂ©es | 55 | 9 | 64 | Une thĂ©orie rĂ©flexive sur chaque `Any` — 26 fichiers fermĂ©s d'un coup | +| Gardes de domaine et d'intervalle jamais violĂ©es | 4 | 57 | 61 | Un test de convention qui passe Ă  chaque garde sa valeur illĂ©gale | +| Gardes null jamais nourries d'un `null` | 4 | 52 | 56 | Porter le `NullArgumentGuardConventionTests` de JustDummies aux autres projets | +| ChaĂźnes `??` d'`Any.Combine` (matrice des positions d'opĂ©rande) | 1 | 51 | 52 | Une thĂ©orie faisant varier l'opĂ©rande porteur du `RandomSource` | +| BibliothĂšque cƓur `FirstClassErrors` | 14 | 19 | 33 | Chemins d'Ă©chec de chargement et de nom null dans `AssemblyErrorDocumentationReader` | +| `FirstClassErrors.RequestBinder` | 29 | 4 | 33 | `BindingScope.Get` et le chemin du convertisseur de propriĂ©tĂ©s simples | +| Autre (`FirstClassErrors.Testing`) | 1 | 1 | 2 | — | +| **Total** | **340** | **993** | **1 333** | | + +### Les quatre motifs rĂ©pliquĂ©s + +Plusieurs entrĂ©es ci-dessus sont un mĂȘme motif rĂ©pĂ©tĂ© sur de nombreux fichiers, ce qui les rend +intĂ©ressantes Ă  attaquer : un seul harnais ferme des dizaines d'unitĂ©s d'un coup. + +**La matrice d'introspection `Any` — 64 unitĂ©s sur 26 fichiers.** `IHasRandomSource.Source`, +`ICardinalityHint.DistinctCardinality` et `ICardinalityHint.Contains` sont des implĂ©mentations +explicites d'interface, et pour la plupart des types scalaires rien dans la suite ne passe jamais par +elles. Une seule thĂ©orie pilotĂ©e par rĂ©flexion sur chaque `Any` ferme les 26 fichiers d'un coup — et +le dĂ©pĂŽt possĂšde dĂ©jĂ  cette forme de harnais dans `SurfaceParityTests`, `FactoryNamingConventionTests` +et `NullArgumentGuardConventionTests`. + +**Des gardes qui existent mais ne sont jamais violĂ©es — 117 unitĂ©s.** RĂ©parties par type d'exception et +par projet : + +| Projet | `ArgumentNullException` | `ArgumentOutOfRangeException` | `ArgumentException` | Total | +|---|---:|---:|---:|---:| +| `JustDummies` | 14 | 10 | 47 | 71 | +| `FirstClassErrors` | 30 | 0 | 0 | 30 | +| `FirstClassErrors.RequestBinder` | 8 | 0 | 0 | 8 | +| `FirstClassErrors.Usage` | 0 | 0 | 2 | 2 | +| **Total** | **52** | **10** | **49** | **111** | + +Le `NullArgumentGuardConventionTests` de JustDummies invoque par rĂ©flexion chaque membre avec `null` et +vĂ©rifie l'`ArgumentNullException` — c'est pourquoi sa colonne null est la plus faible alors qu'il s'agit +du plus gros projet. **`FirstClassErrors` n'a pas d'Ă©quivalent**, et ses 30 gardes null non couvertes en +sont la consĂ©quence directe ; 24 d'entre elles sont dans `OutcomeTaskExtensions.cs`, une par garde +`is null` sur `next`, `fallback`, `onSuccess` et `onFailure`. Porter ce seul test de convention est le +mouvement le moins cher de ce document. Le mĂȘme manque existe pour les **gardes de domaine et +d'intervalle**, qu'aucun test de convention ne couvre dans aucun projet. + +**La matrice des positions d'opĂ©rande d'`Any.Combine` — 52 unitĂ©s dans un seul fichier.** Chaque +surcharge d'aritĂ© enchaĂźne `SourceOf(first) ?? SourceOf(second) ?? 
` : une surcharge Ă  *N* opĂ©randes +Ă©met donc 2*N* branchements, et les tests ne placent jamais la source ailleurs qu'en premiĂšre position. +`Any.Combine.cs` a 0 *ligne* non couverte et 75 *branchements* non couverts — chaque ligne s'exĂ©cute, la +moitiĂ© des chemins jamais. Une thĂ©orie faisant varier l'opĂ©rande porteur de la source parcourt toute la +chaĂźne. + +**Les chaĂźnes de gardes des analyzers — 487 unitĂ©s, la plus grosse catĂ©gorie.** Par forme d'expression, +sur les deux projets d'analyzers (455 unitĂ©s de branchement classĂ©es) : + +| Forme | UnitĂ©s | Part | +|---|---:|---:| +| garde null / null-conditionnelle sur un symbole Roslyn | 150 | 33,0 % | +| `if` simple couvert d'un seul cĂŽtĂ© | 109 | 24,0 % | +| autre | 71 | 15,6 % | +| dispatch `switch` / `case` | 48 | 10,5 % | +| court-circuit `&&` / `\|\|` | 35 | 7,7 % | +| garde `is not ` (catĂ©gorie V5) | 24 | 5,3 % | +| boucle sans chemin Ă  zĂ©ro itĂ©ration | 12 | 2,6 % | +| coalescence `??` | 6 | 1,3 % | + +La plupart sont de vĂ©ritables chemins d'analyzer — syntaxe malformĂ©e ou inhabituelle Ă  laquelle +l'analyzer doit survivre — atteignables via l'`AnalyzerTestHarness` existant avec des extraits de code +nĂ©gatifs. À noter, le contraste avec les tests de mutation sur lesquels ce dĂ©pĂŽt s'appuie dĂ©jĂ  +(ADR-0043, ADR-0046) : beaucoup de ces branchements sont *exĂ©cutĂ©s* mais jamais *vĂ©rifiĂ©s*, ils sont +donc probablement aussi des mutants survivants. + +## 7. Ce que chaque dĂ©cision rapporte + +Deux leviers distincts dĂ©placent le chiffre et ne doivent pas ĂȘtre confondus. Les **exclusions** +changent le dĂ©nominateur et ne coĂ»tent qu'une dĂ©cision documentĂ©e. Les **tests** changent le numĂ©rateur +et coĂ»tent du travail. Les chiffres sont cumulatifs, calculĂ©s avec la formule de Sonar +`((LTC − ln.c.) + (CTC − br.n.c.)) / (LTC + CTC)` sur les mesures par fichier. + +| Étape | Levier | UnitĂ©s | Couverture | +|---|---|---:|---:| +| Aujourd'hui | — | — | 86,62 % | +| Exclure les deux projets d'exemple `Usage` | dĂ©nominateur | −251 | 87,51 % | +| 
 et `GenDoc.Worker`, le point d'entrĂ©e du worker | dĂ©nominateur | −43 | 87,76 % | +| 
 et `Cli/Program.cs`, le cĂąblage Spectre | dĂ©nominateur | −17 | 87,86 % | +| 
 et `SolutionErrorDocumentationGenerator.cs`, le lancement MSBuild | dĂ©nominateur | −226 | 89,03 % | +| 
 puis couvrir les dix fichiers CLI non cousus | numĂ©rateur (aprĂšs couture) | −247 | **90,71 %** | + +AprĂšs tout cela, **1 359 unitĂ©s subsistent et 90,7 % est le plafond des mouvements peu coĂ»teux** — dont +1 084 dans JustDummies et les analyzers, trĂšs majoritairement des branchements. Il n'y a pas de +raccourci pour Ă©viter cette catĂ©gorie : c'est le vrai travail, et c'est aussi le code oĂč la correction +compte le plus. + +## 8. Recommandation + +1. **Corriger d'abord l'angle mort — c'est un dĂ©faut de mesure, pas un trou de couverture.** Forcer + l'analyse de `JustDummies.Xunit` comme code principal. Tant que ce n'est pas fait, aucun objectif de + couverture ne couvre rĂ©ellement la solution, et on ne peut pas se fier au chiffre pour bouger quand + ce package rĂ©gresse. + +2. **Trancher le pĂ©rimĂštre explicitement, une fois, dans une ADR.** Les exemples, les points d'entrĂ©e + de processus et le lancement MSBuild reprĂ©sentent 520 unitĂ©s — un quart du total — pour lesquelles + aucun test unitaire ne devrait jamais ĂȘtre Ă©crit. `Benchmarks` est dĂ©jĂ  exclu pour exactement cette + raison et le raisonnement est dĂ©jĂ  Ă©crit dans `sonar.yml` ; il s'agit d'Ă©tendre la mĂȘme rĂšgle au mĂȘme + type de code. C'est une dĂ©cision durable qu'un futur mainteneur remettrait en question : elle appelle + une ADR plutĂŽt qu'un commentaire. + +3. **Faire compter l'exercice de la CI, au lieu d'Ă©crire des tests unitaires pour l'imiter.** `canary` + et `gendoc-docs` lancent dĂ©jĂ  le vrai `fce generate`, dĂ©marrent le vrai worker et vĂ©rifient le + rĂ©sultat. Soit on collecte la couverture de ces exĂ©cutions, soit on exclut le chemin en disant + pourquoi — mais on n'Ă©crit pas un faux `IProcessRunner` pour faire bouger un chiffre. En cas + d'exclusion, noter que le canary est conditionnĂ© Ă  une prĂ©version : c'est `gendoc-docs` qui s'exĂ©cute + rĂ©ellement Ă  chaque poussĂ©e concernĂ©e. + +4. **Porter le test de convention des gardes null hors de JustDummies.** Un seul harnais, dĂ©jĂ  Ă©crit et + Ă©prouvĂ© dans ce dĂ©pĂŽt, appliquĂ© Ă  `FirstClassErrors`, `RequestBinder` et `GenDoc`. Ferme 56 unitĂ©s, + supprime une catĂ©gorie entiĂšre dĂ©finitivement, et chaque garde future est couverte le jour oĂč elle + est Ă©crite. Étendre ensuite le mĂȘme harnais aux gardes d'intervalle (+61). + +5. **Ensuite, les deux matrices rĂ©flexives de JustDummies.** Les interfaces d'introspection `Any` + (64) et les positions d'opĂ©rande d'`Any.Combine` (52). Ce sont deux thĂ©ories uniques sur une liste de + types existante. D'aprĂšs + l'ADR-0040 (dĂ©tenue ici Ă  l'Ă©poque, partie avec l'extraction), + ce sont des invariants qui tiennent pour tout argument lĂ©gal : ils relĂšvent donc de + `JustDummies.PropertyTests`, pas de la suite unitaire — voir + *Écrire des tests JustDummies*, Ă©galement dĂ©placĂ©. + +6. **Seulement aprĂšs, les analyzers — et les piloter par la mutation, pas par la couverture.** 487 + unitĂ©s, majoritairement des branchements dĂ©jĂ  *exĂ©cutĂ©s* mais non *vĂ©rifiĂ©s*. La couverture les + dĂ©clarera fermĂ©s dĂšs qu'un extrait les atteindra ; seule la campagne de mutation dira si le test a + rĂ©ellement Ă©pinglĂ© le comportement. Le dĂ©pĂŽt lance dĂ©jĂ  cette campagne — qu'elle choisisse les cibles + ici, plutĂŽt que le pourcentage de couverture. + +### ADR candidates + +Deux dĂ©cisions ci-dessus sont durables et qu'un futur mainteneur remettrait en question ; elles sont +proposĂ©es comme brouillons : + +- **La politique de pĂ©rimĂštre de couverture** — quelles catĂ©gories de code sont dĂ©libĂ©rĂ©ment hors du + dĂ©nominateur (exemples, points d'entrĂ©e de processus, lancements de processus) et pourquoi. + Recommandation 2. +- **OĂč les chemins de niveau processus sont vĂ©rifiĂ©s** — acter que les exercices `canary` et + `gendoc-docs` constituent la vĂ©rification retenue pour les chemins MSBuild et worker, plutĂŽt que des + tests unitaires sur mocks. Recommandation 3. + +Aucune n'est rĂ©digĂ©e ici. ConformĂ©ment Ă  la convention du dĂ©pĂŽt, un agent propose et n'accepte jamais. + +## 9. Ce que cette analyse ne prĂ©tend pas + +**Que 100 % soit le bon objectif.** Le quality gate porte sur le code neuf et il est vert Ă  88,3 %. +Rien n'Ă©choue ici. Le plafond atteignable aprĂšs tous les mouvements raisonnables est de l'ordre de +96–97 %, parce que la catĂ©gorie V5 est rĂ©elle et que ses 26 unitĂ©s ne sont que celles dĂ©montrables par +la syntaxe. + +**Que la couverture mesure la qualitĂ© des tests.** Ce dĂ©pĂŽt le sait dĂ©jĂ  : il s'appuie sur le score de +mutation prĂ©cisĂ©ment parce qu'un test peut exĂ©cuter une ligne sans rien vĂ©rifier Ă  son sujet (ADR-0043, +ADR-0046). Plusieurs catĂ©gories ci-dessus passeraient au vert sous la couverture tout en restant rouges +sous Stryker. Quand les deux divergent, c'est la campagne de mutation qui dit vrai. + +**Que les chiffres soient Ă  jour.** Ils dĂ©crivent `da6e7ee`. `main` a avancĂ© depuis, y compris des +refactorings Ă  l'intĂ©rieur de `JustDummies` : les chiffres par fichier auront donc bougĂ©. C'est la +structure de l'analyse — les cinq natures, les motifs rĂ©pliquĂ©s, les deux leviers — qui est censĂ©e +survivre Ă  l'instantanĂ©. + +## 10. Reproduire ces chiffres + +Toutes les donnĂ©es sont publiques ; le projet SonarCloud est lisible sans jeton. + +```sh +# Chiffres clĂ©s +curl -s "https://sonarcloud.io/api/measures/component?component=reefact_first-class-errors\ +&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines,uncovered_conditions,\ +lines_to_cover,conditions_to_cover,ncloc" + +# Mesures par fichier (paginer : ps=500, p=1 puis p=2) +curl -s "https://sonarcloud.io/api/measures/component_tree?component=reefact_first-class-errors\ +&metricKeys=coverage,uncovered_lines,uncovered_conditions,lines_to_cover,conditions_to_cover\ +&strategy=leaves&ps=500&p=1&s=metric&metricSort=uncovered_lines&asc=false" + +# Passages et branchements ligne Ă  ligne pour un fichier +curl -s "https://sonarcloud.io/api/sources/lines?key=reefact_first-class-errors%3A" +``` + +Une ligne est non couverte quand `lineHits == 0` ; une ligne a des branchements manquants quand +`coveredConditions < conditions`. La somme de `uncovered_lines` et de `(conditions − coveredConditions)` +sur tous les fichiers doit reproduire les totaux du projet — c'est cette rĂ©conciliation qui rend la +classification exhaustive plutĂŽt qu'indicative. + +## Voir aussi + +- [Workflow `sonar`](../workflows/sonar.fr.md) — comment l'analyse et son rapport de couverture sont + produits. +- [Workflow `sonar-gate`](../workflows/sonar-gate.fr.md) — comment le quality gate est relu. +- [Workflow `ci`](../workflows/ci.fr.md) — produit le mĂȘme format OpenCover via `coverage.runsettings`. +- [`mutation`](../workflows/mutation.fr.md) — le contrĂŽle qui mesure si une ligne couverte est + rĂ©ellement vĂ©rifiĂ©e. Son pendant JustDummies, `justdummies-mutation`, est parti avec l'extraction. +- *Écrire des tests JustDummies* — Ă  quelle suite appartient un nouveau test JustDummies ; il vit + dĂ©sormais dans [`Reefact/just-dummies`](https://github.com/Reefact/just-dummies). diff --git a/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.md b/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.md new file mode 100644 index 00000000..24cbf3d5 --- /dev/null +++ b/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.md @@ -0,0 +1,392 @@ +# FirstClassErrors — Code Coverage Analysis + +🌍 **Languages:** +🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./2026-07-30-coverage-analysis.fr.md) + +**Date:** 2026-07-30 +**Analysed revision:** `da6e7ee` (tip of `main` at analysis time; `main` has advanced since) +**Source:** SonarCloud project `reefact_first-class-errors`, analysis of 2026-07-30 07:51 UTC +**Scope:** every component SonarCloud reports for the solution — 614 components, 20,668 ncloc, of which +182 files carry a coverage gap. +**Status:** advisory. Per the repository's own convention (ADR-0004), this analysis produces +recommendations, never blockers; the two candidate ADRs it names are drafts for `@reefact` to accept +or reject. + +**Since this analysis:** JustDummies was extracted into +[`Reefact/just-dummies`](https://github.com/Reefact/just-dummies) on 2026-08-07 (ADR-0069), taking its +sources, its decision records and its workflow documentation out of this repository. The JustDummies +figures below are kept as the record of what was measured on 2026-07-30; the references to material +that moved are named but no longer linked. + +**Method.** Per-file measures were pulled from SonarCloud's `api/measures/component_tree` (both pages, +614 components), and per-line hit counts and branch counts from `api/sources/lines` for all 182 files +carrying a gap. The reconstructed totals match Sonar's published figures **exactly** — 840 uncovered +lines and 1,303 uncovered conditions — so the classification below covers the whole population rather +than a sample. Each uncovered line was attributed to its enclosing member by parsing the local sources; +each missing branch was classified by the shape of its own expression. + +**Not verified locally.** `dotnet build` and `dotnet test` were **not** run for this analysis. Every +figure comes from the SonarCloud analysis of 2026-07-30, which is itself produced by +`dotnet test 
 --settings coverage.runsettings` in [`sonar.yml`](../workflows/sonar.en.md). + +> **Terminology.** Throughout this document a *branch* is a conditional path in the code — each way an +> `if`, `&&`, `||`, `??`, `case` or ternary can go. SonarCloud calls these *conditions*. A line +> `if (x is null) { throw new ArgumentNullException(nameof(x)); }` counts as one line and two branches; +> a test that never passes `null` covers the line without covering the branch. Nothing here refers to +> Git branches. + +--- + +## 1. Executive summary + +The solution sits at **86.6% overall coverage** — 91.1% line, 80.2% branch — with **2,143 uncovered +units**: 840 lines and 1,303 branches. Those 2,143 units are not 2,143 missing tests. They fall into +five disjoint kinds, and only one of them is answered by writing more tests. + +Three findings drive every recommendation below. + +1. **The deficit is a branch deficit, and it is not where the percentage suggests.** Line coverage is + already at 91.1%; branches are 61% of all uncovered units. **JustDummies and the two analyzer + projects hold 1,084 of the 2,143 units**, of which 859 are branches. Meanwhile the two projects with + the worst percentages — `FirstClassErrors.Cli` at 53.9% and `FirstClassErrors.GenDoc.Worker` at 0% — + are largely code that CI already exercises for real, just not under the coverage instrument. + Prioritising by percentage would start in exactly the wrong place. + +2. **`JustDummies.Xunit` is not in the denominator at all.** A shipping NuGet package is classified by + Sonar as test code and its coverage never counts, in either direction. Any "100% of the solution" + target silently excludes it today. See [§5](#5-a-measurement-blind-spot-justdummiesxunit). + +3. **The cheapest available move is already written in this repository.** JustDummies has a reflective + `NullArgumentGuardConventionTests` that invokes every member with `null` and asserts the resulting + `ArgumentNullException`. No other project has an equivalent — which is why the core library's 49 + branch gaps are 30 null guards, 24 of them in `OutcomeTaskExtensions.cs` alone. + +The quality gate is **green** and measures *new* code only (88.3% against an 80% bar). Nothing is +failing. Every number in this document is therefore a choice, not a requirement. + +## 2. Headline metrics + +| Metric | Value | +|---|---| +| Overall coverage | **86.6%** | +| Line coverage | 91.1% — 840 uncovered of 9,449 lines to cover | +| Branch coverage | 80.2% — 1,303 uncovered of 6,572 conditions to cover | +| Total gap units | **2,143** (840 lines + 1,303 branches) | +| Quality gate | Pass — `new_coverage` 88.3% vs an 80% threshold | +| Analysed size | 20,668 ncloc across 614 components; 182 files carry a gap | + +Coverage over the reporting period, from SonarCloud's metric history: + +| Date | 07-09 | 07-12 | 07-19 | 07-26 | 07-27 | 07-28 | 07-29 | 07-30 | +|---|---|---|---|---|---|---|---|---| +| Coverage | 78.1% | 79.3% | 79.3% | 82.8% | 84.2% | 86.1% | 86.6% | 86.6% | + +## 3. Where the gap sits + +Per project, ordered by gap size. `LTC` is lines to cover, `CTC` conditions to cover. + +| Project | Coverage | LTC | CTC | Unc. lines | Unc. branches | Gap units | +|---|---:|---:|---:|---:|---:|---:| +| `JustDummies` | 90.4% | 3,229 | 2,745 | 167 | 404 | **571** | +| `JustDummies.Analyzers` | 88.0% | 1,451 | 1,671 | 50 | 324 | **374** | +| `FirstClassErrors.Cli` | 53.9% | 502 | 248 | 227 | 119 | **346** | +| `FirstClassErrors.GenDoc` | 84.2% | 1,414 | 542 | 161 | 148 | **309** | +| `FirstClassErrors.Usage` | 67.1% | 351 | 72 | 81 | 58 | 139 | +| `FirstClassErrors.Analyzers` | 89.7% | 858 | 494 | 8 | 131 | 139 | +| `FirstClassErrors.RequestBinder.Usage` | 75.1% | 352 | 98 | 55 | 57 | 112 | +| `FirstClassErrors` | 94.7% | 701 | 482 | 14 | 49 | 63 | +| `FirstClassErrors.RequestBinder` | 93.2% | 483 | 176 | 33 | 12 | 45 | +| `FirstClassErrors.GenDoc.Worker` | 0.0% | 43 | 0 | 43 | 0 | 43 | +| `FirstClassErrors.Testing` | 98.2% | 65 | 44 | 1 | 1 | 2 | + +`FirstClassErrors.RequestBinder.Benchmarks` is absent because it is already excluded from coverage by +`sonar.coverage.exclusions` in [`sonar.yml`](../workflows/sonar.en.md) — a measurement harness that is +never shipped and never unit-tested. Its code still gets the SonarAnalyzer pass. + +The twelve files carrying the largest gaps: + +| # | File | Unc. lines | Unc. branches | Gap | Coverage | +|---:|---|---:|---:|---:|---:| +| 1 | `FirstClassErrors.GenDoc/SolutionErrorDocumentationGenerator.cs` | 143 | 83 | 226 | 50.0% | +| 2 | `JustDummies/Any.Combine.cs` | 0 | 75 | 75 | 76.6% | +| 3 | `JustDummies/WideIntervalSpec.cs` | 16 | 46 | 62 | 80.9% | +| 4 | `JustDummies/DecimalIntervalSpec.cs` | 19 | 42 | 61 | 83.1% | +| 5 | `JustDummies/RegexParser.cs` | 17 | 41 | 58 | 90.8% | +| 6 | `FirstClassErrors.Usage/Model/Temperature.cs` | 35 | 22 | 57 | 0.0% | +| 7 | `JustDummies.Analyzers/ScalarConstraintState.cs` | 1 | 56 | 57 | 74.3% | +| 8 | `FirstClassErrors.Cli/RendererLoader.cs` | 27 | 21 | 48 | 7.7% | +| 9 | `FirstClassErrors.GenDoc.Worker/Program.cs` | 43 | 0 | 43 | 0.0% | +| 10 | `FirstClassErrors.Usage/Utils/DocumentationFormatter.cs` | 21 | 19 | 40 | 46.7% | +| 11 | `JustDummies.Analyzers/RejectedConstantArgumentAnalyzer.cs` | 14 | 26 | 40 | 86.5% | +| 12 | `FirstClassErrors.Cli/CatalogSnapshotSource.cs` | 26 | 12 | 38 | 0.0% | + +## 4. The five kinds of gap + +Every one of the 2,143 units was classified from its own source line. The buckets are disjoint and sum +to the total. + +| Kind | Unc. lines | Unc. branches | Units | Share | +|---|---:|---:|---:|---:| +| **V1** — exercised by CI, invisible to the instrument | 186 | 83 | 269 | 12.6% | +| **V2** — sample and demo code | 136 | 115 | 251 | 11.7% | +| **V3** — a test closes it today | 340 | 993 | **1,333** | **62.2%** | +| **V4** — needs a seam before a test can reach it | 176 | 88 | 264 | 12.3% | +| **V5** — practically unreachable | 2 | 24 | 26 | 1.2% | +| **Total** | 840 | 1,303 | 2,143 | 100% | + +### V1 — exercised by CI, invisible to the instrument (269 units) + +`SolutionErrorDocumentationGenerator`'s MSBuild shell-out (226 units) and `GenDoc.Worker`'s entry point +(43 units). The uncovered regions are precisely the process-spawning paths: `DotNetBuild`, +`DotNetGetProperty`, `RunProcess`'s timeout and kill branches, and `RunWorker`'s subprocess invocation. + +These are not untested. [`canary.yml`](../../../../.github/workflows/canary.yml) runs the real `fce.dll +generate` against a real project, captures the worker's diagnostics, and *asserts* two things: that the +emitted catalog contains error codes, and that the worker's own banner reports the newest runtime +(`Documenting 
 on .NET .`). [`gendoc-docs.yml`](../../../../.github/workflows/gendoc-docs.yml) runs +the same binary to regenerate the committed catalog. That is a *better* test of the MSBuild and +roll-forward behaviour than any mock could be. These paths are uncovered because `dotnet test` never +spawns them — a property of the instrument, not of the test bed. + +One caveat: the canary only runs when a .NET preview is available and skips otherwise, so it is not a +per-commit guarantee. `gendoc-docs` has no such condition. + +### V2 — sample and demo code (251 units) + +`FirstClassErrors.Usage` and `FirstClassErrors.RequestBinder.Usage`. The intent is already on record in +the code: `Usage/Model/Amount.cs` carries a `SuppressMessage` justifying that the comparison operators +are out of scope because they "would add untested surface to a type the tests only exercise +indirectly". The coverage scope has simply never been made to match that stated intent. + +### V3 — a test closes it today (1,333 units) + +No refactor, no seam, no policy decision — only tests that do not exist yet. 993 of these are branches. +This is the only bucket where writing tests is the answer, and it is broken open in [§6](#6-what-the-1333-actionable-units-are-made-of). + +### V4 — needs a seam before a test can reach it (264 units) + +The CLI's `renderer` and `config` command branch writes straight to `Console.Out` and calls +`Assembly.LoadFrom`, while the `generate` and `catalog` commands go through `IOutputSink`, +`IErrorDocumentationGenerator` and `ICatalogSnapshotSource` and sit between 80% and 97%. + +The untested commands are exactly the ones that never adopted the seam the project already has. The +list is `RendererLoader`, `RendererListCommand`, `RendererAddCommand`, `RendererRemoveCommand`, +`ConfigShowCommand`, `ConsoleGenerationLogger`, `CatalogSnapshotSource`, `CatalogSourceResolver`, +`RendererCatalog`, `SolutionErrorDocumentationGeneratorAdapter` — the last of which is the production +side of the very seam the tests drive with doubles — plus `Cli/Program.cs`, which is Spectre wiring. + +### V5 — practically unreachable (26 units) + +Defensive guards against states the compiler cannot produce: Roslyn `is not ` checks on operation +and symbol types, and `default:` arms of exhaustive switches. **This count is a floor, not a total** — +it is only what could be proved from syntax alone; the true number of unreachable defensive branches is +higher. Chasing these costs correctness, because the only way to "cover" such a guard is to delete a +guard that is there on purpose. + +## 5. A measurement blind spot: `JustDummies.Xunit` + +`JustDummies.Xunit/ReproducibleAttribute.cs` — 60 code lines of a **shipping NuGet package** — is filed +by SonarCloud under qualifier `UTS` (unit-test source), not main code. The SonarScanner for .NET +classifies a project as a test project when it references a test framework, and this package references +`xunit.v3.extensibility.core` because that is exactly what it is: the xUnit adapter. + +The consequence is *not* that it is untested — `JustDummies.Xunit.UnitTests` exists and exercises it, +including through an `InternalsVisibleTo` seam deliberately added so the "report only on failure" rule +can be proved without a test that has to fail for real. The consequence is that its coverage **never +counts**, in either direction: a regression that dropped it to zero would move the reported number by +0.0 points, and the work already done to test it earns nothing. + +Any "100% of the solution" target has this package silently outside it. Correcting it means forcing the +classification — `sonar.test.exclusions`, or an explicit `SonarQubeTestProject=false` on that one +project. + +## 6. What the 1,333 actionable units are made of + +This is bucket **V3** broken open — the units a test can close today. + +| Pattern | Unc. lines | Unc. branches | Units | Shape of the fix | +|---|---:|---:|---:|---| +| Analyzer guard chains and case dispatch | 56 | 431 | **487** | Negative-case snippets through the existing `AnalyzerTestHarness` | +| JustDummies spec engines (interval, string, regex) | 73 | 205 | **278** | Boundary and exhaustion cases; `DescribeExhaustion` and `Cardinality` are never reached | +| Rest of the `Any` surface | 34 | 68 | 102 | Constraint builders dead on specific scalar types — `MultipleOf` on `AnySByte`, `LessThan` on `AnyUInt16`, 
 | +| GenDoc renderers and versioning | 18 | 65 | 83 | Renderer edge cases and catalog-diff labels; the seam already exists and is tested | +| CLI, the part that is already seamed | 51 | 31 | 82 | More cases through the doubles `GenerateCommand` and the catalog commands already use | +| `Any` introspection interfaces never called | 55 | 9 | 64 | One reflective theory over every `Any` — 26 files closed at once | +| Range and domain guards never violated | 4 | 57 | 61 | A convention test that passes each guard its illegal value | +| Null-argument guards never given a `null` | 4 | 52 | 56 | Port JustDummies' `NullArgumentGuardConventionTests` to the other projects | +| `Any.Combine` `??` chains (operand-position matrix) | 1 | 51 | 52 | A theory varying which operand carries the `RandomSource` | +| `FirstClassErrors` core library | 14 | 19 | 33 | Loader-failure and null-name paths in `AssemblyErrorDocumentationReader` | +| `FirstClassErrors.RequestBinder` | 29 | 4 | 33 | `BindingScope.Get` and the simple-property converter path | +| Other (`FirstClassErrors.Testing`) | 1 | 1 | 2 | — | +| **Total** | **340** | **993** | **1,333** | | + +### The four replicated patterns + +Several entries above are one pattern repeated across many files, which is what makes them worth +attacking: a single harness closes dozens of units at once. + +**The `Any` introspection matrix — 64 units across 26 files.** `IHasRandomSource.Source`, +`ICardinalityHint.DistinctCardinality` and `ICardinalityHint.Contains` are explicit interface +implementations, and for most scalar types nothing in the suite ever routes through them. One +reflection-driven theory over every `Any` closes all 26 files at once — and the repository already +has that harness shape in `SurfaceParityTests`, `FactoryNamingConventionTests` and +`NullArgumentGuardConventionTests`. + +**Guards that exist but are never violated — 117 units.** Split by exception type and project: + +| Project | `ArgumentNullException` | `ArgumentOutOfRangeException` | `ArgumentException` | Total | +|---|---:|---:|---:|---:| +| `JustDummies` | 14 | 10 | 47 | 71 | +| `FirstClassErrors` | 30 | 0 | 0 | 30 | +| `FirstClassErrors.RequestBinder` | 8 | 0 | 0 | 8 | +| `FirstClassErrors.Usage` | 0 | 0 | 2 | 2 | +| **Total** | **52** | **10** | **49** | **111** | + +JustDummies' `NullArgumentGuardConventionTests` reflectively invokes every member with a `null` and +asserts the `ArgumentNullException` — which is why its null column is the smallest despite being the +largest project. **`FirstClassErrors` has no equivalent**, and its 30 uncovered null guards are the +direct result; 24 of them sit in `OutcomeTaskExtensions.cs`, one per `is null` guard on `next`, +`fallback`, `onSuccess` and `onFailure`. Porting that one convention test is the single cheapest move +in this document. The same gap exists for **range and domain guards**, which no convention test covers +in any project. + +**`Any.Combine`'s operand-position matrix — 52 units in one file.** Each arity overload chains +`SourceOf(first) ?? SourceOf(second) ?? 
`, so an *N*-operand overload emits 2*N* branches, and the +tests only ever put the source in the first position. `Any.Combine.cs` has 0 uncovered *lines* and 75 +uncovered *branches* — every line runs, half the paths never do. A theory that varies which operand +carries the source walks the whole chain. + +**Analyzer guard chains — 487 units, the largest single bucket.** By expression shape, across both +analyzer projects (455 branch units classified): + +| Shape | Units | Share | +|---|---:|---:| +| null / null-conditional guard on a Roslyn symbol | 150 | 33.0% | +| simple `if` covered on one side only | 109 | 24.0% | +| other | 71 | 15.6% | +| `switch` / `case` dispatch | 48 | 10.5% | +| `&&` / `\|\|` short-circuit | 35 | 7.7% | +| `is not ` guard (bucket V5) | 24 | 5.3% | +| loop with no zero-iteration path | 12 | 2.6% | +| `??` coalesce | 6 | 1.3% | + +Most of these are genuine analyzer paths — malformed or unusual syntax the analyzer must survive — +reachable through the existing `AnalyzerTestHarness` with negative-case source snippets. Note the +contrast with mutation testing, which this repository already gates on (ADR-0043, ADR-0046): many of +these branches are *executed* but never *asserted*, so they are likely surviving mutants too. + +## 7. What each decision buys + +Two different levers move the number and should not be confused. **Exclusions** change the denominator +and cost nothing but a documented decision. **Tests** change the numerator and cost work. Figures are +cumulative, computed with Sonar's own formula `((LTC − unL) + (CTC − unC)) / (LTC + CTC)` against the +per-file measures. + +| Step | Lever | Units | Coverage | +|---|---|---:|---:| +| Today | — | — | 86.62% | +| Exclude the two `Usage` sample projects | denominator | −251 | 87.51% | +| 
 and `GenDoc.Worker`, the worker entry point | denominator | −43 | 87.76% | +| 
 and `Cli/Program.cs`, the Spectre wiring | denominator | −17 | 87.86% | +| 
 and `SolutionErrorDocumentationGenerator.cs`, the MSBuild shell-out | denominator | −226 | 89.03% | +| 
 then cover the ten un-seamed CLI files | numerator (after a seam) | −247 | **90.71%** | + +After all of it, **1,359 units remain and 90.7% is the ceiling of the cheap moves** — 1,084 of them in +JustDummies and the analyzers, overwhelmingly branches. There is no shortcut past that bucket: it is +the actual work, and it is also the code where correctness matters most. + +## 8. Recommendation + +1. **Fix the blind spot first — it is a measurement bug, not a coverage gap.** Force + `JustDummies.Xunit` to be analysed as main code. Until then no coverage target actually covers the + solution, and the number cannot be trusted to move when that package regresses. + +2. **Decide the scope explicitly, once, in an ADR.** The samples, the process entry points and the + MSBuild shell-out are 520 units — a quarter of the total — that no unit test should ever be written + for. `Benchmarks` is already excluded for exactly this reason and the reasoning is already written + down in `sonar.yml`; this extends the same rule to the same kind of code. That is a lasting decision + a future maintainer would question, so it wants an ADR rather than a comment. + +3. **Make the CI exercise count, instead of writing unit tests to imitate it.** `canary` and + `gendoc-docs` already run the real `fce generate`, spawn the real worker and assert on the result. + Either collect coverage from those runs, or exclude the path and say why — but do not write a fake + `IProcessRunner` to make a number move. If the path is excluded, note that the canary is + preview-conditional, so `gendoc-docs` is the exercise that actually runs on every relevant push. + +4. **Port the null-guard convention test out of JustDummies.** One harness, already written and proven + in this repository, applied to `FirstClassErrors`, `RequestBinder` and `GenDoc`. Closes 56 units, + removes a whole category permanently, and every future guard is covered on the day it is written. + Then extend the same harness to range guards (+61). + +5. **Then the two reflection matrices in JustDummies.** The `Any` introspection interfaces (64) and + `Any.Combine`'s operand positions (52). Both are single theories over an existing type list. Per + ADR-0040 (held here at the time, moved with the extraction) these + are invariants that hold for every legal argument, so they belong in `JustDummies.PropertyTests`, + not the unit suite — see *Writing JustDummies tests*, likewise moved. + +6. **Only then the analyzers — and drive them from mutation, not coverage.** 487 units, mostly branches + that are already *executed* but not *asserted*. Coverage will report them closed as soon as a snippet + reaches them; only the mutation sweep will tell you whether the test actually pinned the behaviour. + The repository already runs that sweep — let it pick the targets here rather than the coverage + percentage. + +### Candidate ADRs + +Two decisions above are lasting ones a future maintainer would question, and are offered as drafts: + +- **The coverage scope policy** — which categories of code are deliberately outside the coverage + denominator (samples, process entry points, process shell-outs) and why. Recommendation 2. +- **Where the process-level paths are verified** — that the `canary` and `gendoc-docs` exercises are the + accepted verification for the MSBuild and worker paths, rather than mocked unit tests. + Recommendation 3. + +Neither is drafted here. Per the repository convention, an agent proposes and never accepts. + +## 9. What this analysis does not claim + +**That 100% is the right target.** The quality gate is on new code and it is green at 88.3%. Nothing +here is failing. The reachable ceiling after every reasonable move is roughly 96–97%, because bucket V5 +is real and its 26 units are only the ones provable from syntax. + +**That coverage measures test quality.** This repository already knows that: it gates on mutation score +precisely because a test can execute a line without asserting anything about it (ADR-0043, ADR-0046). +Several buckets above would go green under coverage while staying red under Stryker. Where the two +disagree, the mutation sweep is the one telling the truth. + +**That the figures are current.** They describe `da6e7ee`. `main` has advanced since, including +refactors inside `JustDummies`, so per-file numbers will have shifted. The structure of the analysis — +the five kinds, the replicated patterns, the two levers — is what is meant to outlive the snapshot. + +## 10. Reproducing these figures + +All data is public; the SonarCloud project is readable without a token. + +```sh +# Headline metrics +curl -s "https://sonarcloud.io/api/measures/component?component=reefact_first-class-errors\ +&metricKeys=coverage,line_coverage,branch_coverage,uncovered_lines,uncovered_conditions,\ +lines_to_cover,conditions_to_cover,ncloc" + +# Per-file measures (paginate: ps=500, p=1 then p=2) +curl -s "https://sonarcloud.io/api/measures/component_tree?component=reefact_first-class-errors\ +&metricKeys=coverage,uncovered_lines,uncovered_conditions,lines_to_cover,conditions_to_cover\ +&strategy=leaves&ps=500&p=1&s=metric&metricSort=uncovered_lines&asc=false" + +# Per-line hits and branch counts for one file +curl -s "https://sonarcloud.io/api/sources/lines?key=reefact_first-class-errors%3A" +``` + +A line is uncovered when `lineHits == 0`; a line has missing branches when `coveredConditions < +conditions`. Summing `uncovered_lines` and `(conditions − coveredConditions)` over all files must +reproduce the project totals — that reconciliation is what makes the classification exhaustive rather +than indicative. + +## Related + +- [`sonar` workflow](../workflows/sonar.en.md) — how the analysis and its coverage report are produced. +- [`sonar-gate` workflow](../workflows/sonar-gate.en.md) — how the quality gate is read back. +- [`ci` workflow](../workflows/ci.en.md) — produces the same OpenCover shape via `coverage.runsettings`. +- [`mutation`](../workflows/mutation.en.md) — the check that measures whether a covered line is + actually asserted. Its JustDummies counterpart, `justdummies-mutation`, left with the extraction. +- *Writing JustDummies tests* — which suite a new JustDummies test belongs to; it now lives in + [`Reefact/just-dummies`](https://github.com/Reefact/just-dummies).