diff --git a/FirstClassErrors.RequestBinder.UnitTests/StructuralErrorDescriptionTests.cs b/FirstClassErrors.RequestBinder.UnitTests/StructuralErrorDescriptionTests.cs
new file mode 100644
index 00000000..05525757
--- /dev/null
+++ b/FirstClassErrors.RequestBinder.UnitTests/StructuralErrorDescriptionTests.cs
@@ -0,0 +1,80 @@
+#region Usings declarations
+
+using NFluent;
+
+#endregion
+
+namespace FirstClassErrors.RequestBinder.UnitTests;
+
+///
+/// The public documentation seams a consumer uses to surface its overridden binder structural errors in its
+/// own catalog: SampleArgumentRequired/Invalid (a faithful example error) and
+/// DescribeArgumentRequired/Invalid (the binder's generic prose plus that example). Both are built from a
+/// , so a consumer that renamed the codes or localized the messages documents
+/// what it actually emits — structurally identical to what the binder raises at binding time.
+///
+public sealed class StructuralErrorDescriptionTests {
+
+ private static readonly BinderErrorDefinition CustomRequired =
+ RequestBindingError.DefaultArgumentRequired
+ .WithCode(ErrorCode.Create("ACME_ARGUMENT_REQUIRED"))
+ .WithMessage(argumentPath => new BindingMessage("Champ obligatoire.", $"Le champ '{argumentPath}' est obligatoire."));
+
+ private static readonly BinderErrorDefinition CustomInvalid =
+ RequestBindingError.DefaultArgumentInvalid
+ .WithCode(ErrorCode.Create("ACME_ARGUMENT_INVALID"))
+ .WithMessage(argumentPath => new BindingMessage("Valeur invalide.", $"Le champ '{argumentPath}' est invalide."));
+
+ // ── Samples carry the definition's code + messages, and the binder's structural shape ─────────────────
+
+ [Fact(DisplayName = "SampleArgumentRequired builds an error with the definition's custom code and messages, under the argument-path context.")]
+ public void SampleRequiredCarriesTheDefinition() {
+ PrimaryPortError error = RequestBindingError.SampleArgumentRequired(CustomRequired);
+
+ Check.That(error.Code.ToString()).IsEqualTo("ACME_ARGUMENT_REQUIRED");
+ Check.That(error.ShortMessage).IsEqualTo("Champ obligatoire.");
+ Check.That(error.DetailedMessage).IsEqualTo("Le champ 'Guests[1].FirstName' est obligatoire.");
+ Check.That(BindingAssertions.ArgumentPathOf(error)).IsEqualTo("Guests[1].FirstName");
+ }
+
+ [Fact(DisplayName = "SampleArgumentInvalid builds an error with the definition's custom code and messages, and still wraps a sample cause.")]
+ public void SampleInvalidCarriesTheDefinitionAndCause() {
+ PrimaryPortError error = RequestBindingError.SampleArgumentInvalid(CustomInvalid);
+
+ Check.That(error.Code.ToString()).IsEqualTo("ACME_ARGUMENT_INVALID");
+ Check.That(error.ShortMessage).IsEqualTo("Valeur invalide.");
+ Check.That(error.InnerErrors).Not.IsEmpty();
+ }
+
+ // ── Describe reuses the binder's generic prose, with the definition's example ──────────────────────────
+
+ [Fact(DisplayName = "DescribeArgumentRequired reuses the binder's title and diagnoses; its example carries the definition's messages.")]
+ public void DescribeRequiredReusesProseWithCustomExample() {
+ ErrorDocumentation doc = RequestBindingError.DescribeArgumentRequired(CustomRequired);
+
+ Check.That(doc.Title).IsEqualTo("Required request argument missing"); // binder's generic prose
+ Check.That(doc.Diagnostics.Select(d => d.Origin)).ContainsExactly(ErrorOrigin.External, ErrorOrigin.Internal);
+ Check.That(doc.Examples.Single().ShortMessage).IsEqualTo("Champ obligatoire."); // consumer's message
+ Check.That(doc.Examples.Single().DetailedMessage).IsEqualTo("Le champ 'Guests[1].FirstName' est obligatoire.");
+ }
+
+ [Fact(DisplayName = "DescribeArgumentInvalid reuses the binder's title and diagnoses; its example carries the definition's messages.")]
+ public void DescribeInvalidReusesProseWithCustomExample() {
+ ErrorDocumentation doc = RequestBindingError.DescribeArgumentInvalid(CustomInvalid);
+
+ Check.That(doc.Title).IsEqualTo("Request argument invalid");
+ Check.That(doc.Diagnostics.Select(d => d.Origin)).ContainsExactly(ErrorOrigin.External, ErrorOrigin.Internal);
+ Check.That(doc.Examples.Single().ShortMessage).IsEqualTo("Valeur invalide.");
+ }
+
+ // ── The default-catalog path is unchanged (the parameterless documentation delegates here) ────────────
+
+ [Fact(DisplayName = "The seams with the default definition still yield the shipped default codes and messages.")]
+ public void DefaultDefinitionYieldsTheShippedCatalog() {
+ Check.That(RequestBindingError.SampleArgumentRequired(RequestBindingError.DefaultArgumentRequired).Code.ToString())
+ .IsEqualTo("REQUEST_ARGUMENT_REQUIRED");
+ Check.That(RequestBindingError.DescribeArgumentRequired(RequestBindingError.DefaultArgumentRequired).Examples.Single().ShortMessage)
+ .IsEqualTo("A required argument is missing.");
+ }
+
+}
diff --git a/FirstClassErrors.RequestBinder/RequestBindingError.cs b/FirstClassErrors.RequestBinder/RequestBindingError.cs
index 0e97ad7c..70e4030a 100644
--- a/FirstClassErrors.RequestBinder/RequestBindingError.cs
+++ b/FirstClassErrors.RequestBinder/RequestBindingError.cs
@@ -1,3 +1,9 @@
+#region Usings declarations
+
+using System.Diagnostics.CodeAnalysis;
+
+#endregion
+
namespace FirstClassErrors.RequestBinder;
///
@@ -117,7 +123,30 @@ internal static PrimaryPortError ArgumentInvalid(BinderErrorDefinition definitio
.WithPublicMessage(message.ShortMessage, message.DetailedMessage);
}
- private static ErrorDocumentation ArgumentRequiredDocumentation() {
+ ///
+ /// Builds a representative missing-required-argument error (REQUEST_ARGUMENT_REQUIRED by default) from
+ /// , for documentation. A consumer that overrides the definition through
+ /// uses this to render, in its own catalog, the structural
+ /// error it actually emits — built the same way the binder builds it at binding time, so the documented example
+ /// stays faithful to runtime.
+ ///
+ /// The definition to render — typically the one injected into .
+ /// A representative error carrying the definition's code and public messages.
+ [SuppressMessage("FirstClassErrors.DocumentationWiring", "FCE009:ErrorFactoryNotDocumented",
+ Justification = "Not a catalog entry: a public sample builder a consumer calls to document, in its own catalog, the structural error it emits after overriding the definition. The binder's own documented factory is ArgumentRequired.")]
+ public static PrimaryPortError SampleArgumentRequired(BinderErrorDefinition definition) {
+ return ArgumentRequired(definition, "Guests[1].FirstName");
+ }
+
+ ///
+ /// The full documentation of the missing-required-argument failure for — the
+ /// binder's own generic prose (title, rule, diagnoses), with a live example built from the definition. A consumer
+ /// that overrides the definition surfaces its effective structural error in its own catalog by returning this from
+ /// a [DocumentedBy] documentation method (see the RequestBinder guide).
+ ///
+ /// The definition to document — typically the one injected into .
+ /// The error documentation, ready to return from a documentation method.
+ public static ErrorDocumentation DescribeArgumentRequired(BinderErrorDefinition definition) {
return DescribeError.WithTitle("Required request argument missing")
.WithDescription("An incoming request omits an argument that the bound command requires. The full path of the missing argument is carried in the error context.")
.WithRule("Every argument bound with AsRequired must be present in the request.")
@@ -127,10 +156,29 @@ private static ErrorDocumentation ArgumentRequiredDocumentation() {
.AndDiagnostic("The argument name reported in the path does not match the wire format (the binder uses the C# property name unless an IArgumentNameProvider is configured).",
ErrorOrigin.Internal,
"Configure an IArgumentNameProvider aligned with the serializer naming policy.")
- .WithExamples(() => ArgumentRequired(DefaultArgumentRequired, "Guests[1].FirstName"));
+ .WithExamples(() => SampleArgumentRequired(definition));
}
- private static ErrorDocumentation ArgumentInvalidDocumentation() {
+ ///
+ /// Builds a representative present-but-invalid-argument error (REQUEST_ARGUMENT_INVALID by default) from
+ /// , for documentation — see . A sample
+ /// converter cause is attached, exactly as the binder attaches the real converter's error at binding time.
+ ///
+ /// The definition to render — typically the one injected into .
+ /// A representative error carrying the definition's code and public messages, wrapping a sample cause.
+ [SuppressMessage("FirstClassErrors.DocumentationWiring", "FCE009:ErrorFactoryNotDocumented",
+ Justification = "Not a catalog entry: a public sample builder a consumer calls to document, in its own catalog, the structural error it emits after overriding the definition. The binder's own documented factory is ArgumentInvalid.")]
+ public static PrimaryPortError SampleArgumentInvalid(BinderErrorDefinition definition) {
+ return ArgumentInvalid(definition, "GuestEmail", SampleCause());
+ }
+
+ ///
+ /// The full documentation of the present-but-invalid-argument failure for — see
+ /// .
+ ///
+ /// The definition to document — typically the one injected into .
+ /// The error documentation, ready to return from a documentation method.
+ public static ErrorDocumentation DescribeArgumentInvalid(BinderErrorDefinition definition) {
return DescribeError.WithTitle("Request argument invalid")
.WithDescription("An incoming request carries an argument that fails to convert into its value object. The full path of the failing argument is carried in the error context, and the precise conversion error is attached as the inner error.")
.WithRule("Every bound argument must convert successfully into its target value object.")
@@ -140,7 +188,15 @@ private static ErrorDocumentation ArgumentInvalidDocumentation() {
.AndDiagnostic("The converter rejects values the contract intends to accept (over-strict parsing rule).",
ErrorOrigin.Internal,
"Review the value object's parsing rule against the API contract.")
- .WithExamples(() => ArgumentInvalid(DefaultArgumentInvalid, "GuestEmail", SampleCause()));
+ .WithExamples(() => SampleArgumentInvalid(definition));
+ }
+
+ private static ErrorDocumentation ArgumentRequiredDocumentation() {
+ return DescribeArgumentRequired(DefaultArgumentRequired);
+ }
+
+ private static ErrorDocumentation ArgumentInvalidDocumentation() {
+ return DescribeArgumentInvalid(DefaultArgumentInvalid);
}
/// A representative converter failure used only by the documentation example above.
diff --git a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md
new file mode 100644
index 00000000..462f0125
--- /dev/null
+++ b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md
@@ -0,0 +1,132 @@
+# ADR-0019 | Documenter les erreurs de binder surchargées dans le catalogue du consommateur
+
+🌍 🇬🇧 [English](0019-document-overridden-binder-errors-in-the-consumers-catalog.md) · 🇫🇷 Français (ce fichier)
+
+**Statut :** Accepté
+**Date :** 2026-07-18
+**Décideurs :** Reefact
+
+## Contexte
+
+* L’ADR-0018 (qui a remplacé l’ADR-0016) a rendu les deux erreurs structurelles du binder
+ configurables comme un `BinderErrorDefinition` — code et messages publics ensemble — sur
+ `RequestBinderOptions`. Les deux ADR ont différé une question vers l’issue #140 : comment le
+ catalogue généré d’un consommateur fait apparaître les codes du binder — éventuellement
+ surchargés — sans dériver de ce qu’il émet au runtime.
+* Le générateur de documentation documente une erreur à partir d’une fabrique statique portant
+ `[DocumentedBy]` dans un type `[ProvidesErrorsFor]` : il exécute la méthode de documentation
+ nommée (la prose) et l’exemple qu’elle porte (une erreur vivante), en lisant le code, les
+ messages et le contexte sur l’erreur construite. Il découvre ces types en scannant les
+ projets opt-in de la solution ; il ne scanne pas les paquets référencés.
+* Le binder fabrique lui-même ses deux erreurs structurelles — leurs fabriques sont internes —
+ et documente déjà ses propres défauts dans le catalogue de son paquet.
+* Quand un consommateur surcharge une définition, il possède le code et les messages effectifs :
+ ils vivent dans son propre assembly (le `BinderErrorDefinition` injecté dans les options) et
+ sont de la configuration runtime, non découvrable statiquement depuis le binaire du binder.
+* La prose des erreurs structurelles du binder (titre, règle, diagnostics) est indépendante du
+ code : elle décrit le sens de « un argument requis manquait » quel que soit le code qui le
+ porte.
+* Le modèle d’erreurs codées de la bibliothèque documente une erreur là où elle est définie, via
+ `[ProvidesErrorsFor]` / `[DocumentedBy]`, et interdit de référencer une erreur par une chaîne
+ magique.
+* Un seul paquet livre aujourd’hui des codes documentés et émissibles
+ (`FirstClassErrors.RequestBinder`), et la bibliothèque est en pré-version sans consommateur
+ externe.
+
+## Décision
+
+Un consommateur fait apparaître ses erreurs structurelles de binder surchargées dans son propre
+catalogue généré en les documentant dans son propre type `[ProvidesErrorsFor]` — bâti à partir
+de seams publics du binder qui réutilisent la prose indépendante du code et construisent un
+exemple fidèle depuis la définition du consommateur — plutôt que par une découverte automatique
+des codes d’un paquet référencé par le générateur.
+
+## Justification
+
+* Le consommateur possède les codes effectifs (ADR-0018) : les documenter dans son propre
+ catalogue — là où le modèle d’erreurs codées documente toute autre erreur possédée — garde une
+ règle unique plutôt qu’un chemin cross-paquet spécial.
+* Réutiliser la prose indépendante du code via des seams publics fait que le consommateur n’écrit
+ aucun texte de description et ne construit aucune erreur à la main : les faits mécaniques (code,
+ messages, transience, clé de contexte du chemin d’argument, câblage de l’erreur interne)
+ viennent du binder qui bâtit l’exemple comme à la liaison, si bien que l’entrée documentée ne
+ peut pas dériver de ce qui est émis — fermant le risque même que #140 nommait.
+* Le générateur n’a pas besoin de changer : le catalogue du consommateur est un type
+ `[ProvidesErrorsFor]` / `[DocumentedBy]` ordinaire dans un projet déjà scanné. Cela évite la
+ machinerie qu’exigerait une étape de découverte automatique — résoudre la clôture de références
+ d’un projet, un pré-check métadonnée pour écarter les assemblys non documentants, exécuter le
+ worker sur des binaires tiers, une garde de version de contrat de documentation et une
+ politique de collision de codes — et ses pièges de justesse, puisqu’un paquet référencé n’est
+ pas toujours émis sur la surface publique et que ses défauts sont faux pour un consommateur qui
+ les a surchargés.
+* Garder le lien entre une erreur et sa description dans le code, via `nameof` / `typeof`, le
+ laisse sous l’œil du compilateur — cohérent avec la position de la bibliothèque contre les
+ chaînes magiques — là où un lien exprimé en configuration de build les réintroduirait.
+* Avec un seul paquet à codes documentés et aucun consommateur, la petite glue par consommateur
+ est un prix acceptable, et la surface des options comme les seams de documentation sont arrêtés
+ avant de s’engager sur une découverte automatique.
+
+## Alternatives considérées
+
+### Découvrir automatiquement les codes documentés d’un paquet référencé
+
+Considérée parce qu’elle est sans boilerplate : un consommateur référence le paquet et ses codes
+apparaissent dans son catalogue.
+
+Rejetée parce qu’elle est inutile et trompeuse. Le seul paquet qui livre des codes émissibles est
+`FirstClassErrors.RequestBinder`, dont le binaire ne porte que les défauts — l’auto-découverte ne
+pourrait donc jamais faire apparaître les surcharges d’un consommateur (ce que font ces seams), et
+pour un consommateur qui surcharge elle documenterait les codes par défaut qu’il n’émet plus. La
+restreindre à ce seul paquet n’y change rien : la limite est que les surcharges vivent dans
+l’assembly du consommateur, pas dans celui du paquet. Elle n’économiserait qu’une petite glue au
+consommateur zéro-config qui documente les défauts.
+
+### Un lien en configuration de build reliant une erreur à une méthode de description
+
+Considérée parce qu’elle sort le câblage de documentation du code vers la configuration du projet.
+
+Rejetée parce qu’elle référencerait des membres par chaîne dans les fichiers projet —
+réintroduisant les chaînes magiques que le modèle d’erreurs codées existe pour éliminer, sans
+contrôle à la compilation, ni navigation, ni sûreté au renommage. Un attribut d’assembly
+compile-safe serait le repli si un lien déclaratif était un jour souhaité.
+
+## Conséquences
+
+### Positives
+
+* La moitié « surcharge » de #140 est fermée : un consommateur documente exactement ce qu’il émet,
+ fidèle au runtime, avec la prose du binder et sans changer le générateur.
+* Le lien entre une erreur et sa documentation reste compile-safe et dans le code, cohérent avec
+ la position de la bibliothèque sur les chaînes magiques.
+
+### Négatives
+
+* Un consommateur fait entrer les codes du binder dans son catalogue avec un petit type de glue
+ par erreur (un type `[ProvidesErrorsFor]` déléguant aux seams publics) ; il n’y a pas de
+ découverte automatique sans boilerplate.
+* Le binder gagne quatre membres publics (un seam de description et un d’exemple par erreur
+ structurelle). Les deux seams d’exemple renvoient une erreur sans `[DocumentedBy]` et sont
+ supprimés contre FCE009 comme helpers délibérément hors catalogue.
+
+### Risques
+
+* Un consommateur pourrait appeler un seam d’exemple pour fabriquer une erreur structurelle hors
+ documentation. Il produit la même forme que ce que le binder émet et n’est injecté nulle part :
+ il est donc inerte — pas différent de bâtir n’importe quelle erreur via le `PrimaryPortError.Create`
+ public.
+
+## Actions de suivi
+
+* Aucune. L’auto-découverte n’est pas nécessaire (voir Alternatives) : le seul paquet à codes
+ émissibles est couvert par ces seams.
+
+## Références
+
+* ADR-0018 — regrouper le code et les messages d’une erreur structurelle du binder dans une seule
+ définition ; l’appropriation sur laquelle cette décision s’appuie.
+* ADR-0016 — rendre configurables les codes d’erreur structurels du binder (remplacée) ; elle a
+ d’abord différé ce sujet vers #140.
+* Issue #140 — documenter les codes d’erreur des paquets FirstClassErrors référencés dans le
+ catalogue d’un consommateur.
+* FCE009 — `ErrorFactoryNotDocumented`, l’analyseur contre lequel les seams d’exemple sont
+ supprimés.
diff --git a/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md
new file mode 100644
index 00000000..f3ced31f
--- /dev/null
+++ b/doc/handwritten/for-maintainers/adr/0019-document-overridden-binder-errors-in-the-consumers-catalog.md
@@ -0,0 +1,125 @@
+# ADR-0019 | Document overridden binder errors in the consumer's own catalog
+
+🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0019-document-overridden-binder-errors-in-the-consumers-catalog.fr.md)
+
+**Status:** Accepted
+**Date:** 2026-07-18
+**Decision Makers:** Reefact
+
+## Context
+
+* ADR-0018 (which superseded ADR-0016) made the binder's two structural errors configurable
+ as a `BinderErrorDefinition` — code and public messages together — on
+ `RequestBinderOptions`. Both ADRs deferred one question to issue #140: how a consumer's
+ generated catalog surfaces the binder's — possibly overridden — codes without drifting
+ from what it emits at runtime.
+* The documentation generator documents an error from a static factory carrying
+ `[DocumentedBy]` inside a `[ProvidesErrorsFor]` type: it runs the named documentation
+ method (the prose) and the example it holds (a live error), reading the code, messages and
+ context off the built error. It discovers these types by scanning the solution's opted-in
+ projects; it does not scan referenced packages.
+* The binder manufactures its two structural errors itself — their factories are internal —
+ and already documents its own defaults in its own package catalog.
+* When a consumer overrides a definition, it owns the effective code and messages: they live
+ in the consumer's own assembly (the `BinderErrorDefinition` it injects into the options)
+ and are runtime configuration, not statically discoverable from the binder's binary.
+* The binder's structural-error prose (title, rule, diagnoses) is code-independent: it
+ describes the meaning of "a required argument was missing" whatever code carries it.
+* The library's coded-error model documents an error where it is defined, through
+ `[ProvidesErrorsFor]` / `[DocumentedBy]`, and forbids referencing an error by a magic
+ string.
+* Exactly one package ships documented, emittable codes today (`FirstClassErrors.RequestBinder`),
+ and the library is pre-release with no external consumers.
+
+## Decision
+
+A consumer surfaces its overridden binder structural errors in its own generated catalog by
+documenting them in its own `[ProvidesErrorsFor]` type — built from public binder seams that
+reuse the binder's code-independent prose and build a faithful example from the consumer's
+definition — rather than the generator auto-discovering a referenced package's codes.
+
+## Rationale
+
+* The consumer owns the effective codes (ADR-0018), so documenting them in its own catalog —
+ where the coded-error model documents every other owned error — keeps one consistent rule
+ instead of a special cross-package path.
+* Reusing the binder's code-independent prose through public seams means the consumer writes
+ no description text and constructs no error by hand: the mechanical facts (code, messages,
+ transience, the argument-path context key, inner-error wiring) come from the binder building
+ the sample the same way it does at binding time, so the documented entry cannot drift from
+ what is emitted — closing the exact risk #140 named.
+* The generator needs no change: the consumer's catalog is an ordinary
+ `[ProvidesErrorsFor]` / `[DocumentedBy]` type in an already-scanned project. This avoids the
+ machinery an auto-discovery step would need — resolving a project's reference closure, a
+ metadata pre-check to skip non-documenting assemblies, running the worker over third-party
+ binaries, a documentation-contract-version gate, and a code-collision policy — and its
+ correctness hazards, since a referenced package is not always emitted on the public surface
+ and its defaults are wrong for a consumer that overrode them.
+* Keeping the link between an error and its description in code, through `nameof` / `typeof`,
+ leaves it under the compiler — consistent with the library's stance against magic strings —
+ where a link expressed in build configuration would reintroduce them.
+* With one documented-code package and no consumers, the small per-consumer glue is an
+ acceptable price, and the options surface and documentation seams are settled before any
+ auto-discovery is committed to.
+
+## Alternatives Considered
+
+### Auto-discover a referenced package's documented codes
+
+Considered because it is zero-boilerplate: a consumer references the package and its codes
+appear in the consumer's catalog.
+
+Rejected because it is unnecessary and misleading. The only package that ships emittable codes
+is `FirstClassErrors.RequestBinder`, whose binary carries only the defaults — so auto-discovery
+could never surface a consumer's overrides (these seams do), and for an overriding consumer it
+would document the default codes it no longer emits. Scoping it to that one package removes none
+of this: the limitation is that overrides live in the consumer's assembly, not the package's. It
+would buy only sparing a zero-config consumer the small glue that documents the defaults.
+
+### A build-configuration link binding an error to a description method
+
+Considered because it moves the documentation wiring out of code into project configuration.
+
+Rejected because it would reference members by string in project files — reintroducing the
+magic strings the coded-error model exists to eliminate, with no compile-time check,
+navigation, or refactoring safety. A compile-safe assembly attribute would be the fallback if
+a declarative link is ever wanted.
+
+## Consequences
+
+### Positive
+
+* The override half of #140 is closed: a consumer documents exactly what it emits, faithful to
+ runtime, with the binder's prose and no change to the generator.
+* The link between an error and its documentation stays compile-safe and in code, consistent
+ with the library's magic-string stance.
+
+### Negative
+
+* A consumer folds the binder's codes into its catalog with a small per-error glue type (a
+ `[ProvidesErrorsFor]` type delegating to the public seams); there is no zero-boilerplate
+ auto-discovery.
+* The binder grows four public members (a describe and a sample seam per structural error).
+ The two sample seams return an error without `[DocumentedBy]` and are suppressed against
+ FCE009 as deliberate non-catalog helpers.
+
+### Risks
+
+* A consumer could call a sample seam to manufacture a structural error outside documentation.
+ It produces the same shape the binder emits and is injected nowhere, so it is inert — no
+ different from building any error through the public `PrimaryPortError.Create`.
+
+## Follow-up Actions
+
+* None. Auto-discovery is not needed (see Alternatives): the one package that ships emittable
+ codes is covered by these seams.
+
+## References
+
+* ADR-0018 — bundle the binder's structural error code and messages in one definition; the
+ ownership this decision builds on.
+* ADR-0016 — make the binder's structural error codes configurable (superseded); it first
+ deferred this concern to #140.
+* Issue #140 — document error codes from referenced FirstClassErrors packages in a consumer's
+ catalog.
+* FCE009 — `ErrorFactoryNotDocumented`, the analyzer the sample seams are suppressed against.
diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md
index 25a48e56..e19c71c3 100644
--- a/doc/handwritten/for-maintainers/adr/README.md
+++ b/doc/handwritten/for-maintainers/adr/README.md
@@ -192,3 +192,4 @@ Optional supporting material:
| [ADR-0016](0016-make-the-binders-structural-error-codes-configurable.md) | Make the binder's structural error codes configurable | Superseded |
| [ADR-0017](0017-provide-a-configurable-application-wide-default-for-the-binder-options.md) | Provide a configurable application-wide default for the binder options | Accepted |
| [ADR-0018](0018-bundle-the-binders-structural-error-code-and-messages.md) | Bundle the binder's structural error code and messages in one definition | Accepted |
+| [ADR-0019](0019-document-overridden-binder-errors-in-the-consumers-catalog.md) | Document overridden binder errors in the consumer's own catalog | Accepted |
diff --git a/doc/handwritten/for-users/RequestBinder.en.md b/doc/handwritten/for-users/RequestBinder.en.md
index ed2124c5..ee544ce8 100644
--- a/doc/handwritten/for-users/RequestBinder.en.md
+++ b/doc/handwritten/for-users/RequestBinder.en.md
@@ -450,6 +450,37 @@ or, when you keep the defaults, the ones the binder exposes.
if (error.Code == RequestBindingError.DefaultArgumentRequiredCode) { return 422; }
```
+### Documenting your overridden errors in your own catalog
+
+When you override the codes or messages and you generate an error catalog with
+`fce generate`, document the errors where you now own them — in **your** catalog, reusing
+the binder's own prose. The public seams `DescribeArgumentRequired` /
+`DescribeArgumentInvalid` return the binder's generic description with a live example built
+from your definition; `SampleArgumentRequired` / `SampleArgumentInvalid` return the example
+error itself, for the `[DocumentedBy]` factory:
+
+```csharp
+[ProvidesErrorsFor("MyApi")]
+public static class MyApiBinderErrors {
+
+ // The definition you inject into RequestBinderOptions — the single source of truth.
+ public static readonly BinderErrorDefinition ArgumentRequired =
+ RequestBindingError.DefaultArgumentRequired.WithCode(ErrorCode.Create("ACME_ARGUMENT_REQUIRED"));
+
+ [DocumentedBy(nameof(ArgumentRequiredDoc))]
+ internal static PrimaryPortError ArgumentRequiredError() => RequestBindingError.SampleArgumentRequired(ArgumentRequired);
+ private static ErrorDocumentation ArgumentRequiredDoc() => RequestBindingError.DescribeArgumentRequired(ArgumentRequired);
+
+ // ArgumentInvalid follows the same shape, with DescribeArgumentInvalid / SampleArgumentInvalid.
+}
+```
+
+The generator discovers this like any other catalog: the prose is the binder's, the code
+and messages are yours, and the example is built the way the binder builds it at binding
+time — so the documented entry matches what you actually emit. The same pattern folds the
+**default** codes into your catalog too — point the definition at
+`RequestBindingError.DefaultArgumentRequired` / `DefaultArgumentInvalid`.
+
## Configuring the default for the whole application
`Bind.PropertiesOf(request)` binds with `RequestBinderOptions.Default`. That default is
diff --git a/doc/handwritten/for-users/RequestBinder.fr.md b/doc/handwritten/for-users/RequestBinder.fr.md
index c95fd819..7e1deb2c 100644
--- a/doc/handwritten/for-users/RequestBinder.fr.md
+++ b/doc/handwritten/for-users/RequestBinder.fr.md
@@ -467,6 +467,38 @@ expose.
if (error.Code == RequestBindingError.DefaultArgumentRequiredCode) { return 422; }
```
+### Documenter vos erreurs surchargées dans votre propre catalogue
+
+Quand vous surchargez les codes ou les messages et que vous générez un catalogue d’erreurs
+avec `fce generate`, documentez les erreurs là où vous en êtes désormais propriétaire —
+dans **votre** catalogue, en réutilisant la prose du binder. Les seams publics
+`DescribeArgumentRequired` / `DescribeArgumentInvalid` renvoient la description générique du
+binder avec un exemple vivant bâti depuis votre définition ; `SampleArgumentRequired` /
+`SampleArgumentInvalid` renvoient l’erreur d’exemple elle-même, pour la fabrique
+`[DocumentedBy]` :
+
+```csharp
+[ProvidesErrorsFor("MyApi")]
+public static class MyApiBinderErrors {
+
+ // La définition injectée dans RequestBinderOptions — la source unique de vérité.
+ public static readonly BinderErrorDefinition ArgumentRequired =
+ RequestBindingError.DefaultArgumentRequired.WithCode(ErrorCode.Create("ACME_ARGUMENT_REQUIRED"));
+
+ [DocumentedBy(nameof(ArgumentRequiredDoc))]
+ internal static PrimaryPortError ArgumentRequiredError() => RequestBindingError.SampleArgumentRequired(ArgumentRequired);
+ private static ErrorDocumentation ArgumentRequiredDoc() => RequestBindingError.DescribeArgumentRequired(ArgumentRequired);
+
+ // ArgumentInvalid suit la même forme, avec DescribeArgumentInvalid / SampleArgumentInvalid.
+}
+```
+
+Le générateur découvre ceci comme n’importe quel autre catalogue : la prose est celle du
+binder, le code et les messages sont les vôtres, et l’exemple est bâti comme le binder le
+bâtit à la liaison — l’entrée documentée correspond donc à ce que vous émettez réellement.
+Le même patron fait aussi apparaître les codes **par défaut** dans votre catalogue — pointez
+la définition sur `RequestBindingError.DefaultArgumentRequired` / `DefaultArgumentInvalid`.
+
## Configurer le défaut pour toute l’application
`Bind.PropertiesOf(request)` lie avec `RequestBinderOptions.Default`. Ce défaut est