From 668b12514899173bb4a946604fedc24244e7e118 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 30 Jul 2026 15:20:17 +0000 Subject: [PATCH 1/2] docs: add the 2026-07-30 coverage analysis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Classify all 2,143 uncovered units reported by SonarCloud at da6e7ee — 840 lines and 1,303 branches — from their own source, into five disjoint kinds: verified by CI outside the coverage instrument, sample code, closable by a test today, blocked behind a missing seam, and practically unreachable. The reconstructed per-line totals match Sonar's published figures exactly, so the classification covers the whole population rather than a sample. Records two things worth acting on independently of any coverage target: JustDummies.Xunit is analysed as test code and so sits outside the denominator entirely, and the null-guard convention test that JustDummies already owns has no equivalent in the other projects. The document is advisory per ADR-0004 and names two candidate ADRs — the coverage scope policy, and where the process-level paths are verified — without drafting either. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_012Yz2hMQaajVd5qPRfxRMxd --- .../audit/2026-07-30-coverage-analysis.fr.md | 414 ++++++++++++++++++ .../audit/2026-07-30-coverage-analysis.md | 387 ++++++++++++++++ 2 files changed, 801 insertions(+) create mode 100644 doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.fr.md create mode 100644 doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.md 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..7bbf73d0 --- /dev/null +++ b/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.fr.md @@ -0,0 +1,414 @@ +# 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. + +**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](../adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md), + 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](../WritingJustDummiesTests.fr.md). + +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) et + [`justdummies-mutation`](../workflows/justdummies-mutation.fr.md) — les contrôles qui mesurent si une + ligne couverte est réellement vérifiée. +- [Écrire des tests JustDummies](../WritingJustDummiesTests.fr.md) — à quelle suite appartient un + nouveau test JustDummies. 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..6eea4126 --- /dev/null +++ b/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.md @@ -0,0 +1,387 @@ +# 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. + +**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](../adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.md) these + are invariants that hold for every legal argument, so they belong in `JustDummies.PropertyTests`, + not the unit suite — see [Writing JustDummies tests](../WritingJustDummiesTests.en.md). + +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) and + [`justdummies-mutation`](../workflows/justdummies-mutation.en.md) — the checks that measure whether a + covered line is actually asserted. +- [Writing JustDummies tests](../WritingJustDummiesTests.en.md) — which suite a new JustDummies test + belongs to. From 94ca26cf9152b28f9f693b65fcdd87e76bb8e411 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 10 Aug 2026 17:00:11 +0000 Subject: [PATCH 2/2] docs: repoint the links the JustDummies extraction broke The analysis was written on 2026-07-30 and referenced JustDummies records, guides and workflow documentation that left this repository with the extraction of 2026-08-07 (ADR-0069). Name what moved instead of linking to files that are gone, and date the change in the header. --- .../audit/2026-07-30-coverage-analysis.fr.md | 19 ++++++++++++------- .../audit/2026-07-30-coverage-analysis.md | 19 ++++++++++++------- 2 files changed, 24 insertions(+), 14 deletions(-) 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 index 7bbf73d0..c4b753eb 100644 --- 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 @@ -12,6 +12,12 @@ 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 @@ -335,10 +341,10 @@ compte le plus. 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](../adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.fr.md), + 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](../WritingJustDummiesTests.fr.md). + *É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 @@ -407,8 +413,7 @@ classification exhaustive plutôt qu'indicative. 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) et - [`justdummies-mutation`](../workflows/justdummies-mutation.fr.md) — les contrôles qui mesurent si une - ligne couverte est réellement vérifiée. -- [Écrire des tests JustDummies](../WritingJustDummiesTests.fr.md) — à quelle suite appartient un - nouveau test JustDummies. +- [`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 index 6eea4126..24cbf3d5 100644 --- a/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.md +++ b/doc/handwritten/for-maintainers/audit/2026-07-30-coverage-analysis.md @@ -12,6 +12,12 @@ 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 @@ -314,9 +320,9 @@ the actual work, and it is also the code where correctness matters most. 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](../adr/0040-split-the-justdummies-test-bed-between-example-and-property-suites.md) these + 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](../WritingJustDummiesTests.en.md). + 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 @@ -380,8 +386,7 @@ than indicative. - [`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) and - [`justdummies-mutation`](../workflows/justdummies-mutation.en.md) — the checks that measure whether a - covered line is actually asserted. -- [Writing JustDummies tests](../WritingJustDummiesTests.en.md) — which suite a new JustDummies test - belongs to. +- [`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).