diff --git a/doc/handwritten/for-maintainers/adr/0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.fr.md b/doc/handwritten/for-maintainers/adr/0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.fr.md new file mode 100644 index 00000000..72716932 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.fr.md @@ -0,0 +1,172 @@ +# ADR-0067 | Traiter les codes de sortie du CLI comme un contrat fermé et publié + +🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.md) + +**Statut :** Accepté +**Proposé :** 2026-07-31 +**Accepté :** 2026-07-31 +**Décideurs :** Reefact + +## Contexte + +`fce` est un outil de chaîne d'intégration avant d'être un outil interactif. `fce catalog diff` +existe pour faire échouer un job lorsque le catalogue d'erreurs dérive : son rapport part sur la +sortie standard, et la chaîne se branche sur ce que le processus retourne. La documentation +utilisateur le dit et le montre — la référence du versionnage de catalogue liste un tableau de codes +de sortie par commande, et les pipelines d'exemple du guide CI propagent le code pour décider du +résultat du job. Pour cette commande, le code de sortie n'est pas un effet de bord de l'exécution : +c'est la réponse. + +Quatre codes sont publiés aujourd'hui : `0` (la commande a fait ce qu'on lui demandait), `1` (erreur +d'exécution), `2` (`catalog diff` a trouvé des changements atteignant le seuil choisi par +`--fail-on`) et `130` (l'exécution a été interrompue). `2` est délibérément distinct de `1` : une +chaîne doit pouvoir séparer « l'outil a fonctionné et le catalogue a bougé » de « l'outil n'a pas pu +s'exécuter ». `130` est la valeur conventionnelle d'un processus tué par SIGINT, `128 + 2`. + +Jusqu'à récemment ces nombres étaient des littéraux nus à 32 endroits dans neuf fichiers de commande, +et les trois commandes retournant `130` portaient chacune son propre commentaire en prose expliquant +sa signification. Ils sont désormais nommés dans un type `ExitCodes` par exécutable, et les tests de +commande les assertent — `130` compris. Les nommer est ce qui rend l'ensemble lisible pour la +première fois ; ce n'est pas ce qui en fait une promesse. + +L'ensemble n'est en réalité pas fermé aujourd'hui. Le CLI délègue l'analyse de la ligne de commande à +Spectre.Console.Cli, qui possède le chemin d'échec d'une ligne inanalysable et répond avec un code de +son choix. Une sous-commande inconnue sort en `-1` — rapporté 255 par un shell POSIX — sans rien +écrire sur aucun des deux flux standard. Cette cinquième valeur ne figure dans aucun tableau, a été +choisie par une dépendance plutôt que par ce dépôt, et atteint un appelant qui a demandé quelque +chose que l'outil n'a pas. L'audit d'architecture et de conception du 2026-07-20 avait déjà consigné +ce point comme un élément à normaliser et documenter. + +Rien ne contraint l'ensemble mécaniquement. Les tests assertent les valeurs que les commandes +retournent aujourd'hui, ce qui n'est pas la même affirmation que « aucune commande ne peut en +retourner une sixième ». Une commande ajoutée demain compile tout aussi bien en retournant `3`, et un +script qui lit `2` comme « des changements ont été trouvés » casse silencieusement si une commande +ultérieure emprunte `2` pour autre chose. + +## Décision + +Les codes de sortie que retournent `fce` et son worker forment un ensemble fermé aux significations +publiées et fixes, possédé par ce dépôt, et étendu ou modifié uniquement par un acte délibéré et +documenté. + +## Justification + +Un code de sortie est la seule partie d'un outil en ligne de commande qu'une machine consomme. Tout +le reste de ce que `fce` émet — le rapport, les lignes de journal, les diagnostics — est lu par une +personne capable de s'adapter ; le code de sortie est lu par une chaîne qui ne le peut pas. Cela le +place dans la même catégorie qu'une signature d'API publique : le dépôt traite déjà le renommage d'un +code d'erreur ou d'un type public comme un changement cassant, et un code de sortie sur lequel un job +se branche pèse autant. Il est déjà publié, donc la promesse existe qu'elle soit consignée ou non ; ce +qui manquait, c'est le relevé de ce qu'elle couvre et de ce que la rompre coûterait. + +La consigner comme décision plutôt que la laisser à l'état d'habitude, c'est ce que démontre le trou +en `-1`. Un contrat implicite ne reste pas entier tout seul : personne n'a décidé qu'une ligne de +commande inanalysable devait sortir en `-1` silencieusement, et personne ne l'a remarqué tant que les +nombres étaient des littéraux dispersés dans neuf fichiers. Un contrat énoncé une fois peut être +confronté ; un contrat qui n'existe que dans la somme de ses sites d'appel dérive sans que personne +n'ait pris la décision de le laisser faire. + +La décision porte sur l'ensemble, pas sur la façon dont il est épelé. Si `ExitCodes` devenait une +énumération, si le CLI quittait Spectre.Console.Cli, ou si les commandes étaient entièrement +réécrites, « ces codes signifient ces choses et l'ensemble est fermé » tiendrait toujours et ce +relevé n'aurait pas à être modifié — ce qui est précisément le test que cette base applique pour +décider si une décision y a sa place. + +Fermer l'ensemble coûte la liberté d'ajouter un code à la légère, et ce coût est le but. Un sixième +code est bon marché à ajouter et cher à reprendre, parce que l'outil ne peut pas savoir quelles +chaînes lisent déjà le cinquième. Rendre l'ajout délibéré — un changement de documentation dans les +deux langues, pesé comme tout autre changement de compatibilité — place le coût là où il est visible, +au moment du choix plutôt qu'au moment où le build d'un utilisateur casse. + +## Alternatives considérées + +### Laisser les codes implicites, comme ils l'étaient + +Considérée parce que les valeurs sont déjà assertées par les tests de commande et déjà listées dans +la documentation utilisateur : un lecteur qui regarde aux deux bons endroits peut reconstituer +l'ensemble. + +Rejetée parce que reconstituer n'est pas promettre. Les tests épinglent ce que les commandes font ; +ils n'interdisent pas un sixième code, et ils n'ont rien dit pendant que le chemin d'erreur d'analyse +répondait `-1` en dehors de tout tableau publié. C'est le mode de défaillance d'un contrat non +consigné, observé dans ce dépôt plutôt qu'imaginé. + +### Traiter les codes de sortie comme un détail d'implémentation de chaque commande + +Considérée parce que chaque commande décide de son propre dénouement, et qu'on pourrait soutenir que +les codes appartiennent à la commande plutôt qu'à l'outil. + +Rejetée comme contredite par la documentation publiée et par la vocation de l'outil. Les tableaux de +référence sont par commande, mais une chaîne lit un seul nombre d'un seul processus, et `0`, `1` et +`130` signifient la même chose dans toutes les commandes par conception. Éparpiller la propriété, +c'est ainsi que `130` en est venu à être expliqué par trois commentaires séparés disant la même +chose. + +### Modéliser l'ensemble par une énumération plutôt que par des constantes entières nommées + +Considérée parce qu'une énumération ferait de l'ensemble un type, et qu'une valeur hors de cet +ensemble exigerait une conversion explicite. + +Rejetée parce que les valeurs doivent atteindre le processus en entiers bruts : le contrat du +framework de commandes retourne `int`, donc chaque commande convertirait à son retour, et le +compilateur ne pourrait toujours pas empêcher une conversion de `3`. Le typage est nominal alors que +la friction est réelle — et la décision consignée ici porte sur le caractère fermé et publié de +l'ensemble, ce qu'aucune construction C# n'exprime dans un sens ou dans l'autre. + +### Corriger le code d'erreur d'analyse dans le cadre de cette décision + +Considérée parce que ce trou est ce qui a révélé le problème, et que le combler ici réglerait +l'affaire d'un seul mouvement. + +Rejetée parce que la valeur qu'une ligne de commande inanalysable doit retourner, et ce qu'elle doit +afficher, est un choix de conception avec ses propres arbitrages — réutiliser `1`, ou réserver un code +distinct pour qu'une chaîne puisse séparer une invocation fautive d'une exécution échouée. Ce relevé +établit que l'ensemble est fermé et possédé ; il laisse le choix de cette valeur au suivi qui le +comblera, ce qui relève de la spécification. + +## Conséquences + +### Positives + +* Une chaîne qui se branche sur le code de sortie de `fce` dispose d'une promesse sur laquelle + s'appuyer d'une version à l'autre, et les recettes CI du versionnage de catalogue reposent sur + quelque chose de consigné plutôt que sur le comportement du moment. +* Une nouvelle commande a une réponse à « que dois-je retourner » qui n'exige pas de lire neuf autres + fichiers. +* Étendre l'ensemble devient visible : c'est une décision assortie d'un changement de documentation + dans les deux langues, pas un littéral tapé à un `return`. + +### Négatives + +* Ajouter un code de sortie coûte désormais davantage que taper un nombre — les tableaux de référence + anglais et français bougent avec lui, et l'ajout se pèse comme un changement de compatibilité. +* Le dépôt assume une promesse dont il n'admettait pas la propriété jusqu'ici, y compris pour les + chemins auxquels une dépendance répond actuellement. + +### Risques + +* Le chemin d'erreur d'analyse en `-1` contredit la décision le jour même où elle est proposée. Tant + que le suivi ne l'a pas comblé, l'ensemble publié et le comportement réel de l'outil divergent — le + relevé rend cette divergence visible, il ne la supprime pas. +* Rien ne vérifie la règle. Une commande future peut retourner `3` et compiler, et aucun test ni + analyseur n'y objectera ; ce relevé est une règle que le relecteur applique, pas une que le build + fait respecter — l'arrangement contre lequel l'ADR-0056 met en garde, accepté ici parce que la + surface est petite et relue. + +## Suites à donner + +* Normaliser et documenter le code de sortie d'une ligne de commande inanalysable, et lui donner un + diagnostic sur la sortie d'erreur — l'élément déjà soulevé par l'audit d'architecture et de + conception du 2026-07-20. +* Tenir les tableaux de codes de sortie de la référence du versionnage de catalogue, en anglais comme + en français, en phase avec les types `ExitCodes` à chaque changement de l'ensemble. + +## Références + +* [ADR-0056](0056-state-the-coding-rules-where-an-agent-can-act-on-them.fr.md) — ce qu'il advient + d'une règle sur laquelle rien ne peut agir, qui est le risque accepté par ce relevé. +* Référence des commandes de versionnage de catalogue, anglaise et française — les tableaux de codes + de sortie publiés. +* Guide CI du versionnage de catalogue — les pipelines d'exemple qui se branchent sur le code. +* Audit d'architecture et de conception du 2026-07-20 — le code de sortie d'erreur d'analyse soulevé + comme élément. diff --git a/doc/handwritten/for-maintainers/adr/0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.md b/doc/handwritten/for-maintainers/adr/0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.md new file mode 100644 index 00000000..f35c2828 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.md @@ -0,0 +1,155 @@ +# ADR-0067 | Treat the CLI's exit codes as a closed, published contract + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.fr.md) + +**Status:** Accepted +**Proposed:** 2026-07-31 +**Accepted:** 2026-07-31 +**Decision Makers:** Reefact + +## Context + +`fce` is a build-pipeline tool before it is an interactive one. `fce catalog diff` exists to fail a +job when the error catalog drifts: its report goes to standard output, and the pipeline branches on +what the process returns. The user documentation says so and shows it — the catalog-versioning +reference lists an exit-code table per command, and the CI guide's example pipelines propagate the +code to decide the job result. For that command the exit code is not a side effect of running; it is +the answer. + +Four codes are published today: `0` (the command did what it was asked), `1` (execution error), `2` +(`catalog diff` found changes at or above the threshold `--fail-on` selected), and `130` (the run was +interrupted). `2` is deliberately distinct from `1`: a pipeline must be able to tell "the tool +worked, and the catalog moved" from "the tool could not run". `130` is the conventional value for a +process killed by SIGINT, `128 + 2`. + +Until recently those numbers were bare literals at 32 sites across nine command files, and the three +commands returning `130` each carried their own prose comment explaining what it meant. They are now +named in one `ExitCodes` type per executable, and the command tests assert them — including `130`. +Naming them is what makes the set legible for the first time; it is not what makes it a promise. + +The set is not in fact closed today. The CLI delegates command-line parsing to Spectre.Console.Cli, +which owns the failure path for an unparseable command line and answers with a code of its own +choosing. An unknown subcommand exits `-1` — reported as 255 by a POSIX shell — writing nothing to +either standard stream. That fifth value appears in no exit-code table, was chosen by a dependency +rather than by this repository, and reaches a caller who asked for something the tool does not have. +The 2026-07-20 architecture and design audit already recorded this as an item to normalize and +document. + +Nothing mechanically constrains the set. The tests assert the values the commands return today, +which is a different statement from "no command may return a sixth". A new command added tomorrow +compiles just as well returning `3`, and a script that reads `2` as "changes found" breaks silently +if some later command borrows `2` for something else. + +## Decision + +The exit codes `fce` and its worker return are a closed set with fixed published meanings, owned by +this repository, and extended or changed only as a deliberate, documented act. + +## Rationale + +An exit code is the one part of a command-line tool that a machine consumes. Everything else `fce` +emits — the report, the log lines, the diagnostics — is read by a person who can adapt; the exit +code is read by a pipeline that cannot. That places it in the same category as a public API +signature: the repository already treats renamed error codes and public types as breaking changes, +and an exit code a CI job branches on carries the same weight. It is already published, so the +promise exists whether or not it is recorded; what was missing is the record of what the promise +covers and what breaking it costs. + +Recording it as a decision rather than leaving it a habit is what the `-1` hole demonstrates. An +implicit contract does not stay whole on its own: nobody decided that an unparseable command line +should exit `-1` silently, and nobody noticed for as long as the numbers were literals scattered +across nine files. A contract stated once can be checked against; a contract that exists only in the +sum of its call sites drifts without anyone taking a decision to let it. + +The decision is about the set, not about how the set is spelled. If `ExitCodes` became an enum, or +the CLI moved off Spectre.Console.Cli, or the commands were rewritten entirely, "these codes mean +these things and the set is closed" would still hold and this record would not need editing — which +is the test this base applies to decide whether a decision belongs here at all. + +Closing the set costs the freedom to add a code casually, and that cost is the point. A sixth code is +cheap to add and expensive to take back, because the tool cannot know which pipelines already read +the fifth. Making the addition deliberate — a documentation change in both languages, weighed like +any other compatibility change — puts the cost where it is visible, at the moment the choice is made +rather than at the moment a user's build breaks. + +## Alternatives Considered + +### Leave the codes implicit, as they were + +Considered because the values are already asserted by the command tests and already listed in the +user documentation, so a reader who looks in the right two places can reconstruct the set. + +Rejected because reconstructing is not promising. The tests pin what the commands do; they do not +forbid a sixth code, and they said nothing while the parse-error path answered `-1` outside every +published table. That is the failure mode of an unrecorded contract, observed in this repository +rather than imagined. + +### Treat exit codes as an implementation detail of each command + +Considered because each command decides its own outcome, and the codes could be argued to belong to +the command rather than to the tool. + +Rejected as contradicted by the published documentation and by the tool's purpose. The reference +tables are per command, but a pipeline reads one number from one process, and `0`, `1` and `130` +mean the same thing across every command by design. Scattering ownership is how `130` came to be +explained by three separate comments saying the same thing. + +### Model the set as an enum rather than named integer constants + +Considered because an enum would make the set a type, and a value outside it would need a cast. + +Rejected because the values must reach the process as raw integers: the command framework's contract +returns `int`, so every command would cast at its return, and the compiler still could not stop a +cast of `3`. The typing is nominal while the friction is real — and the decision recorded here is +about the set being closed and published, which no C# construct expresses either way. + +### Fix the parse-error code as part of this decision + +Considered because that hole is what exposed the problem, and closing it here would settle the +matter in one move. + +Rejected because which value an unparseable command line should return, and what it should print, is +a design choice with its own trade-offs — reusing `1`, or reserving a distinct code so a pipeline can +tell a bad invocation from a failed run. This record settles that the set is closed and owned; it +leaves the choice of that value to the follow-up that closes it, which is specification. + +## Consequences + +### Positive + +* A pipeline branching on `fce`'s exit code has a promise it can rely on across versions, and the + catalog-versioning CI recipes rest on something recorded rather than on current behaviour. +* A new command has an answer to "what do I return" that does not require reading nine other files. +* Extending the set becomes visible: it is a decision with a documentation change in both languages, + not a literal typed at a `return`. + +### Negative + +* Adding an exit code now costs more than typing a number — the reference tables in English and + French move with it, and the addition is weighed as a compatibility change. +* The repository owns a promise it did not previously admit to owning, including for the paths a + dependency currently answers for. + +### Risks + +* The `-1` parse-error path contradicts the decision the day it is proposed. Until the follow-up + closes it, the published set and the tool's actual behaviour disagree — the record makes that + disagreement visible, it does not remove it. +* Nothing checks the rule. A future command can return `3` and compile, and no test or analyzer will + object; this record is a rule the reviewer applies, not one the build enforces — the arrangement + ADR-0056 warns about, accepted here because the surface is small and reviewed. + +## Follow-up Actions + +* Normalize and document the exit code for an unparseable command line, and give it a diagnostic on + standard error — the item already raised by the 2026-07-20 architecture and design audit. +* Keep the exit-code tables in the catalog-versioning reference, English and French, in step with the + `ExitCodes` types whenever the set changes. + +## References + +* [ADR-0056](0056-state-the-coding-rules-where-an-agent-can-act-on-them.md) — what becomes of a rule + nothing can act on, which is the risk this record accepts. +* Catalog-versioning command reference, English and French — the published exit-code tables. +* Catalog-versioning CI guide — the example pipelines that branch on the code. +* 2026-07-20 architecture and design audit — the parse-error exit code raised as an item. diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 94c6f412..1172d1ab 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -268,3 +268,4 @@ Optional supporting material: | [ADR-0064](0064-exempt-the-whole-failure-reporting-path-from-the-null-guard-convention.md) | Exempt the whole failure-reporting path from the null-guard convention, declared with `[BuiltOnTheFailurePath]`; supersedes ADR-0045 | Accepted | | [ADR-0065](0065-carry-a-declared-constraint-as-a-value-object.md) | Carry a declared constraint as a value object, not as its rendered text | Proposed | | [ADR-0066](0066-declare-a-value-object-and-enforce-its-identity.md) | Declare a value object with an attribute, and enforce its identity by convention | Proposed | +| [ADR-0067](0067-treat-the-cli-s-exit-codes-as-a-closed-published-contract.md) | Treat the CLI's exit codes as a closed, published contract | Accepted |