diff --git a/README.md b/README.md index 9eeb9f1..b075b80 100644 --- a/README.md +++ b/README.md @@ -5,11 +5,11 @@ | | | | :-- | :-- | -| **Build** | [![ci](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml) | -| **Quality** | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=coverage)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) | +| **Build** | [![ci](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml) | +| **Quality** | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=coverage)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) | | **Security** | [![codeql](https://github.com/Reefact/first-class-errors/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/codeql.yml) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13567/badge)](https://www.bestpractices.dev/projects/13567) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Reefact/first-class-errors/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Reefact/first-class-errors) | -| **Package** | [![NuGet](https://img.shields.io/nuget/vpre/FirstClassErrors?logo=nuget)](https://www.nuget.org/packages/FirstClassErrors) ![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4) | -| **Project** | [![License](https://img.shields.io/github/license/Reefact/first-class-errors)](LICENSE) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org) | +| **Package** | [![NuGet](https://img.shields.io/nuget/vpre/FirstClassErrors?logo=nuget)](https://www.nuget.org/packages/FirstClassErrors) ![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4) | +| **Project** | [![License](https://img.shields.io/github/license/Reefact/first-class-errors)](LICENSE) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org) | --- @@ -17,245 +17,166 @@ ![FirstClassErrors](./doc/images/first-class-errors.png "FirstClassErrors") -FirstClassErrors is a .NET library that treats errors as first-class, documented, and diagnosable concepts — not just strings thrown at runtime. +## đŸ§© One model for the whole chain -It helps you: +An application error too often boils down to a bare code or string, with nothing that explains what it actually means. FirstClassErrors turns it into a first-class concept of your domain. You describe each error situation once, with its rules, in one place in the code; the whole library then builds on that model. -* express errors in a consistent and structured way -* attach meaningful diagnostics to each error -* keep error documentation close to the code -* generate human-readable error documentation automatically +**Model the error** +An error becomes an object that carries its own meaning — a violated business rule, a rejected incoming request, or a failing external service, transient or not. -## 🚹 The problem - -In most systems, errors are: - -* scattered across the codebase -* described by ad-hoc messages -* poorly documented -* hard to troubleshoot -* disconnected from support and operations - -Over time, this leads to: - -* duplicated investigations -* tribal knowledge -* support teams guessing -* developers reinventing error explanations - -## 💡 The idea - -What if: - -> **Every error in your system was explicitly described, structured, and documented — directly in code — and that documentation could be generated automatically?** - -FirstClassErrors introduces: - -* a **rich error model** -* a **structured diagnostic system** -* a **DSL to document errors** -* a **documentation extraction pipeline** - -Errors become: - -> not just failures, -> but **documented knowledge units**. - -## đŸ§± What this library provides - -### 1ïžâƒŁ A richer error model - -The `Error` carries: +**Handle the failure** +You choose how it travels: thrown as an ordinary exception, or returned as an explicit result the caller inspects without a try/catch. -* a stable error code -* a timestamp -* three distinct messages — a mandatory public short summary, an optional public detail, and a mandatory internal diagnostic message -* contextual data -* structured diagnostics +**Document the error** +Each error can carry its explanation, its rule, and its likely causes right there in the code. FirstClassErrors generates a human-readable catalog from that same code — so it never drifts. -The exception itself only exposes its `.Error`, and is designed to be: +**Check the model** +Roslyn analyzers enforce these rules at compile time: a duplicated error code or inconsistent documentation becomes a build error, caught before it ever reaches production. -* logged consistently -* understood by humans -* used by tooling +**Bind external input** +A form or a JSON request must become a valid domain object. Where classic .NET binding restates those rules separately, in a validator or attributes, FirstClassErrors binds that input directly to your value objects, reusing their own construction rules instead of duplicating them. -#### Three messages, two audiences - -An `Error` carries three messages but deliberately splits them across just **two audiences** — a public one (end users / API clients) and an internal one (logs, support, developers). The separation is enforced by construction, so what reaches a caller can never leak what is meant for developers and support: - -| Message | Mandatory | Audience | Exposure | -| --- | --- | --- | --- | -| `ShortMessage` | Yes | End users / API clients | A short public summary, safe to surface as-is (e.g. the `title` of an RFC 9457 problem detail). | -| `DetailedMessage` | No | End users / API clients | A controlled public detail (e.g. the `detail` of an RFC 9457 problem detail), exposed **only** when the application explicitly chooses to. Must not carry sensitive or internal information. | -| `DiagnosticMessage` | Yes | Logs, support, developers | The internal diagnostic message. May contain technical/operational detail (identifiers, offending values, internal state); it is **never** exposed to external clients by default. | - -The core model is HTTP-agnostic: `DiagnosticMessage` is never used as a default HTTP response body, and `type` and `status` remain the application's concern. When an error is exposed over HTTP, its `Code` is the natural RFC 9457 `type`: surface it as a stable URI such as `urn:problem:{service}:{code}`, where `{code}` is the error code in lowercase kebab-case — for example `urn:problem:temperature-simulator:temperature-below-absolute-zero` or `urn:problem:banking-api:money-transfer-amount-not-positive` — so clients can branch on the problem type. When you turn an error into an exception with `.ToException()`, the resulting `Exception.Message` is the `DiagnosticMessage` — the developer- and log-facing text. - -### 2ïžâƒŁ Structured diagnostics - -Each error can declare **possible causes** and **analysis leads**: - -* What might have caused this error? -* Is it likely input-related, system-related, or both? -* Where should investigation start? - -Diagnostics guide troubleshooting without hardcoding operational processes. - -### 3ïžâƒŁ A DSL to describe errors +## 🚹 The problem -Errors are documented directly in code using a fluent API: +A production error is rarely useful as only a type and a string: -```csharp -return DescribeError.WithTitle("Temperature below absolute zero") - .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.") - .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.") - .WithDiagnostics(ValueObjectDiagnostic.Diagnostic) - .WithExamples( - () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin), - () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius)); +```text +Invalid operation. ``` -This is not just comments — it is **structured, executable documentation**. - -### 4ïžâƒŁ Documentation extraction - -The library includes a mechanism to scan assemblies and extract all declared error documentation: - -* linked to error factory classes -* linked to factory methods -* enriched with examples -* ready to be rendered - -This enables: - -* Markdown or JSON error catalogs (or any custom format via a renderer) -* support-oriented documentation -* living documentation generated from code -* multi-language catalogs (opt-in) — see [Internationalization](doc/Internationalization.en.md) +Developers and support still need to discover: -## 🔁 Exception or not? You choose. +- which situation actually occurred; +- which rule was violated; +- which facts belong to this occurrence; +- what might have caused it; +- where to start investigating. -The library supports both: +When that knowledge lives in logs, tickets, comments, and people's memories, it drifts away from the code. -* **throwing errors** (traditional exception flow) -* **transporting errors without throwing** via `Outcome` and `Outcome` +## 💡 The FirstClassErrors approach -This lets you treat the same error either way: - -> as a runtime signal you throw -> or as structured data you transport - -depending on the context (domain logic, validation, pipelines, etc.). - -## 📩 Installation - -```bash -dotnet add package FirstClassErrors -``` - -Targets **.NET Standard 2.0**. The Roslyn analyzers are bundled in the package — no separate install. - -## đŸ§© Example - -From the `FirstClassErrors.Usage` project: +A factory gives the error situation a stable identity, keeps its construction in one place, and attaches structured documentation that will feed a generated, human-readable catalog: ```csharp -[ProvidesErrorsFor(nameof(Temperature))] -public static class InvalidTemperatureError { +[ProvidesErrorsFor(nameof(Amount))] +public static class InvalidAmountOperationError { - [DocumentedBy(nameof(BelowAbsoluteZeroDocumentation))] - internal static DomainError BelowAbsoluteZero(decimal invalidValue, TemperatureUnit invalidValueUnit) { + [DocumentedBy(nameof(CurrencyMismatchDocumentation))] + internal static DomainError CurrencyMismatch(Amount left, Amount right) { return DomainError.Create( - Code.TemperatureBelowAbsoluteZero, - diagnosticMessage: $"Failed to instantiate temperature: the value {invalidValue} {invalidValueUnit} is below absolute zero.") + ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"), + diagnosticMessage: $"Cannot add {left} and {right} because their currencies differ.") .WithPublicMessage( - shortMessage: "Temperature is invalid.", - detailedMessage: $"The temperature {invalidValue} {invalidValueUnit} is below absolute zero."); + shortMessage: "The amounts use different currencies.", + detailedMessage: "Both amounts must use the same currency."); } - private static ErrorDocumentation BelowAbsoluteZeroDocumentation() { - return DescribeError.WithTitle("Temperature below absolute zero") - .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.") - .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.") - .WithDiagnostics(ValueObjectDiagnostic.Diagnostic) - .WithExamples( - () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin), - () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius)); + private static ErrorDocumentation CurrencyMismatchDocumentation() { + return DescribeError.WithTitle("Amount currency mismatch") + .WithDescription("This error occurs when an operation combines amounts expressed in different currencies.") + .WithRule("A monetary operation must use one common currency.") + .WithDiagnostic( + "The amounts reached the operation without being converted to one currency.", + ErrorOrigin.Internal, + "Verify where the amounts should have been converted before this operation.") + .WithExamples(() => CurrencyMismatch( + new Amount(10, Currency.EUR), + new Amount(12, Currency.USD))); } +} +``` - private static class Code { - public static readonly ErrorCode TemperatureBelowAbsoluteZero = ErrorCode.Create("TEMPERATURE_BELOW_ABSOLUTE_ZERO"); - } +The domain code stays focused on intent: + +```csharp +if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } ``` -The factory returns a structured `Error`. When you need to throw it, you turn it into an exception with `.ToException()`: +The factory returns an `Error` (`DomainError` is one of its categories), so expected failures can use the same model without throwing: ```csharp -throw InvalidTemperatureError.BelowAbsoluteZero(-1, TemperatureUnit.Kelvin).ToException(); +return Outcome.Failure( + InvalidAmountOperationError.CurrencyMismatch(left, right)); ``` -Here, the error, its meaning, its rule, its diagnostics, and example messages are all defined together — in code. +From these factories, FirstClassErrors can generate a human-readable catalog for developers, support teams, and operations. -## 🎯 Who is this for? +## 📩 Installation -FirstClassErrors is especially useful if: +```bash +dotnet add package FirstClassErrors +``` -* you build complex business systems -* you care about supportability -* you want consistent error handling -* you want documentation that doesn’t drift from code -* you design with domain-driven thinking +The package targets **.NET Standard 2.0**. Its Roslyn analyzers are bundled automatically; no separate analyzer package is required. -## 🔍 Analyzers +To generate documentation, install the CLI: -FirstClassErrors ships with a set of Roslyn analyzers (rule ids `FCExxx`) **bundled in the NuGet package** — reference the package and they run at build time, no extra install. They catch, before you run anything, the mistakes the runtime or the documentation pipeline would otherwise surface late or silently: duplicate error codes, `[DocumentedBy]` references that don't resolve, documented errors that never reach the catalog, and more. +```bash +dotnet tool install --global FirstClassErrors.Cli +``` -> **Compiler requirement:** the bundled analyzers are compiled against Roslyn 4.8, so they load in any host from **.NET 8 SDK / Visual Studio 2022 17.8** onward. Older SDKs/IDEs cannot load them. +Then follow the [Getting Started guide](doc/GettingStarted.en.md) to create and generate your first documented error. -See the [analyzer rules reference](doc/analyzers/README.md). +## 🎯 When it is a good fit -## 🔐 Supply chain +FirstClassErrors is especially useful for long-lived application or domain code where: -Every released package is built and published by [`release.yml`](.github/workflows/release.yml) with two verifiable guarantees: +- errors represent rules, constraints, or boundary failures; +- several teams or systems depend on stable error codes; +- support and operations investigate production failures; +- documentation must remain aligned with behavior. -- **Signed build provenance** ([SLSA](https://slsa.dev)) — each package is attested at build time, binding its checksum to this repository, the exact commit and the workflow run. The attested bytes are attached to the matching [GitHub Release](https://github.com/Reefact/first-class-errors/releases); download the `.nupkg` from there and verify with: +For prototypes, tiny utilities, or low-level technical code, standard exceptions may be enough. See [When Not to Use FirstClassErrors](doc/WhenNotToUseFirstClassErrors.en.md). - ```bash - gh attestation verify FirstClassErrors..nupkg --repo Reefact/first-class-errors - ``` +## 🔍 Analyzers and supply-chain information - (nuget.org serves a repository-signed copy with a different checksum, so verify **that** one with `dotnet nuget verify` instead.) +The package includes Roslyn rules with stable `FCExxx` identifiers. They detect duplicate or malformed error codes, invalid documentation wiring, missing examples, and common API misuse. See the [analyzer rules reference](doc/analyzers/README.md). -- **Embedded SBOM** — each package carries its SPDX software bill of materials at `_manifest/spdx_2.2/manifest.spdx.json`, inventorying the files it ships and the third-party components it was built from. +Released packages include signed build provenance and an embedded SPDX SBOM. See the release and verification details in the [supply-chain documentation](SECURITY.md). -## 🐛 Feedback & contributing +## 🐛 Feedback and contributing -Found a bug or want to request a feature? Open an issue on the [GitHub issue tracker](https://github.com/Reefact/first-class-errors/issues). Contributions are welcome — see [CONTRIBUTING.md](CONTRIBUTING.md) to get started. +Found a bug or want to request a feature? Open an issue on the [GitHub issue tracker](https://github.com/Reefact/first-class-errors/issues). Contributions are welcome; see [CONTRIBUTING.md](CONTRIBUTING.md). -For **security** vulnerabilities, please follow the private process in [SECURITY.md](SECURITY.md) instead of opening a public issue. +For security vulnerabilities, follow the private process in [SECURITY.md](SECURITY.md). -## 📚 Next steps +## 📚 Documentation -See the full documentation: +### Discover - [Getting Started](doc/GettingStarted.en.md) - [Design Principles](doc/DesignPrinciples.en.md) - [When Not to Use FirstClassErrors](doc/WhenNotToUseFirstClassErrors.en.md) + +### Understand the model + - [Core Concepts](doc/CoreConcepts.en.md) +- [Error Taxonomy and Composition](doc/ErrorTaxonomy.en.md) - [Error Context Guide](doc/ErrorContext.en.md) + +### Write and use errors + - [Writing Errors Guide](doc/WritingErrorsGuide.en.md) - [Usage Patterns](doc/UsagePatterns.en.md) - [Best Practices](doc/BestPractices.en.md) - [Testing Guide](doc/Testing.en.md) + +### Generate and operate the catalog + - [CI/CD and Operational Integration](doc/OperationalIntegration.en.md) - Catalog versioning - - [Overview & workflow](doc/CatalogVersioning.en.md) + - [Overview and workflow](doc/CatalogVersioning.en.md) - [Command reference](doc/CatalogVersioningReference.en.md) - [CI/CD integration](doc/CatalogVersioningCI.en.md) - [Architecture of the Documentation Pipeline](doc/ArchitectureOfTheDocumentationPipeline.en.md) - [Writing a custom renderer](doc/WritingACustomRenderer.en.md) - [Internationalization](doc/Internationalization.en.md) + +### Evaluate and troubleshoot + - [Comparison with error-handling libraries](doc/ComparisonWithOtherLibraries.en.md) - [Analyzer rules (FCExxx)](doc/analyzers/README.md) - [FAQ](doc/FAQ.en.md) diff --git a/doc/CoreConcepts.en.md b/doc/CoreConcepts.en.md index be9b865..92b3300 100644 --- a/doc/CoreConcepts.en.md +++ b/doc/CoreConcepts.en.md @@ -3,151 +3,155 @@ 🌍 **Languages:** 🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./CoreConcepts.fr.md) -FirstClassErrors is not just a utility library. -It introduces a different way to think about application errors. +FirstClassErrors separates the **meaning of a failure** from the mechanism used to propagate it. -Instead of reducing a failure to a mere technical incident, it turns the **error** behind it into **structured knowledge about what went wrong** — while the exception stays the mechanism that signals and propagates that failure. +The central object is `Error`. An exception or an `Outcome` only transports that error. -## 🧠 An error is not just a message +```mermaid +flowchart LR + A[Error factory] --> B[Error] + B --> C[ToException] + B --> D[Outcome or Outcome of T] + A --> E[DocumentedBy] + E --> F[Generated documentation] +``` -In many systems, exceptions are reduced to: +## An error represents one situation -> a type + a string message +A useful error answers a precise question: -In FirstClassErrors, an **error** represents: +> What situation did the system recognize? -* a **specific error situation** -* identified by a **stable error code** -* described with three purpose-built messages (a public summary, an optional public detail, and an internal diagnostic message) -* optionally enriched with context -* associated with structured diagnostics +For example: -An **error** becomes a semantic object, not just a runtime signal. +```csharp +InvalidAmountOperationError.CurrencyMismatch(left, right) +``` -### Three messages, two audiences +The factory gives the situation a readable name. Its stable code gives the same situation a machine-readable identity: -An error carries three messages but deliberately splits them across just **two audiences** — a public one (end users / API clients) and an internal one (logs, support, developers). The separation is guaranteed by construction, so what reaches a caller can never leak what is meant for developers and support: +```text +AMOUNT_CURRENCY_MISMATCH +``` -* **`ShortMessage`** (mandatory) — a short public summary, safe to expose to an end user or an API client (e.g. the `title` of an RFC 9457 problem detail). -* **`DetailedMessage`** (optional) — a controlled public detail, exposable **only** when the application explicitly chooses to (e.g. the `detail` of an RFC 9457 problem detail). It must never carry sensitive or internal information. -* **`DiagnosticMessage`** (mandatory) — the internal diagnostic message for logs, support and developers. It may contain technical/operational detail (identifiers, offending values, internal state) and is **never** exposed to external clients by default. `error.ToException()` uses it as the exception's `Message`. +One factory should therefore correspond to one documented error case. -The core model stays HTTP-agnostic: the diagnostic message is never a default HTTP response body, and `type` and `status` remain the application's concern. When an error is exposed over HTTP, its `Code` is the natural RFC 9457 `type`: surface it as a stable URI such as `urn:problem:{service}:{code}`, where `{code}` is the error code in lowercase kebab-case — for example `urn:problem:temperature-simulator:temperature-below-absolute-zero` or `urn:problem:banking-api:money-transfer-amount-not-positive` — so clients can branch on the problem type. +## What an `Error` carries -## đŸ§© A factory represents an error situation +An error occurrence — one runtime instance of the situation — contains: -**Error** factories are central to the model. +- a stable `Code`; +- a unique `InstanceId`; +- an `OccurredAt` timestamp; +- public and internal messages; +- optional typed context; +- optional inner errors (nested errors recording the cause). -A factory method: +The factory centralizes how these values are created, so every occurrence of the same situation remains consistent. -* represents one precise error scenario -* gives it a **name** in the code -* centralizes error creation -* becomes the anchor for documentation +## Three messages, two audiences -This means: +An error carries three messages, divided between a public audience and an internal audience. -> Each factory = one documented error case. +| Message | Mandatory | Audience | Purpose | +| --- | --- | --- | --- | +| `ShortMessage` | yes | users and API clients | safe public summary | +| `DetailedMessage` | no | users and API clients | optional controlled detail | +| `DiagnosticMessage` | yes | logs, support, developers | internal investigation detail | -Factories improve readability and make error situations explicit, while keeping construction details out of the business logic. +The separation is deliberate. A diagnostic message may contain identifiers, offending values, or internal state that must never be exposed to an external client by default. -## 📘 Documentation lives with the code +```csharp +return DomainError.Create( + Code.CurrencyMismatch, + diagnosticMessage: $"Cannot add {left} and {right} because their currencies differ.") + .WithPublicMessage( + shortMessage: "The amounts use different currencies.", + detailedMessage: "Both amounts must use the same currency."); +``` -Error documentation is written using the `DescribeError` DSL and linked directly to **error** factories. +Calling `error.ToException()` uses the diagnostic message as the exception's `Message`. How public messages are mapped to HTTP, gRPC, a UI, or another transport remains the application's responsibility. -This creates: +## A factory is the source of truth -* structured descriptions -* violated rules -* diagnostics -* realistic examples +Factories keep construction details out of business logic: -Because documentation is code: +```csharp +if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); +} +``` -* it evolves with the system -* it does not drift -* it can be extracted automatically +The code states the recognized situation without repeating its code, messages, context, or construction rules. -This is **living documentation**. +Factories also anchor the documentation: -## 🔎 Diagnostics describe hypotheses, not blame +```csharp +[DocumentedBy(nameof(CurrencyMismatchDocumentation))] +internal static DomainError CurrencyMismatch(...) { ... } +``` -Diagnostics answer: +The linked method describes the stable meaning of the situation: its title, explanation, rule, diagnostics, and representative examples. -* What might have caused this error? -* Is it likely input-related, system-related, or both? -* Where should investigation start? +## Documentation and runtime data are different -Diagnostics are: +The documentation describes the error **category**: -* structured -* human-oriented -* guidance for analysis +- what the situation means; +- which rule it represents; +- what might cause it; +- where investigation can start. -They do not encode operational processes. They provide **direction**, not procedures. +The runtime error describes one **occurrence**: -## 🧭 Error taxonomy +- when it happened; +- its unique instance id; +- the actual diagnostic message; +- occurrence-specific context values. -Errors are modeled as a hierarchy rooted in the abstract `Error` type: +For example, `ORDER_NOT_FOUND` is the stable category. `OrderId = 42` belongs to one occurrence and therefore belongs in [`ErrorContext`](ErrorContext.en.md). -* **`DomainError`** — a violation of a domain rule (the domain layer). -* **`InfrastructureError`** — a failure at a technical boundary. It carries a `Transience` (`Unknown` / `NonTransient` / `Transient`) and an `InteractionDirection`. - * **`PrimaryPortError`** — incoming boundary (`Direction` fixed to `Incoming`). - * **`SecondaryPortError`** — outgoing boundary (`Direction` fixed to `Outgoing`). +## Diagnostics guide investigation -The Port errors replace the old Adapter exceptions. When a port failure wraps several causes, `PrimaryPortInnerErrors` / `SecondaryPortInnerErrors` aggregate the inner errors and compute the overall transience. +A diagnostic is a structured hypothesis composed of: -**Nesting rules.** Inner errors capture *causes*, and the model enforces what may nest in what — by construction, not by convention: +- a possible cause; +- its likely `ErrorOrigin` (`Internal`, `External`, or `InternalOrExternal`); +- an analysis lead. -* A **`DomainError`** nests **only** other `DomainError`s. A domain failure is only ever caused by — or aggregated from — other domain failures; it never carries an infrastructure cause (that would leak a technical concern into the domain vocabulary). -* A **`PrimaryPortError`** / **`SecondaryPortError`** nests **infrastructure errors of its own direction** (a primary port nests primary-port errors, a secondary port nests secondary-port errors) **and/or** `DomainError`s — for example a boundary rejection whose cause is a domain-invariant violation surfaced while mapping an incoming request. The typed `PrimaryPortInnerErrors` / `SecondaryPortInnerErrors` builders make anything else unrepresentable: they only expose `Add(DomainError)` and `Add(`_same-direction port error_`)`. -* The base **`InfrastructureError`** is the permissive general case and accepts any `Error` as inner. Prefer the Port types in real code: they pin the `InteractionDirection` and keep nesting consistent. +Diagnostics should not claim a root cause that is not yet known. They should describe plausible states and suggest what to verify first. -In short: **a domain error contains only domain errors; an infrastructure error contains same-direction infrastructure errors and/or domain errors.** The [FAQ](FAQ.en.md) explains *why* this asymmetry exists. +## One model, two common transports -Each error has a paired exception reached via `error.ToException()`: `DomainException`, `InfrastructureException`, `PrimaryPortException`, `SecondaryPortException`. You never `new` these directly; the exception exposes its `Error` (and through it the context and inner errors). +When the system cannot continue normally, throw the paired exception: -## 🔁 Error or data? Both are supported +```csharp +throw error.ToException(); +``` -Traditionally, exceptions are always thrown. -FirstClassErrors supports two complementary models: +When failure is expected and should remain explicit in the normal flow, return it as data: -* **Exception as control flow** (classic throw) -* **Error as data** (`Outcome`, or non-generic `Outcome` when there is no value) +```csharp +return Outcome.Failure(error); +``` -This allows errors to be: +The error does not change identity when its transport changes. This makes it possible to return an error from domain logic, log it, or escalate it later without recreating or translating the model. -* raised immediately -* transported through validation pipelines -* escalated later +## Error categories -The same error situation can serve both roles. +FirstClassErrors provides several categories for distinguishing domain rule violations from technical-boundary failures: -The non-throwing model is `Outcome` / `Outcome`: the `Error` is carried as data (`IsSuccess` / `IsFailure` / `Error`) and can be converted into an exception on demand via `error.ToException()`. +- `DomainError`; +- `InfrastructureError`; +- `PrimaryPortError`; +- `SecondaryPortError`. -## 🎯 From failures to knowledge - -With this model, errors are no longer: - -> isolated technical failures - -They become: - -> shared, structured knowledge about how the system can fail. - -This bridges: - -* development -* support -* documentation -* operations - -All based on the same source of truth: the code. +Their interaction direction, transience, and composition rules are explained separately in [Error Taxonomy and Composition](ErrorTaxonomy.en.md). --- ---- \ No newline at end of file +--- diff --git a/doc/CoreConcepts.fr.md b/doc/CoreConcepts.fr.md index 5e8a6f1..aab4dde 100644 --- a/doc/CoreConcepts.fr.md +++ b/doc/CoreConcepts.fr.md @@ -1,152 +1,157 @@ -# Concepts clĂ©s +# Concepts fondamentaux -🌍 **Langues:** +🌍 **Langues :** 🇬🇧 [English](./CoreConcepts.en.md) | đŸ‡«đŸ‡· Français (ce fichier) -FirstClassErrors n’est pas simplement une bibliothĂšque utilitaire. Il introduit une autre maniĂšre de penser les erreurs applicatives. +FirstClassErrors sĂ©pare le **sens d’un Ă©chec** du mĂ©canisme utilisĂ© pour le propager. -Au lieu de rĂ©duire une dĂ©faillance Ă  un simple incident technique, elle fait de l’**erreur** qui la sous-tend **une connaissance structurĂ©e sur ce qui s’est mal passĂ©** — l’exception restant le mĂ©canisme qui la signale et la propage. +L’objet central est `Error`. Une exception ou un `Outcome` ne fait que transporter cette erreur. -## 🧠 Une erreur n’est pas juste un message +```mermaid +flowchart LR + A[Factory d’erreur] --> B[Error] + B --> C[ToException] + B --> D[Outcome ou Outcome de T] + A --> E[DocumentedBy] + E --> F[Documentation gĂ©nĂ©rĂ©e] +``` -Dans de nombreux systĂšmes, les exceptions se rĂ©sument Ă  : +## Une erreur reprĂ©sente une situation -> un type + un message texte +Une erreur utile rĂ©pond Ă  une question prĂ©cise : -Avec FirstClassErrors, une **erreur** reprĂ©sente : +> Quelle situation le systĂšme a-t-il reconnue ? -* une **situation d’erreur spĂ©cifique** -* identifiĂ©e par un **code d’erreur stable** -* dĂ©crite avec trois messages dĂ©diĂ©s (un rĂ©sumĂ© public, un dĂ©tail public optionnel et un message de diagnostic interne) -* Ă©ventuellement enrichie de contexte -* associĂ©e Ă  des diagnostics structurĂ©s +Par exemple : -Une **erreur** devient un objet sĂ©mantique, pas seulement un signal d’exĂ©cution. +```csharp +InvalidAmountOperationError.CurrencyMismatch(left, right) +``` -### Trois messages, deux publics +La factory donne Ă  la situation un nom lisible. Son code stable lui donne une identitĂ© lisible par machine : -Une erreur porte trois messages mais les rĂ©partit dĂ©libĂ©rĂ©ment entre seulement **deux publics** — un public externe (utilisateurs finaux / clients d’API) et un public interne (logs, support, dĂ©veloppeurs). La sĂ©paration est garantie par construction : ce qui atteint un appelant ne peut jamais laisser fuiter ce qui est destinĂ© aux dĂ©veloppeurs et au support : +```text +AMOUNT_CURRENCY_MISMATCH +``` -* **`ShortMessage`** (obligatoire) — un rĂ©sumĂ© public court, exposable sans risque Ă  un utilisateur final ou Ă  un client d’API (par ex. le `title` d’un problem detail RFC 9457). -* **`DetailedMessage`** (optionnel) — un dĂ©tail public maĂźtrisĂ©, exposable **uniquement** si l’application le dĂ©cide explicitement (par ex. le `detail` d’un problem detail RFC 9457). Il ne doit jamais contenir d’information sensible ou interne. -* **`DiagnosticMessage`** (obligatoire) — le message de diagnostic interne destinĂ© aux logs, au support et aux dĂ©veloppeurs. Il peut contenir des dĂ©tails techniques/opĂ©rationnels (identifiants, valeurs fautives, Ă©tat interne) et n’est **jamais** exposĂ© aux clients externes par dĂ©faut. `error.ToException()` l’utilise comme `Message` de l’exception. +Une factory doit donc correspondre Ă  un cas d’erreur documentĂ©. -Le cƓur du modĂšle reste agnostique vis-Ă -vis d’HTTP : le message de diagnostic n’est jamais un corps de rĂ©ponse HTTP par dĂ©faut, et `type` et `status` restent l’affaire de l’application. Lorsqu’une erreur est exposĂ©e en HTTP, son `Code` est le `type` RFC 9457 naturel : exposez-le comme un URI stable tel que `urn:problem:{service}:{code}`, oĂč `{code}` est le code d’erreur en minuscules et en kebab-case — par exemple `urn:problem:temperature-simulator:temperature-below-absolute-zero` ou `urn:problem:banking-api:money-transfer-amount-not-positive` — afin que les clients puissent aiguiller sur le type de problĂšme. +## Ce que porte une `Error` -## đŸ§© Une factory reprĂ©sente une situation d’erreur +Une occurrence d’erreur — une instance de la situation Ă  l’exĂ©cution — contient : -Les factories d’**erreur** sont au cƓur du modĂšle. +- un `Code` stable ; +- un `InstanceId` unique ; +- un horodatage `OccurredAt` ; +- des messages publics et internes ; +- un contexte typĂ© optionnel ; +- des erreurs internes optionnelles (erreurs imbriquĂ©es qui enregistrent la cause). -Une mĂ©thode factory : +La factory centralise la crĂ©ation de ces valeurs, afin que chaque occurrence d’une mĂȘme situation reste cohĂ©rente. -* reprĂ©sente un scĂ©nario d’erreur prĂ©cis -* lui donne un **nom** dans le code -* centralise la crĂ©ation de l’erreur -* devient le point d’ancrage de la documentation +## Trois messages, deux publics -Cela signifie : +Une erreur porte trois messages rĂ©partis entre un public externe et un public interne. -> Chaque factory = un cas d’erreur documentĂ©. +| Message | Obligatoire | Public | RĂŽle | +| --- | --- | --- | --- | +| `ShortMessage` | oui | utilisateurs et clients d’API | rĂ©sumĂ© public sĂ»r | +| `DetailedMessage` | non | utilisateurs et clients d’API | dĂ©tail public optionnel et maĂźtrisĂ© | +| `DiagnosticMessage` | oui | logs, support et dĂ©veloppeurs | dĂ©tail interne pour l’investigation | -Les factories amĂ©liorent la lisibilitĂ© et rendent explicites les situations d’erreur, tout en gardant les dĂ©tails de construction en dehors de la logique mĂ©tier. +Cette sĂ©paration est volontaire. Un message de diagnostic peut contenir des identifiants, des valeurs fautives ou un Ă©tat interne qui ne doivent jamais ĂȘtre exposĂ©s par dĂ©faut Ă  un client externe. -## 📘 La documentation vit avec le code +```csharp +return DomainError.Create( + Code.CurrencyMismatch, + diagnosticMessage: $"Impossible d’additionner {left} et {right} car leurs devises diffĂšrent.") + .WithPublicMessage( + shortMessage: "Les montants utilisent des devises diffĂ©rentes.", + detailedMessage: "Les deux montants doivent utiliser la mĂȘme devise."); +``` -La documentation des erreurs est Ă©crite avec le DSL `DescribeError` et liĂ©e directement aux factories d’**erreur**. +`error.ToException()` utilise le message de diagnostic comme `Message` de l’exception. Le mapping des messages publics vers HTTP, gRPC, une interface utilisateur ou un autre transport reste la responsabilitĂ© de l’application. -Cela permet de dĂ©finir : +## La factory est la source de vĂ©ritĂ© -* des descriptions structurĂ©es -* les rĂšgles violĂ©es -* des diagnostics -* des exemples rĂ©alistes +Les factories gardent les dĂ©tails de construction hors de la logique mĂ©tier : -Comme la documentation est du code : +```csharp +if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); +} +``` -* elle Ă©volue avec le systĂšme -* elle ne dĂ©rive pas -* elle peut ĂȘtre extraite automatiquement +Le code nomme la situation reconnue sans rĂ©pĂ©ter son code, ses messages, son contexte ni ses rĂšgles de construction. -C’est de la **documentation vivante**. +Les factories servent Ă©galement de point d’ancrage Ă  la documentation : -## 🔎 Les diagnostics dĂ©crivent des hypothĂšses, pas des fautes +```csharp +[DocumentedBy(nameof(CurrencyMismatchDocumentation))] +internal static DomainError CurrencyMismatch(...) { ... } +``` -Les diagnostics rĂ©pondent Ă  : +La mĂ©thode liĂ©e dĂ©crit le sens stable de la situation : son titre, son explication, sa rĂšgle, ses diagnostics et des exemples reprĂ©sentatifs. -* Qu’est-ce qui pourrait avoir causĂ© cette erreur ? -* Est-ce probablement liĂ© aux donnĂ©es d’entrĂ©e, au systĂšme, ou aux deux ? -* Par oĂč commencer l’investigation ? +## Documentation et donnĂ©es d’exĂ©cution sont diffĂ©rentes -Les diagnostics sont : +La documentation dĂ©crit la **catĂ©gorie** d’erreur : -* structurĂ©s -* orientĂ©s humains -* des guides pour l’analyse +- ce que signifie la situation ; +- quelle rĂšgle elle reprĂ©sente ; +- ce qui pourrait la provoquer ; +- par oĂč commencer l’investigation. -Ils n’encodent pas de processus opĂ©rationnels. Ils donnent une **direction**, pas des procĂ©dures. +L’erreur d’exĂ©cution dĂ©crit une **occurrence** : -## 🧭 Taxonomie des erreurs +- quand elle s’est produite ; +- son identifiant unique ; +- son message de diagnostic rĂ©el ; +- les valeurs de contexte propres Ă  cette occurrence. -Les erreurs sont modĂ©lisĂ©es sous forme de hiĂ©rarchie ayant pour racine le type abstrait `Error` : +Par exemple, `ORDER_NOT_FOUND` est la catĂ©gorie stable. `OrderId = 42` appartient Ă  une occurrence prĂ©cise et doit donc ĂȘtre placĂ© dans [`ErrorContext`](ErrorContext.fr.md). -* **`DomainError`** — une violation d’une rĂšgle mĂ©tier (la couche domaine). -* **`InfrastructureError`** — une dĂ©faillance Ă  une frontiĂšre technique. Elle porte une `Transience` (`Unknown` / `NonTransient` / `Transient`) et une `InteractionDirection`. - * **`PrimaryPortError`** — frontiĂšre entrante (`Direction` fixĂ©e Ă  `Incoming`). - * **`SecondaryPortError`** — frontiĂšre sortante (`Direction` fixĂ©e Ă  `Outgoing`). +## Les diagnostics orientent l’investigation -Les erreurs de Port remplacent les anciennes exceptions d’Adapter. Lorsqu’une dĂ©faillance de port enveloppe plusieurs causes, `PrimaryPortInnerErrors` / `SecondaryPortInnerErrors` agrĂšgent les erreurs internes et calculent la transience globale. +Un diagnostic est une hypothĂšse structurĂ©e composĂ©e de : -**RĂšgles d’imbrication.** Les erreurs internes capturent des *causes*, et le modĂšle contraint ce qui peut s’imbriquer dans quoi — par construction, pas par convention : +- une cause possible ; +- son `ErrorOrigin` probable (`Internal`, `External` ou `InternalOrExternal`) ; +- une piste d’analyse. -* Une **`DomainError`** n’imbrique **que** d’autres `DomainError`. Une dĂ©faillance mĂ©tier n’a jamais pour cause — ou pour agrĂ©gat — que d’autres dĂ©faillances mĂ©tier ; elle ne porte jamais une cause d’infrastructure (ce serait faire fuiter une prĂ©occupation technique dans le vocabulaire du domaine). -* Une **`PrimaryPortError`** / **`SecondaryPortError`** imbrique des **erreurs d’infrastructure de sa propre direction** (un port primaire imbrique des erreurs de port primaire, un port secondaire des erreurs de port secondaire) **et/ou** des `DomainError` — par exemple un rejet Ă  la frontiĂšre dont la cause est une violation d’invariant mĂ©tier dĂ©tectĂ©e lors du mapping d’une requĂȘte entrante. Les collections typĂ©es `PrimaryPortInnerErrors` / `SecondaryPortInnerErrors` rendent tout le reste non reprĂ©sentable : elles n’exposent que `Add(DomainError)` et `Add(`_erreur de port de mĂȘme direction_`)`. -* La base **`InfrastructureError`** est le cas gĂ©nĂ©ral permissif et accepte n’importe quelle `Error` comme cause interne. Dans du vrai code, prĂ©fĂšre les types de Port : ils fixent l’`InteractionDirection` et gardent l’imbrication cohĂ©rente. +Les diagnostics ne doivent pas affirmer une cause racine qui n’est pas encore connue. Ils dĂ©crivent des Ă©tats plausibles et suggĂšrent ce qu’il faut vĂ©rifier en premier. -En rĂ©sumĂ© : **une erreur de domaine ne contient que des erreurs de domaine ; une erreur d’infrastructure contient des erreurs d’infrastructure de mĂȘme direction et/ou des erreurs de domaine.** La [FAQ](FAQ.fr.md) explique *pourquoi* cette asymĂ©trie existe. +## Un modĂšle, deux transports courants -Chaque erreur possĂšde une exception associĂ©e, obtenue via `error.ToException()` : `DomainException`, `InfrastructureException`, `PrimaryPortException`, `SecondaryPortException`. On ne les instancie jamais directement avec `new` ; l’exception expose son `Error` (et, Ă  travers lui, le contexte et les erreurs internes). +Lorsque le systĂšme ne peut pas continuer normalement, levez l’exception associĂ©e : -## 🔁 Erreur ou donnĂ©e ? Les deux sont possibles +```csharp +throw error.ToException(); +``` -Traditionnellement, les exceptions sont toujours levĂ©es. -FirstClassErrors supporte deux modĂšles complĂ©mentaires : +Lorsque l’échec est attendu et doit rester explicite dans le flux normal, retournez-le comme une donnĂ©e : -* **L’exception comme flux de contrĂŽle** (throw classique) -* **L’erreur comme donnĂ©e** (`Outcome`, ou `Outcome` non gĂ©nĂ©rique lorsqu’il n’y a pas de valeur) +```csharp +return Outcome.Failure(error); +``` -Cela permet aux erreurs d’ĂȘtre : +L’erreur ne change pas d’identitĂ© lorsque son transport change. Elle peut donc ĂȘtre retournĂ©e depuis la logique mĂ©tier, journalisĂ©e ou transformĂ©e en exception plus tard sans recrĂ©er ni traduire le modĂšle. -* levĂ©es immĂ©diatement -* transportĂ©es dans des pipelines de validation -* escaladĂ©es plus tard +## CatĂ©gories d’erreur -La mĂȘme situation d’erreur peut servir ces deux rĂŽles. +FirstClassErrors fournit plusieurs catĂ©gories pour distinguer les violations de rĂšgles mĂ©tier des dĂ©faillances aux frontiĂšres techniques : -Le modĂšle sans levĂ©e d’exception est `Outcome` / `Outcome` : l’`Error` est portĂ©e comme donnĂ©e (`IsSuccess` / `IsFailure` / `Error`) et peut ĂȘtre convertie en exception Ă  la demande via `error.ToException()`. +- `DomainError` ; +- `InfrastructureError` ; +- `PrimaryPortError` ; +- `SecondaryPortError`. -## 🎯 De l’échec Ă  la connaissance - -Avec ce modĂšle, les erreurs ne sont plus : - -> des dĂ©faillances techniques isolĂ©es - -Elles deviennent : - -> une connaissance partagĂ©e et structurĂ©e sur la maniĂšre dont le systĂšme peut Ă©chouer. - -Cela crĂ©e un pont entre : - -* le dĂ©veloppement -* le support -* la documentation -* l’exploitation - -Le tout basĂ© sur une mĂȘme source de vĂ©ritĂ© : le code. +Leur direction d’interaction, leur transience et leurs rĂšgles de composition sont expliquĂ©es sĂ©parĂ©ment dans [Taxonomie et composition des erreurs](ErrorTaxonomy.fr.md). --- ---- \ No newline at end of file +--- diff --git a/doc/DesignPrinciples.en.md b/doc/DesignPrinciples.en.md index e722d08..31b5ef4 100644 --- a/doc/DesignPrinciples.en.md +++ b/doc/DesignPrinciples.en.md @@ -3,24 +3,97 @@ 🌍 **Languages:** 🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./DesignPrinciples.fr.md) -FirstClassErrors is built on the idea that errors are not accidental by-products of code, but meaningful parts of the system’s knowledge. In many applications, errors are treated as technical noise — something to log, catch, or hide. This library takes the opposite stance: when an error is expressed, it reveals something about the rules, assumptions, and boundaries of the system. +FirstClassErrors is built around five principles. Each one has a direct consequence in the API and in the way errors are written. -An **error** is not merely a failure of execution. It represents a situation the system recognizes and gives a name to. By turning error situations into explicit concepts — through factory methods, codes, diagnostics, and documentation — the system becomes more readable, more explainable, and more supportable. +## 1. An error is a recognized situation -Another core principle is that documentation must not drift away from behavior. Traditional documentation lives in external files and slowly becomes outdated. Here, documentation is defined next to the code that creates the error. This proximity ensures that knowledge evolves with the system itself. If behavior changes, the documentation changes with it, because they share the same source. +An error is not merely a message emitted after something failed. It is a situation the system recognizes and gives a stable name. -Diagnostics are not post-mortem analysis; they are structured hypotheses. The goal is not to assign blame or determine root causes in advance, but to provide meaningful starting points for investigation. Errors describe what is known, not what is assumed. +```csharp +InvalidAmountOperationError.CurrencyMismatch(left, right) +``` -The library also separates semantics from mechanics. Throwing, catching, logging, or transporting errors are mechanical concerns. The meaning of an error — what rule was violated, what situation occurred, what might explain it — belongs to the domain of knowledge. FirstClassErrors focuses on preserving that meaning, regardless of how the error travels through the system. +The factory name expresses the situation for humans. Its `ErrorCode` gives the same situation a stable identity for clients, logs, dashboards, and documentation. -Finally, the design acknowledges that not every failure should be exceptional in the runtime sense. Some errors are expected parts of normal flow, such as validation failures or parsing issues. By allowing errors to be carried as structured data through `Outcome` instead of thrown, the model supports both throwing and non-throwing flows without losing semantic richness. +**Consequence:** one factory represents one precise error situation. Avoid generic factories such as `InvalidOperation(...)` that hide several unrelated failures behind one name. -In essence, the library encourages teams to treat errors as first-class knowledge artifacts. When errors are explicit, documented, and structured, they improve communication between developers, support teams, and the system itself. +## 2. The error is the model; the exception is a transport + +Throwing is one way to move an error through the system, not the definition of the error itself. + +```csharp +Error error = InvalidAmountOperationError.CurrencyMismatch(left, right); + +throw error.ToException(); +``` + +The same error can instead be carried as data: + +```csharp +return Outcome.Failure(error); +``` + +**Consequence:** model the meaning once, then choose the transport according to the flow. Use an exception when execution cannot continue normally; use `Outcome` when failure is expected and should be handled explicitly. + +## 3. Public and internal information must remain separate + +A useful diagnostic message often contains identifiers, offending values, or internal state. That information must not accidentally become an API response. + +FirstClassErrors separates: + +- public messages intended for users or API clients; +- an internal diagnostic message intended for logs, support, and developers. + +The staged builder — each construction step only exposes the next valid calls — enforces that distinction when the error is created. + +**Consequence:** public messages remain safe and controlled, while diagnostics can still contain the detail required for investigation. + +## 4. Documentation belongs beside behavior + +Documentation written far from the code eventually drifts. FirstClassErrors links each factory to structured documentation in the same class: + +```csharp +[DocumentedBy(nameof(CurrencyMismatchDocumentation))] +internal static DomainError CurrencyMismatch(...) { ... } +``` + +The documentation method describes the situation, rule, diagnostic hypotheses, and executable examples. + +**Consequence:** changing an error situation naturally brings its construction and documentation into the same review. The catalog is generated from the code rather than maintained as a second source of truth. + +## 5. Diagnostics are hypotheses, not verdicts + +No one can be certain of an error's exact origin — even a likely cause may itself just be a symptom of something deeper. What can usually be named are several plausible causes, and naming them gives investigation somewhere to start. + +Prefer: + +> The amounts reached the operation without being converted to one currency. + +Over: + +> The developer forgot to convert the amounts. + +**Consequence:** diagnostics describe observable states and direct investigation. They do not encode support procedures, claim certainty, or accuse a person or system. + +## The resulting model + +These principles work together: + +```mermaid +flowchart LR + A[Named factory] --> B[Structured Error] + B --> C[Exception] + B --> D[Outcome] + A --> E[Structured documentation] + E --> F[Generated catalog] +``` + +The factory defines one meaningful situation. The `Error` preserves that meaning whichever transport is chosen, and the linked documentation makes the same knowledge available outside the runtime flow. --- ---- \ No newline at end of file +--- diff --git a/doc/DesignPrinciples.fr.md b/doc/DesignPrinciples.fr.md index 1d7e576..2d37ace 100644 --- a/doc/DesignPrinciples.fr.md +++ b/doc/DesignPrinciples.fr.md @@ -1,26 +1,99 @@ # Principes de conception -🌍 **Langues:** +🌍 **Langues :** 🇬🇧 [English](./DesignPrinciples.en.md) | đŸ‡«đŸ‡· Français (ce fichier) -FirstClassErrors repose sur l’idĂ©e que les erreurs ne sont pas des sous-produits accidentels du code, mais des Ă©lĂ©ments porteurs de sens dans la connaissance du systĂšme. Dans de nombreuses applications, les erreurs sont traitĂ©es comme du bruit technique — quelque chose Ă  logger, attraper ou masquer. Cette bibliothĂšque adopte la position inverse : lorsqu’une erreur est exprimĂ©e, elle rĂ©vĂšle quelque chose sur les rĂšgles, les hypothĂšses et les frontiĂšres du systĂšme. +FirstClassErrors repose sur cinq principes. Chacun possĂšde une consĂ©quence directe dans l’API et dans la maniĂšre d’écrire les erreurs. -Une **erreur** n’est pas simplement un Ă©chec d’exĂ©cution. Elle reprĂ©sente une situation que le systĂšme reconnaĂźt et Ă  laquelle il donne un nom. En transformant les situations d’erreur en concepts explicites — via des mĂ©thodes factory, des codes, des diagnostics et de la documentation — le systĂšme devient plus lisible, plus explicable et plus facile Ă  maintenir. +## 1. Une erreur est une situation reconnue -Un autre principe fondamental est que la documentation ne doit pas dĂ©river du comportement. La documentation traditionnelle vit dans des fichiers externes et devient progressivement obsolĂšte. Ici, la documentation est dĂ©finie Ă  cĂŽtĂ© du code qui crĂ©e l’erreur. Cette proximitĂ© garantit que la connaissance Ă©volue avec le systĂšme lui-mĂȘme. Si le comportement change, la documentation change avec lui, car ils partagent la mĂȘme source. +Une erreur n’est pas seulement un message Ă©mis aprĂšs un Ă©chec. C’est une situation que le systĂšme reconnaĂźt et Ă  laquelle il donne un nom stable. -Les diagnostics ne sont pas une analyse post-mortem ; ce sont des hypothĂšses structurĂ©es. L’objectif n’est pas d’attribuer une faute ni de dĂ©terminer Ă  l’avance une cause racine, mais de fournir des points de dĂ©part pertinents pour l’investigation. Les erreurs dĂ©crivent ce qui est connu, pas ce qui est supposĂ©. +```csharp +InvalidAmountOperationError.CurrencyMismatch(left, right) +``` -La bibliothĂšque sĂ©pare Ă©galement la sĂ©mantique de la mĂ©canique. Lever, intercepter, logger ou transporter des erreurs sont des prĂ©occupations mĂ©caniques. Le sens d’une erreur — quelle rĂšgle a Ă©tĂ© violĂ©e, quelle situation s’est produite, ce qui pourrait l’expliquer — appartient au domaine de la connaissance. FirstClassErrors se concentre sur la prĂ©servation de ce sens, indĂ©pendamment de la maniĂšre dont l’erreur circule dans le systĂšme. +Le nom de la factory exprime la situation pour les humains. Son `ErrorCode` donne Ă  cette mĂȘme situation une identitĂ© stable pour les clients, les logs, les dashboards et la documentation. -Enfin, la conception reconnaĂźt que tous les Ă©checs ne doivent pas ĂȘtre exceptionnels au sens runtime. Certaines erreurs font partie du flux normal, comme les Ă©checs de validation ou les problĂšmes de parsing. En permettant de transporter les erreurs comme donnĂ©es structurĂ©es via `Outcome` plutĂŽt que de les lever, le modĂšle supporte Ă  la fois les flux avec et sans levĂ©e d’exception, sans perdre la richesse sĂ©mantique. +**ConsĂ©quence :** une factory reprĂ©sente une situation d’erreur prĂ©cise. Évitez les factories gĂ©nĂ©riques comme `InvalidOperation(...)`, qui masquent plusieurs Ă©checs sans rapport derriĂšre un mĂȘme nom. -En essence, la bibliothĂšque encourage les Ă©quipes Ă  considĂ©rer les erreurs comme des artefacts de connaissance de premier plan. Lorsque les erreurs sont explicites, documentĂ©es et structurĂ©es, elles amĂ©liorent la communication entre les dĂ©veloppeurs, les Ă©quipes de support et le systĂšme lui-mĂȘme. +## 2. L’erreur est le modĂšle ; l’exception est un transport + +Lever une exception est une maniĂšre de faire circuler une erreur dans le systĂšme, pas la dĂ©finition de l’erreur elle-mĂȘme. + +```csharp +Error error = InvalidAmountOperationError.CurrencyMismatch(left, right); + +throw error.ToException(); +``` + +La mĂȘme erreur peut ĂȘtre transportĂ©e comme une donnĂ©e : + +```csharp +return Outcome.Failure(error); +``` + +**ConsĂ©quence :** modĂ©lisez le sens une seule fois, puis choisissez le transport selon le flux. Utilisez une exception lorsque l’exĂ©cution ne peut pas continuer normalement ; utilisez `Outcome` lorsque l’échec est attendu et doit ĂȘtre traitĂ© explicitement. + +## 3. Les informations publiques et internes doivent rester sĂ©parĂ©es + +Un message de diagnostic utile contient souvent des identifiants, des valeurs fautives ou un Ă©tat interne. Ces informations ne doivent pas devenir accidentellement une rĂ©ponse d’API. + +FirstClassErrors sĂ©pare : + +- les messages publics destinĂ©s aux utilisateurs ou clients d’API ; +- le message de diagnostic interne destinĂ© aux logs, au support et aux dĂ©veloppeurs. + +Le builder Ă©tagĂ© — chaque Ă©tape de construction n’expose que les appels valides suivants — impose cette distinction lors de la crĂ©ation de l’erreur. + +**ConsĂ©quence :** les messages publics restent sĂ»rs et maĂźtrisĂ©s, tandis que les diagnostics peuvent conserver les dĂ©tails nĂ©cessaires Ă  l’investigation. + +## 4. La documentation doit vivre Ă  cĂŽtĂ© du comportement + +Une documentation Ă©crite loin du code finit par dĂ©river. FirstClassErrors relie chaque factory Ă  une documentation structurĂ©e situĂ©e dans la mĂȘme classe : + +```csharp +[DocumentedBy(nameof(CurrencyMismatchDocumentation))] +internal static DomainError CurrencyMismatch(...) { ... } +``` + +La mĂ©thode de documentation dĂ©crit la situation, la rĂšgle, les hypothĂšses de diagnostic et des exemples exĂ©cutables. + +**ConsĂ©quence :** modifier une situation d’erreur amĂšne naturellement sa construction et sa documentation dans la mĂȘme revue. Le catalogue est gĂ©nĂ©rĂ© depuis le code au lieu d’ĂȘtre maintenu comme une seconde source de vĂ©ritĂ©. + +## 5. Les diagnostics sont des hypothĂšses, pas des verdicts + +Personne ne peut ĂȘtre certain de l’origine exacte d’une erreur — mĂȘme une cause probable peut n’ĂȘtre que le symptĂŽme d’une cause plus profonde. Ce qui peut gĂ©nĂ©ralement ĂȘtre identifiĂ©, ce sont plusieurs causes plausibles, et les nommer donne Ă  l’investigation un point de dĂ©part. + +PrĂ©fĂ©rez : + +> Les montants ont atteint l’opĂ©ration sans avoir Ă©tĂ© convertis dans une devise commune. + +À : + +> Le dĂ©veloppeur a oubliĂ© de convertir les montants. + +**ConsĂ©quence :** les diagnostics dĂ©crivent des Ă©tats observables et orientent l’investigation. Ils n’encodent pas de procĂ©dures de support, n’affirment pas une certitude et n’accusent ni une personne ni un systĂšme. + +## Le modĂšle qui en dĂ©coule + +Ces principes fonctionnent ensemble : + +```mermaid +flowchart LR + A[Factory nommĂ©e] --> B[Error structurĂ©e] + B --> C[Exception] + B --> D[Outcome] + A --> E[Documentation structurĂ©e] + E --> F[Catalogue gĂ©nĂ©rĂ©] +``` + +La factory dĂ©finit une situation significative. L’`Error` en prĂ©serve le sens quel que soit le transport choisi, et la documentation liĂ©e rend cette mĂȘme connaissance disponible en dehors du flux d’exĂ©cution. --- ---- \ No newline at end of file +--- diff --git a/doc/ErrorTaxonomy.en.md b/doc/ErrorTaxonomy.en.md new file mode 100644 index 0000000..6cc3a66 --- /dev/null +++ b/doc/ErrorTaxonomy.en.md @@ -0,0 +1,165 @@ +# Error Taxonomy and Composition + +🌍 **Languages:** +🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./ErrorTaxonomy.fr.md) + +FirstClassErrors distinguishes the **nature of a failure** from the place where it happens to be detected. + +That distinction helps domain code remain independent from infrastructure while giving operational tooling useful signals such as interaction direction (incoming or outgoing) and transience (whether retrying may succeed). + +## The hierarchy at a glance + +```mermaid +classDiagram + Error <|-- DomainError + Error <|-- InfrastructureError + InfrastructureError <|-- PrimaryPortError + InfrastructureError <|-- SecondaryPortError +``` + +| Type | Meaning | Typical example | +| --- | --- | --- | +| `DomainError` | a domain rule or invariant was violated | an amount uses an unsupported currency | +| `InfrastructureError` | a technical interaction failed, without a more precise port category | a generic infrastructure component failed | +| `PrimaryPortError` | an incoming interaction could not be accepted or processed | an API request cannot be mapped into valid domain values | +| `SecondaryPortError` | an outgoing interaction failed | a database or remote service is unavailable | + +## `DomainError`: the business rule was violated + +Use a `DomainError` when the failure is expressed entirely in domain language. + +```csharp +return DomainError.Create( + Code.CurrencyMismatch, + diagnosticMessage: $"Cannot combine {left.Currency} and {right.Currency} amounts.") + .WithPublicMessage( + shortMessage: "The amounts use different currencies."); +``` + +The same error remains a domain error whether it is detected in a value object, an entity, a domain service, or while an adapter is constructing a domain value. + +The type follows **which rule was violated**, not which layer happened to observe it. + +## `PrimaryPortError`: an incoming interaction failed + +A primary port represents an interaction entering the application: HTTP, messaging, a CLI command, a file import, or another inbound adapter. + +Consider a bank-statement file import rejecting a transaction whose date falls outside the statement period: + +```csharp +internal static PrimaryPortError DateOutOfStatementPeriod(DateOnly periodStart, DateOnly periodEnd, DateOnly transactionDate) { + return PrimaryPortError.Create( + Code.DateOutOfStatementPeriod, + $"Transaction dated {transactionDate} is outside the statement period [{periodStart};{periodEnd}].", + Transience.NonTransient, + ctx => ctx.Add(ErrCtxKey.TransactionDate, transactionDate)) + .WithPublicMessage("The file contains a transaction outside the statement period."); +} +``` + +The port error describes the boundary condition on its own: this incoming file cannot be accepted as it stands. + +Two facts may instead need to be preserved together: the domain rejected a value because an invariant was violated, and the incoming request therefore cannot be accepted. The adapter can wrap the domain cause in a primary-port error: + +```csharp +DomainError mismatch = InvalidAmountOperationError.CurrencyMismatch(left, right); +var innerErrors = new PrimaryPortInnerErrors().Add(mismatch); + +return PrimaryPortError.Create( + ErrorCode.Create("REQUEST_REJECTED"), + $"Request rejected: {mismatch.DiagnosticMessage}", + innerErrors) + .WithPublicMessage("The request contains invalid data."); +``` + +`PrimaryPortInnerErrors` is a typed collection that only accepts the error types a primary-port error may nest (see [Composition rules](#composition-rules)). + +The domain error still describes the violated business rule. The primary-port error describes the boundary condition: this incoming interaction is rejected. When inner errors are supplied, the port error computes its overall transience from them. + +## `SecondaryPortError`: an outgoing interaction failed + +A secondary port represents an interaction initiated by the application toward a database, broker, filesystem, or remote service. + +```csharp +return SecondaryPortError.Create( + Code.PaymentProviderUnavailable, + diagnosticMessage: "The payment provider timed out after 5 seconds.", + transience: Transience.Transient) + .WithPublicMessage( + shortMessage: "The payment service is temporarily unavailable."); +``` + +The outgoing direction and transient classification provide operational meaning that a generic exception type or message would not preserve. + +## `Transience`: is retrying meaningful? + +Infrastructure errors carry a `Transience` value: + +| Value | Meaning | +| --- | --- | +| `Transient` | the same operation may succeed later without changing the request | +| `NonTransient` | retrying the same operation is not expected to help | +| `Unknown` | the system cannot classify the failure reliably | + +Examples: + +- database timeout → usually `Transient`; +- unsupported request format → `NonTransient`; +- unclassified third-party failure → `Unknown`. + +Transience is an operational hint, not a retry policy. The application still decides whether, when, and how often to retry. + +## Interaction direction + +Port errors fix their direction by construction: + +- `PrimaryPortError` → `Incoming`; +- `SecondaryPortError` → `Outgoing`. + +This allows logs and monitoring to distinguish a rejected inbound request from a failing outbound dependency, even when both are non-transient. + +For example, an invalid email entered by a user should not trigger the same alert as a database outage. Direction and transience preserve that distinction. + +## Composition rules + +Inner errors represent causes or aggregated failures. The model constrains what may be nested: + +| Outer error | Allowed inner errors | +| --- | --- | +| `DomainError` | `DomainError` only | +| `PrimaryPortError` | `DomainError` and `PrimaryPortError` | +| `SecondaryPortError` | `DomainError` and `SecondaryPortError` | +| base `InfrastructureError` | any `Error` | + +These rules prevent technical concerns from leaking into domain vocabulary and prevent an incoming port error from accidentally containing an unrelated outgoing-port classification. + +### Why a domain error cannot contain an infrastructure error + +A `DomainError` states that a business rule was violated. If it contained a database timeout as its cause, the model would describe a technical outage as though it were part of the business rule. + +Keep the domain failure and the technical failure distinct. If the actual failure is infrastructural, represent it with an infrastructure or port error at the boundary that owns that interaction. + +### Why a port error may contain a domain error + +A boundary may legitimately reject an interaction because domain construction failed. The domain error explains the underlying rule; the port error explains the boundary-level outcome. + +This preserves both facts without making the domain depend on HTTP, messaging, files, or any other adapter technology. + +## Choosing the type + +Ask these questions in order: + +1. **Was a business rule or invariant violated?** Use `DomainError`. +2. **Did an incoming interaction fail at the application boundary?** Use `PrimaryPortError`. +3. **Did an outgoing dependency interaction fail?** Use `SecondaryPortError`. +4. **Is it infrastructural but not meaningfully classified by direction?** Use `InfrastructureError`. + +Do not choose a type from the current class or folder. Choose it from the meaning of the failure. + +--- + + + +--- diff --git a/doc/ErrorTaxonomy.fr.md b/doc/ErrorTaxonomy.fr.md new file mode 100644 index 0000000..82d1b42 --- /dev/null +++ b/doc/ErrorTaxonomy.fr.md @@ -0,0 +1,165 @@ +# Taxonomie et composition des erreurs + +🌍 **Langues :** +🇬🇧 [English](./ErrorTaxonomy.en.md) | đŸ‡«đŸ‡· Français (ce fichier) + +FirstClassErrors distingue la **nature d’un Ă©chec** de l’endroit oĂč il est dĂ©tectĂ©. + +Cette distinction permet au domaine de rester indĂ©pendant de l’infrastructure, tout en fournissant Ă  l’exploitation des signaux utiles comme la direction de l’interaction (entrante ou sortante) et la transience (retenter peut-il rĂ©ussir ?). + +## La hiĂ©rarchie en un coup d’Ɠil + +```mermaid +classDiagram + Error <|-- DomainError + Error <|-- InfrastructureError + InfrastructureError <|-- PrimaryPortError + InfrastructureError <|-- SecondaryPortError +``` + +| Type | Signification | Exemple courant | +| --- | --- | --- | +| `DomainError` | une rĂšgle ou un invariant mĂ©tier a Ă©tĂ© violĂ© | un montant utilise une devise incompatible | +| `InfrastructureError` | une interaction technique a Ă©chouĂ© sans catĂ©gorie de port plus prĂ©cise | un composant d’infrastructure gĂ©nĂ©rique Ă©choue | +| `PrimaryPortError` | une interaction entrante ne peut pas ĂȘtre acceptĂ©e ou traitĂ©e | une requĂȘte API ne peut pas ĂȘtre convertie en valeurs mĂ©tier valides | +| `SecondaryPortError` | une interaction sortante a Ă©chouĂ© | une base de donnĂ©es ou un service distant est indisponible | + +## `DomainError` : une rĂšgle mĂ©tier a Ă©tĂ© violĂ©e + +Utilisez une `DomainError` lorsque l’échec s’exprime entiĂšrement dans le langage du domaine. + +```csharp +return DomainError.Create( + Code.CurrencyMismatch, + diagnosticMessage: $"Impossible de combiner des montants en {left.Currency} et {right.Currency}.") + .WithPublicMessage( + shortMessage: "Les montants utilisent des devises diffĂ©rentes."); +``` + +La mĂȘme erreur reste une erreur de domaine, qu’elle soit dĂ©tectĂ©e dans un value object, une entitĂ©, un service de domaine ou pendant qu’un adapter construit une valeur mĂ©tier. + +Le type dĂ©pend de **la rĂšgle violĂ©e**, pas de la couche qui constate l’échec. + +## `PrimaryPortError` : une interaction entrante a Ă©chouĂ© + +Un port primaire reprĂ©sente une interaction qui entre dans l’application : HTTP, message, commande CLI, import de fichier ou autre adapter entrant. + +Prenons l’import d’un fichier de relevĂ© bancaire qui rejette une transaction dont la date tombe hors de la pĂ©riode du relevĂ© : + +```csharp +internal static PrimaryPortError DateOutOfStatementPeriod(DateOnly periodStart, DateOnly periodEnd, DateOnly transactionDate) { + return PrimaryPortError.Create( + Code.DateOutOfStatementPeriod, + $"La transaction datĂ©e du {transactionDate} est hors de la pĂ©riode de relevĂ© [{periodStart};{periodEnd}].", + Transience.NonTransient, + ctx => ctx.Add(ErrCtxKey.TransactionDate, transactionDate)) + .WithPublicMessage("Le fichier contient une transaction hors de la pĂ©riode de relevĂ©."); +} +``` + +L’erreur de port dĂ©crit Ă  elle seule la condition Ă  la frontiĂšre : ce fichier entrant ne peut pas ĂȘtre acceptĂ© tel quel. + +Deux faits peuvent au contraire devoir ĂȘtre conservĂ©s ensemble : le domaine a refusĂ© une valeur parce qu’un invariant a Ă©tĂ© violĂ©, et la requĂȘte entrante ne peut donc pas ĂȘtre acceptĂ©e. L’adapter peut encapsuler la cause mĂ©tier dans une erreur de port primaire : + +```csharp +DomainError mismatch = InvalidAmountOperationError.CurrencyMismatch(left, right); +var innerErrors = new PrimaryPortInnerErrors().Add(mismatch); + +return PrimaryPortError.Create( + ErrorCode.Create("REQUEST_REJECTED"), + $"RequĂȘte rejetĂ©e : {mismatch.DiagnosticMessage}", + innerErrors) + .WithPublicMessage("La requĂȘte contient des donnĂ©es invalides."); +``` + +`PrimaryPortInnerErrors` est une collection typĂ©e qui n’accepte que les types d’erreurs qu’une erreur de port primaire peut imbriquer (voir [RĂšgles de composition](#rĂšgles-de-composition)). + +L’erreur de domaine dĂ©crit toujours la rĂšgle violĂ©e. L’erreur de port primaire dĂ©crit la condition Ă  la frontiĂšre : cette interaction entrante est rejetĂ©e. Lorsque des erreurs internes sont fournies, l’erreur de port calcule sa transience globale Ă  partir d’elles. + +## `SecondaryPortError` : une interaction sortante a Ă©chouĂ© + +Un port secondaire reprĂ©sente une interaction initiĂ©e par l’application vers une base de donnĂ©es, un broker, un systĂšme de fichiers ou un service distant. + +```csharp +return SecondaryPortError.Create( + Code.PaymentProviderUnavailable, + diagnosticMessage: "Le fournisseur de paiement a dĂ©passĂ© le dĂ©lai de 5 secondes.", + transience: Transience.Transient) + .WithPublicMessage( + shortMessage: "Le service de paiement est temporairement indisponible."); +``` + +La direction sortante et la classification transiente apportent un sens opĂ©rationnel qu’un type d’exception ou un message gĂ©nĂ©rique ne conserverait pas. + +## `Transience` : retenter peut-il ĂȘtre utile ? + +Les erreurs d’infrastructure portent une valeur `Transience` : + +| Valeur | Signification | +| --- | --- | +| `Transient` | la mĂȘme opĂ©ration peut rĂ©ussir plus tard sans modifier la requĂȘte | +| `NonTransient` | retenter la mĂȘme opĂ©ration ne devrait pas aider | +| `Unknown` | le systĂšme ne peut pas classer l’échec de façon fiable | + +Exemples : + +- timeout de base de donnĂ©es → gĂ©nĂ©ralement `Transient` ; +- format de requĂȘte non supportĂ© → `NonTransient` ; +- Ă©chec tiers non classĂ© → `Unknown`. + +La transience est un indice opĂ©rationnel, pas une politique de retry. L’application dĂ©cide toujours si, quand et combien de fois elle retente. + +## Direction de l’interaction + +Les erreurs de port fixent leur direction par construction : + +- `PrimaryPortError` → `Incoming` ; +- `SecondaryPortError` → `Outgoing`. + +Les logs et la supervision peuvent ainsi distinguer une requĂȘte entrante rejetĂ©e d’une dĂ©pendance sortante en panne, mĂȘme si les deux erreurs sont non transientes. + +Par exemple, une adresse e-mail invalide saisie par un utilisateur ne doit pas dĂ©clencher la mĂȘme alerte qu’une indisponibilitĂ© de base de donnĂ©es. La direction et la transience prĂ©servent cette diffĂ©rence. + +## RĂšgles de composition + +Les erreurs internes reprĂ©sentent des causes ou des Ă©checs agrĂ©gĂ©s. Le modĂšle contraint ce qui peut ĂȘtre imbriquĂ© : + +| Erreur externe | Erreurs internes autorisĂ©es | +| --- | --- | +| `DomainError` | uniquement des `DomainError` | +| `PrimaryPortError` | des `DomainError` et `PrimaryPortError` | +| `SecondaryPortError` | des `DomainError` et `SecondaryPortError` | +| `InfrastructureError` de base | n’importe quelle `Error` | + +Ces rĂšgles empĂȘchent les prĂ©occupations techniques de fuiter dans le vocabulaire mĂ©tier et Ă©vitent qu’une erreur de port entrant contienne accidentellement une classification de port sortant sans rapport. + +### Pourquoi une erreur de domaine ne contient-elle pas une erreur d’infrastructure ? + +Une `DomainError` affirme qu’une rĂšgle mĂ©tier a Ă©tĂ© violĂ©e. Si elle contenait un timeout de base de donnĂ©es comme cause, le modĂšle dĂ©crirait une panne technique comme si elle faisait partie de la rĂšgle mĂ©tier. + +Gardez l’échec mĂ©tier et l’échec technique distincts. Lorsque l’échec rĂ©el est infrastructurel, reprĂ©sentez-le par une erreur d’infrastructure ou de port Ă  la frontiĂšre qui possĂšde cette interaction. + +### Pourquoi une erreur de port peut-elle contenir une erreur de domaine ? + +Une frontiĂšre peut lĂ©gitimement rejeter une interaction parce que la construction d’un objet mĂ©tier a Ă©chouĂ©. L’erreur de domaine explique la rĂšgle sous-jacente ; l’erreur de port explique le rĂ©sultat au niveau de la frontiĂšre. + +Les deux faits sont prĂ©servĂ©s sans rendre le domaine dĂ©pendant de HTTP, de la messagerie, des fichiers ou d’une autre technologie d’adapter. + +## Choisir le type + +Posez ces questions dans l’ordre : + +1. **Une rĂšgle ou un invariant mĂ©tier a-t-il Ă©tĂ© violĂ© ?** Utilisez `DomainError`. +2. **Une interaction entrante a-t-elle Ă©chouĂ© Ă  la frontiĂšre de l’application ?** Utilisez `PrimaryPortError`. +3. **Une interaction avec une dĂ©pendance sortante a-t-elle Ă©chouĂ© ?** Utilisez `SecondaryPortError`. +4. **L’échec est-il infrastructurel sans direction pertinente ?** Utilisez `InfrastructureError`. + +Ne choisissez pas un type selon la classe ou le dossier actuel. Choisissez-le selon le sens de l’échec. + +--- + + + +--- diff --git a/doc/GettingStarted.en.md b/doc/GettingStarted.en.md index 053112e..10cf3d4 100644 --- a/doc/GettingStarted.en.md +++ b/doc/GettingStarted.en.md @@ -3,192 +3,182 @@ 🌍 **Languages:** 🇬🇧 English (this file) | đŸ‡«đŸ‡· [Français](./GettingStarted.fr.md) -FirstClassErrors helps you treat errors as **structured, diagnosable knowledge** instead of simple exception messages. +This guide takes you from an empty project to your first generated error catalog. -In a few minutes, you will see how to: +You will: -* define an error (with a factory) -* use error factories (required for living documentation) -* document the error in a structured way -* attach diagnostics -* optionally use the error without throwing +1. install the library and CLI; +2. opt a project into documentation generation; +3. define one documented error; +4. use it in code; +5. generate the catalog. -## 1ïžâƒŁ Define an error (with a factory) +## 1. Install the package and CLI -To benefit from **living documentation**, errors are not created directly with `new`. Instead, they are created through **static factory methods** inside the error class. +In the application project: -This pattern is essential because: +```bash +dotnet add package FirstClassErrors +``` + +Install the documentation CLI once on your machine: + +```bash +dotnet tool install --global FirstClassErrors.Cli +``` -* each factory method represents a **specific error situation** -* it is the anchor point for the documentation DSL -* the documentation generator links factories to documentation +## 2. Opt the project into documentation generation + +Add this property directly to the project file that contains your errors: + +```xml + + true + +``` -Note: +The marker must be present in the `.csproj` itself. Projects without it are skipped when the CLI scans a solution. -*Using factory methods to create errors is a well-established .NET pattern for centralizing and standardizing error creation. FirstClassErrors builds on this idea and makes error factories the anchor point for structured, living error documentation. Beyond documentation, factories significantly improve code readability: they keep error construction (error codes, messages, formatting, and wording) out of the “happy path,” allowing domain logic to remain focused on business rules rather than technical details. A call such as `throw InvalidAmountOperationError.CurrencyMismatch(a1, a2).ToException();` expresses intent far more clearly than inlined error construction. This approach aligns with clean code principles by separating concerns, reducing duplication, and giving each error situation a named, explicit representation in the codebase — while also providing a single, consistent place to attach diagnostics and documentation.* +## 3. Define one error situation -Example: +Create a static factory class. Each factory method represents one precise situation the system recognizes. The examples use a small `Amount` value type with a `Currency`; substitute any type from your own domain. ```csharp +using FirstClassErrors; + [ProvidesErrorsFor(nameof(Amount))] public static class InvalidAmountOperationError { [DocumentedBy(nameof(CurrencyMismatchDocumentation))] - internal static DomainError CurrencyMismatch(Amount amount1, Amount amount2) { + internal static DomainError CurrencyMismatch(Amount left, Amount right) { return DomainError.Create( Code.CurrencyMismatch, - diagnosticMessage: $"Failed to perform the monetary operation because the involved amounts are expressed in different currencies: {amount1} and {amount2}.") + diagnosticMessage: $"Cannot add {left} and {right} because their currencies differ.") .WithPublicMessage( - shortMessage: "Currency mismatch", - detailedMessage: "The amounts involved in the operation are expressed in different currencies."); + shortMessage: "The amounts use different currencies.", + detailedMessage: "Both amounts must use the same currency."); } private static class Code { - public static readonly ErrorCode CurrencyMismatch = ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"); + public static readonly ErrorCode CurrencyMismatch = + ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"); } - } ``` -Here: - -* The **error type** represents a category of domain errors. -* The **factory method** represents a precise error case. -* The **error code** is stable and machine-readable. -* The factory method is what will be documented. - -The error is built in two stages: `DomainError.Create(...)` captures the mandatory internal information (the error `code` and the internal `diagnosticMessage`), and `.WithPublicMessage(shortMessage, detailedMessage)` supplies the public-facing messages and produces the final error. The mandatory `shortMessage` is a safe public summary, the optional `detailedMessage` is a controlled public detail, and the `diagnosticMessage` is for logs, support and developers — never exposed to clients by default. There is no `.Build()`, and the public constructors are internal, so an error can never be left without its public message. +The four important parts are: -You never `new` the error or the exception yourself: the error is created through the staged builder inside the factory, and when you need to throw, you call `error.ToException()` (see section 4). +- `[ProvidesErrorsFor]` ties the factory class to the concept whose errors it declares; the generator uses it to find and group errors; +- the factory name expresses the situation in code; +- the error code is its stable, machine-readable identity; +- the diagnostic message is internal, while the public messages are safe for callers. -## 2ïžâƒŁ Link the factory to structured documentation +## 4. Add the structured documentation -Each factory method is linked to documentation using `[DocumentedBy]`. +The `[DocumentedBy]` attribute links the factory to a documentation method in the same class: ```csharp private static ErrorDocumentation CurrencyMismatchDocumentation() { return DescribeError.WithTitle("Amount currency mismatch") - .WithDescription("This error occurs when trying to use multiple amounts together in an operation while they are expressed in different currencies.") - .WithRule("All monetary operations must involve amounts expressed in the same currency.") + .WithDescription( + "This error occurs when an operation combines amounts expressed in different currencies.") + .WithRule( + "A monetary operation must use one common currency.") .WithDiagnostic( - "Amounts were used in a monetary operation without having been converted to the same currency.", + "The amounts reached the operation without being converted to one currency.", ErrorOrigin.Internal, - "Verify whether all amounts involved in the operation were converted to a common currency before being used together." - ) + "Verify where the amounts should have been converted before this operation.") .AndDiagnostic( - "Amounts expected to be expressed in the same currency were provided with different currencies.", - ErrorOrigin.InternalOrExternal, - "Check the currencies associated with each amount and confirm whether a common currency was expected for this operation." - ) - .WithExamples(() => CurrencyMismatch(new Amount(127.33m, Currency.EUR), new Amount(57689.00m, Currency.USD))); + "The caller provided amounts in currencies that cannot be combined directly.", + ErrorOrigin.External, + "Check the currencies supplied by the caller.") + .WithExamples(() => CurrencyMismatch( + new Amount(10m, Currency.EUR), + new Amount(12m, Currency.USD))); } ``` -Each diagnostic declares an **origin** via `ErrorOrigin`, whose values are `Internal`, `External`, and `InternalOrExternal` — indicating whether the cause lies inside the system, outside it (input), or could be either. - -This documentation: - -* explains what the error means -* states the violated rule -* provides diagnostic hypotheses -* gives realistic example messages - -This is structured knowledge, not a comment. - -## 3ïžâƒŁ Add structured error context (`ErrorContext`) - -When information helps diagnose **a specific occurrence**, attach it as context. - -```csharp -return PrimaryPortError.Create( - Code.DateOutOfStatementPeriod, - diagnosticMessage: $"Transaction dated {transactionDate} is outside the statement period [{periodStart};{periodEnd}].", - transience: Transience.NonTransient, - configureContext: ctx => ctx.Add(ErrCtxKey.TransactionDate, transactionDate)) - .WithPublicMessage( - shortMessage: "Transaction date is outside the statement period.", - detailedMessage: "The transaction date falls outside the allowed statement period."); -``` - -The context lives on the `Error`; when an exception is later produced with `error.ToException()`, it is reached through `exception.Error.Context`. +This is structured knowledge rather than a comment: the generator can extract the title, explanation, rule, diagnostics, and real example produced by the factory. -Best practices: +Each diagnostic is a hypothesis: a plausible cause, an origin (whether the suspected cause lies inside the system or with an external caller), and an investigation lead. -* use named, stable keys (`ErrorContextKey`) -* add context at factory level -* avoid sensitive or oversized data +## 5. Use the error -## 4ïžâƒŁ Use the exception in domain code +When the failure is exceptional, turn the error into its paired exception: ```csharp public Amount Add(Amount other) { - if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } + if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); + } return new Amount(Value + other.Value, Currency); } ``` -Domain logic remains clean and expressive. +The business code names the situation without repeating codes or messages. -## 5ïžâƒŁ Or use it without throwing (`Outcome`) - -For validation or batch scenarios: +When failure is an expected part of the flow, carry the same `Error` without throwing: ```csharp -public static Outcome TryAdd(Amount a1, Amount a2) { - if (a1.Currency != a2.Currency) { - return Outcome.Failure(InvalidAmountOperationError.CurrencyMismatch(a1, a2)); +public static Outcome TryAdd(Amount left, Amount right) { + if (left.Currency != right.Currency) { + return Outcome.Failure( + InvalidAmountOperationError.CurrencyMismatch(left, right)); } - return Outcome.Success(new Amount(a1.Value + a2.Value, a1.Currency)); + return Outcome.Success( + new Amount(left.Value + right.Value, left.Currency)); } ``` -Note: `Failure(...)` takes an **`Error`** — the factory returns one directly, so no exception is involved. +These two usages do not define two different errors. They transport the same error situation in different ways. -You can inspect: +## 6. Generate the catalog -```csharp -if (result.IsFailure) { - Log(result.Error); -} -``` +Build the solution, then generate a Markdown catalog: -Or escalate: +```bash +dotnet build MyApp.sln -c Release -```csharp -var amount = result.GetResultOrThrow(); +fce generate \ + --solution MyApp.sln \ + --configuration Release \ + --no-build \ + --format markdown \ + --service-name my-api \ + --output artifacts/errors.md ``` -## 6ïžâƒŁ Generate documentation +`--configuration Release` matches the build above: with `--no-build`, the generator reads the assemblies produced by that exact configuration (it defaults to `Debug` otherwise). -Because factories are linked to structured documentation: +The generated document contains an entry for `AMOUNT_CURRENCY_MISMATCH`, including its description, rule, diagnostic hypotheses, and the messages produced by the example factory. -* errors can be extracted from assemblies -* documentation can be generated automatically -* support and developers share the same source of truth +A shortened result looks like this: -## ✅ What you gain +```markdown +## Amount currency mismatch -With FirstClassErrors: +**Code:** `AMOUNT_CURRENCY_MISMATCH` -* errors are consistent -* documentation is close to the code -* diagnostics guide troubleshooting -* knowledge does not drift +This error occurs when an operation combines amounts expressed in different currencies. -You move from: +**Rule:** A monetary operation must use one common currency. +``` -> “An exception happened” +Commit or publish this generated catalog according to your delivery workflow. For automatic generation in CI, see [CI/CD and Operational Integration](OperationalIntegration.en.md). -to +## Optional next steps -> “This specific, documented error occurred, here is what it means and where to look.” +- Add occurrence-specific facts with [Error Context](ErrorContext.en.md). +- Understand the available error categories in [Error Taxonomy and Composition](ErrorTaxonomy.en.md). +- Learn when to throw or return an `Outcome` in [Usage Patterns](UsagePatterns.en.md). +- Protect stable codes and context keys with [Catalog Versioning](CatalogVersioning.en.md). --- ---- \ No newline at end of file +--- diff --git a/doc/GettingStarted.fr.md b/doc/GettingStarted.fr.md index 3e6a609..71ee218 100644 --- a/doc/GettingStarted.fr.md +++ b/doc/GettingStarted.fr.md @@ -1,194 +1,184 @@ -# Premier pas +# Premiers pas -🌍 **Langues:** +🌍 **Langues :** 🇬🇧 [English](./GettingStarted.en.md) | đŸ‡«đŸ‡· Français (ce fichier) -FirstClassErrors vous aide Ă  considĂ©rer les erreurs comme une **connaissance structurĂ©e et diagnostiquable**, plutĂŽt que comme de simples messages d’exception. +Ce guide vous mĂšne d’un projet vide Ă  votre premier catalogue d’erreurs gĂ©nĂ©rĂ©. -En quelques minutes, vous allez voir comment : +Vous allez : -* dĂ©finir une erreur (avec une factory) -* utiliser des factories d’erreur (indispensables pour la documentation vivante) -* documenter l’erreur de maniĂšre structurĂ©e -* attacher des diagnostics -* utiliser Ă©ventuellement l’erreur sans la lever +1. installer la bibliothĂšque et le CLI ; +2. activer la gĂ©nĂ©ration pour un projet ; +3. dĂ©finir une erreur documentĂ©e ; +4. l’utiliser dans le code ; +5. gĂ©nĂ©rer le catalogue. -## 1ïžâƒŁ DĂ©finir une erreur (avec une factory) +## 1. Installer le package et le CLI -Pour bĂ©nĂ©ficier de la **documentation vivante**, les erreurs ne sont pas créées directement avec `new`. Elles sont créées via des **mĂ©thodes factory statiques** dans la classe d’erreur. +Dans le projet applicatif : -Ce pattern est essentiel car : +```bash +dotnet add package FirstClassErrors +``` + +Installez une fois le CLI de documentation sur votre machine : + +```bash +dotnet tool install --global FirstClassErrors.Cli +``` -* chaque mĂ©thode factory reprĂ©sente une **situation d’erreur spĂ©cifique** -* c’est le point d’ancrage du DSL de documentation -* le gĂ©nĂ©rateur de documentation relie les factories Ă  la documentation +## 2. Activer la gĂ©nĂ©ration pour le projet -Remarque : +Ajoutez cette propriĂ©tĂ© directement dans le fichier du projet qui contient vos erreurs : -*L’utilisation de mĂ©thodes factory pour crĂ©er des erreurs est un pattern .NET bien Ă©tabli pour centraliser et standardiser la crĂ©ation d’erreurs. FirstClassErrors s’appuie sur cette idĂ©e et fait des factories le point d’ancrage de la documentation d’erreurs structurĂ©e et vivante. Au-delĂ  de la documentation, les factories amĂ©liorent fortement la lisibilitĂ© du code : elles sortent la construction de l’erreur (codes, messages, formatage, formulation) du “happy path”, ce qui permet Ă  la logique mĂ©tier de rester centrĂ©e sur les rĂšgles mĂ©tier plutĂŽt que sur des dĂ©tails techniques. Un appel comme `throw InvalidAmountOperationError.CurrencyMismatch(a1, a2).ToException();` exprime l’intention bien plus clairement qu’une construction d’erreur inline. Cette approche s’aligne avec les principes du clean code en sĂ©parant les responsabilitĂ©s, en rĂ©duisant la duplication et en donnant Ă  chaque situation d’erreur une reprĂ©sentation explicite et nommĂ©e dans le code — tout en fournissant un point unique et cohĂ©rent pour attacher diagnostics et documentation.* +```xml + + true + +``` + +Le marqueur doit ĂȘtre prĂ©sent dans le `.csproj` lui-mĂȘme. Les projets qui ne le dĂ©clarent pas sont ignorĂ©s lorsque le CLI analyse une solution. -Exemple : +## 3. DĂ©finir une situation d’erreur + +CrĂ©ez une classe factory statique. Chaque mĂ©thode factory reprĂ©sente une situation prĂ©cise reconnue par le systĂšme. Les exemples utilisent un petit type valeur `Amount` dotĂ© d’une `Currency` ; remplacez-le par n’importe quel type de votre propre domaine. ```csharp +using FirstClassErrors; + [ProvidesErrorsFor(nameof(Amount))] public static class InvalidAmountOperationError { [DocumentedBy(nameof(CurrencyMismatchDocumentation))] - internal static DomainError CurrencyMismatch(Amount amount1, Amount amount2) { + internal static DomainError CurrencyMismatch(Amount left, Amount right) { return DomainError.Create( Code.CurrencyMismatch, - diagnosticMessage: $"Impossible d’effectuer l’opĂ©ration monĂ©taire car les montants impliquĂ©s sont exprimĂ©s dans des devises diffĂ©rentes : {amount1} et {amount2}.") + diagnosticMessage: $"Impossible d’additionner {left} et {right} car leurs devises diffĂšrent.") .WithPublicMessage( - shortMessage: "Devise diffĂ©rente", - detailedMessage: "Les montants impliquĂ©s dans l’opĂ©ration sont exprimĂ©s dans des devises diffĂ©rentes."); + shortMessage: "Les montants utilisent des devises diffĂ©rentes.", + detailedMessage: "Les deux montants doivent utiliser la mĂȘme devise."); } private static class Code { - public static readonly ErrorCode CurrencyMismatch = ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"); + public static readonly ErrorCode CurrencyMismatch = + ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"); } - } -```` - -Ici : - -* Le **type d’erreur** reprĂ©sente une catĂ©gorie d’erreurs mĂ©tier. -* La **mĂ©thode factory** reprĂ©sente un cas d’erreur prĂ©cis. -* Le **code d’erreur** est stable et lisible par machine. -* C’est la mĂ©thode factory qui sera documentĂ©e. +``` -L’erreur est construite en deux Ă©tapes : `DomainError.Create(...)` capture l’information interne obligatoire (le `code` d’erreur et le `diagnosticMessage` interne), et `.WithPublicMessage(shortMessage, detailedMessage)` fournit les messages publics et produit l’erreur finale. Le `shortMessage` obligatoire est un rĂ©sumĂ© public sĂ»r, le `detailedMessage` optionnel est un dĂ©tail public maĂźtrisĂ©, et le `diagnosticMessage` est destinĂ© aux logs, au support et aux dĂ©veloppeurs — jamais exposĂ© aux clients par dĂ©faut. Il n’y a pas de `.Build()`, et les constructeurs publics sont internes, de sorte qu’une erreur ne peut jamais rester sans son message public. +Quatre Ă©lĂ©ments sont essentiels : -Vous ne faites jamais `new` sur l’erreur ni sur l’exception vous-mĂȘme : l’erreur est créée via le builder Ă©tagĂ© dans la factory, et pour lever, vous appelez `error.ToException()` (voir section 4). +- `[ProvidesErrorsFor]` relie la classe factory au concept dont elle dĂ©clare les erreurs ; le gĂ©nĂ©rateur s’en sert pour les dĂ©couvrir et les regrouper ; +- le nom de la factory exprime la situation dans le code ; +- le code d’erreur constitue son identitĂ© stable et lisible par machine ; +- le message de diagnostic est interne, tandis que les messages publics sont sĂ»rs pour les appelants. -## 2ïžâƒŁ Lier la factory Ă  une documentation structurĂ©e +## 4. Ajouter la documentation structurĂ©e -Chaque mĂ©thode factory est liĂ©e Ă  sa documentation via `[DocumentedBy]`. +L’attribut `[DocumentedBy]` relie la factory Ă  une mĂ©thode de documentation situĂ©e dans la mĂȘme classe : ```csharp private static ErrorDocumentation CurrencyMismatchDocumentation() { - return DescribeError.WithTitle("IncohĂ©rence de devise des montants") - .WithDescription("Cette erreur survient lorsqu’on tente d’utiliser plusieurs montants dans une opĂ©ration alors qu’ils sont exprimĂ©s dans des devises diffĂ©rentes.") - .WithRule("Toutes les opĂ©rations monĂ©taires doivent impliquer des montants exprimĂ©s dans la mĂȘme devise.") + return DescribeError.WithTitle("IncohĂ©rence de devise") + .WithDescription( + "Cette erreur survient lorsqu’une opĂ©ration combine des montants exprimĂ©s dans des devises diffĂ©rentes.") + .WithRule( + "Une opĂ©ration monĂ©taire doit utiliser une devise commune.") .WithDiagnostic( - "Des montants ont Ă©tĂ© utilisĂ©s dans une opĂ©ration monĂ©taire sans avoir Ă©tĂ© convertis dans une devise commune.", + "Les montants ont atteint l’opĂ©ration sans avoir Ă©tĂ© convertis dans une devise commune.", ErrorOrigin.Internal, - "VĂ©rifiez si tous les montants impliquĂ©s ont Ă©tĂ© convertis dans une devise commune avant d’ĂȘtre utilisĂ©s ensemble." - ) + "VĂ©rifiez Ă  quel endroit les montants auraient dĂ» ĂȘtre convertis avant cette opĂ©ration.") .AndDiagnostic( - "Des montants censĂ©s ĂȘtre exprimĂ©s dans la mĂȘme devise ont Ă©tĂ© fournis avec des devises diffĂ©rentes.", - ErrorOrigin.InternalOrExternal, - "VĂ©rifiez les devises associĂ©es Ă  chaque montant et confirmez si une devise commune Ă©tait attendue pour cette opĂ©ration." - ) - .WithExamples(() => CurrencyMismatch(new Amount(127.33m, Currency.EUR), new Amount(57689.00m, Currency.USD))); + "L’appelant a fourni des montants dans des devises qui ne peuvent pas ĂȘtre combinĂ©es directement.", + ErrorOrigin.External, + "VĂ©rifiez les devises fournies par l’appelant.") + .WithExamples(() => CurrencyMismatch( + new Amount(10m, Currency.EUR), + new Amount(12m, Currency.USD))); } ``` -Chaque diagnostic dĂ©clare une **origine** via `ErrorOrigin`, dont les valeurs sont `Internal`, `External` et `InternalOrExternal` — indiquant si la cause se situe Ă  l’intĂ©rieur du systĂšme, Ă  l’extĂ©rieur (entrĂ©e) ou peut ĂȘtre l’une ou l’autre. +Il s’agit de connaissance structurĂ©e, pas d’un commentaire : le gĂ©nĂ©rateur peut extraire le titre, l’explication, la rĂšgle, les hypothĂšses de diagnostic et l’exemple rĂ©ellement produit par la factory. -Cette documentation : +Chaque diagnostic est une hypothĂšse : une cause plausible, une origine (le soupçon porte-t-il sur l’intĂ©rieur du systĂšme ou sur un appelant externe ?) et une piste d’investigation. -* explique la signification de l’erreur -* Ă©nonce la rĂšgle violĂ©e -* fournit des hypothĂšses de diagnostic -* donne des exemples rĂ©alistes de messages +## 5. Utiliser l’erreur -Il s’agit de connaissance structurĂ©e, pas d’un commentaire. - -## 3ïžâƒŁ Ajouter un contexte d’erreur structurĂ© (`ErrorContext`) - -Quand une information est utile pour diagnostiquer **une occurrence prĂ©cise**, ajoutez-la dans le contexte. - -```csharp -return PrimaryPortError.Create( - Code.DateOutOfStatementPeriod, - diagnosticMessage: $"Transaction datĂ©e du {transactionDate} hors pĂ©riode [{periodStart};{periodEnd}].", - transience: Transience.NonTransient, - configureContext: ctx => ctx.Add(ErrCtxKey.TransactionDate, transactionDate)) - .WithPublicMessage( - shortMessage: "Date de transaction hors pĂ©riode.", - detailedMessage: "La date de transaction se situe hors de la pĂ©riode du relevĂ© autorisĂ©e."); -``` - -Le contexte est portĂ© par l’`Error` ; lorsqu’une exception est ensuite produite avec `error.ToException()`, on y accĂšde via `exception.Error.Context`. - -Bonnes pratiques : - -* utilisez des clĂ©s nommĂ©es et stables (`ErrorContextKey`) -* ajoutez le contexte au niveau des factories -* Ă©vitez les donnĂ©es sensibles ou trop volumineuses - -## 4ïžâƒŁ Utiliser l’exception dans le code mĂ©tier +Lorsque l’échec est exceptionnel, transformez l’erreur en son exception associĂ©e : ```csharp public Amount Add(Amount other) { - if (Currency != other.Currency) { throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } + if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); + } return new Amount(Value + other.Value, Currency); } ``` -La logique mĂ©tier reste propre et expressive. +Le code mĂ©tier nomme la situation sans rĂ©pĂ©ter les codes ni les messages. -## 5ïžâƒŁ Ou l’utiliser sans lever d’exception (`Outcome`) - -Pour les scĂ©narios de validation ou de traitement par lots : +Lorsque l’échec fait normalement partie du flux, transportez la mĂȘme `Error` sans la lever : ```csharp -public static Outcome TryAdd(Amount a1, Amount a2) { - if (a1.Currency != a2.Currency) { - return Outcome.Failure(InvalidAmountOperationError.CurrencyMismatch(a1, a2)); +public static Outcome TryAdd(Amount left, Amount right) { + if (left.Currency != right.Currency) { + return Outcome.Failure( + InvalidAmountOperationError.CurrencyMismatch(left, right)); } - return Outcome.Success(new Amount(a1.Value + a2.Value, a1.Currency)); + return Outcome.Success( + new Amount(left.Value + right.Value, left.Currency)); } ``` -Remarque : `Failure(...)` prend une **`Error`** — la factory en renvoie une directement, donc aucune exception n’est impliquĂ©e. +Ces deux usages ne dĂ©finissent pas deux erreurs diffĂ©rentes. Ils transportent la mĂȘme situation d’erreur de deux maniĂšres diffĂ©rentes. -Vous pouvez inspecter : +## 6. GĂ©nĂ©rer le catalogue -```csharp -if (result.IsFailure) { - Log(result.Error); -} -``` +Construisez la solution, puis gĂ©nĂ©rez un catalogue Markdown : -Ou escalader : +```bash +dotnet build MyApp.sln -c Release -```csharp -var amount = result.GetResultOrThrow(); +fce generate \ + --solution MyApp.sln \ + --configuration Release \ + --no-build \ + --format markdown \ + --service-name my-api \ + --output artifacts/errors.md ``` -## 6ïžâƒŁ GĂ©nĂ©rer la documentation +`--configuration Release` correspond au build ci-dessus : avec `--no-build`, le gĂ©nĂ©rateur lit les assemblies produits par cette configuration prĂ©cise (Ă  dĂ©faut, il utilise `Debug`). -Comme les factories sont liĂ©es Ă  une documentation structurĂ©e : +Le document gĂ©nĂ©rĂ© contient une entrĂ©e pour `AMOUNT_CURRENCY_MISMATCH`, avec sa description, sa rĂšgle, ses hypothĂšses de diagnostic et les messages produits par l’exemple. -* les erreurs peuvent ĂȘtre extraites des assemblies -* la documentation peut ĂȘtre gĂ©nĂ©rĂ©e automatiquement -* le support et les dĂ©veloppeurs partagent la mĂȘme source de vĂ©ritĂ© +Un rĂ©sultat abrĂ©gĂ© ressemble Ă  ceci : -## ✅ Ce que vous y gagnez +```markdown +## IncohĂ©rence de devise -Avec FirstClassErrors : +**Code :** `AMOUNT_CURRENCY_MISMATCH` -* les erreurs sont cohĂ©rentes -* la documentation est proche du code -* les diagnostics guident le dĂ©pannage -* la connaissance ne dĂ©rive pas +Cette erreur survient lorsqu’une opĂ©ration combine des montants exprimĂ©s dans des devises diffĂ©rentes. -Vous passez de : +**RĂšgle :** Une opĂ©ration monĂ©taire doit utiliser une devise commune. +``` -> « Une exception s’est produite » +Commitez ou publiez ce catalogue selon votre workflow de livraison. Pour automatiser sa gĂ©nĂ©ration en CI, consultez [IntĂ©gration CI/CD et exploitation](OperationalIntegration.fr.md). -Ă  +## Étapes suivantes facultatives -> « Cette erreur prĂ©cise et documentĂ©e s’est produite, voici ce qu’elle signifie et oĂč chercher. » +- Ajoutez des faits propres Ă  chaque occurrence avec le [contexte d’erreur](ErrorContext.fr.md). +- DĂ©couvrez les catĂ©gories disponibles dans [Taxonomie et composition des erreurs](ErrorTaxonomy.fr.md). +- Apprenez quand lever ou retourner un `Outcome` dans les [cas d’usage](UsagePatterns.fr.md). +- ProtĂ©gez les codes et clĂ©s de contexte stables avec le [versionnage du catalogue](CatalogVersioning.fr.md). --- ---- \ No newline at end of file +--- diff --git a/doc/README.fr.md b/doc/README.fr.md index 759e5e8..1903a79 100644 --- a/doc/README.fr.md +++ b/doc/README.fr.md @@ -1,15 +1,15 @@ # FirstClassErrors -🌍 **Langues:** +🌍 **Langues :** 🇬🇧 [English](../README.md) | đŸ‡«đŸ‡· Français (ce fichier) | | | | :-- | :-- | -| **Build** | [![ci](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml) | -| **QualitĂ©** | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) [![Couverture](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=coverage)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) | +| **Build** | [![ci](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/ci.yml) | +| **QualitĂ©** | [![Quality Gate](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) [![Couverture](https://sonarcloud.io/api/project_badges/measure?project=reefact_first-class-errors&metric=coverage)](https://sonarcloud.io/summary/new_code?id=reefact_first-class-errors) | | **SĂ©curitĂ©** | [![codeql](https://github.com/Reefact/first-class-errors/actions/workflows/codeql.yml/badge.svg?branch=main)](https://github.com/Reefact/first-class-errors/actions/workflows/codeql.yml) [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/13567/badge)](https://www.bestpractices.dev/projects/13567) [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/Reefact/first-class-errors/badge)](https://securityscorecards.dev/viewer/?uri=github.com/Reefact/first-class-errors) | -| **Package** | [![NuGet](https://img.shields.io/nuget/vpre/FirstClassErrors?logo=nuget)](https://www.nuget.org/packages/FirstClassErrors) ![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4) | -| **Projet** | [![License](https://img.shields.io/github/license/Reefact/first-class-errors)](../LICENSE) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org) | +| **Package** | [![NuGet](https://img.shields.io/nuget/vpre/FirstClassErrors?logo=nuget)](https://www.nuget.org/packages/FirstClassErrors) ![.NET Standard 2.0](https://img.shields.io/badge/.NET%20Standard-2.0-512BD4) | +| **Projet** | [![License](https://img.shields.io/github/license/Reefact/first-class-errors)](../LICENSE) [![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-fe5196?logo=conventionalcommits&logoColor=white)](https://www.conventionalcommits.org) | --- @@ -17,245 +17,166 @@ ![FirstClassErrors](./images/first-class-errors.png "FirstClassErrors") -FirstClassErrors est une bibliothĂšque .NET qui considĂšre les erreurs comme des concepts de premier ordre, documentĂ©s et diagnostiquables — pas seulement comme des chaĂźnes de caractĂšres lancĂ©es Ă  l’exĂ©cution. +## đŸ§© Un seul modĂšle pour toute la chaĂźne -Elle vous aide Ă  : +Une erreur applicative se rĂ©duit trop souvent Ă  un code ou une chaĂźne de caractĂšres, sans rien qui en explique le sens. FirstClassErrors en fait un concept de premiĂšre classe de votre domaine. Vous dĂ©crivez chaque situation d’erreur une seule fois, avec ses rĂšgles, Ă  un seul endroit du code ; toute la bibliothĂšque s’appuie ensuite sur ce modĂšle. -* exprimer les erreurs de maniĂšre cohĂ©rente et structurĂ©e -* associer des diagnostics utiles Ă  chaque erreur -* garder la documentation des erreurs proche du code -* gĂ©nĂ©rer automatiquement une documentation humaine des erreurs +**ModĂ©liser l’erreur** +Une erreur devient un objet qui porte son propre sens — qu’il s’agisse d’une rĂšgle mĂ©tier violĂ©e, d’un appel entrant rejetĂ© ou d’une panne d’un service externe, passagĂšre ou dĂ©finitive. -## 🚹 Le problĂšme - -Dans la plupart des systĂšmes, les erreurs sont : - -* dispersĂ©es dans le code -* dĂ©crites par des messages ad hoc -* peu ou pas documentĂ©es -* difficiles Ă  analyser -* dĂ©connectĂ©es du support et des opĂ©rations - -Avec le temps, cela entraĂźne : - -* des investigations rĂ©pĂ©tĂ©es -* du savoir implicite (“tribal knowledge”) -* des Ă©quipes support qui devinent -* des dĂ©veloppeurs qui rĂ©expliquent sans cesse les mĂȘmes erreurs - -## 💡 L’idĂ©e - -Et si : - -> **Chaque erreur de votre systĂšme Ă©tait explicitement dĂ©crite, structurĂ©e et documentĂ©e — directement dans le code — et que cette documentation pouvait ĂȘtre gĂ©nĂ©rĂ©e automatiquement ?** - -FirstClassErrors introduit : - -* un **modĂšle d’erreur enrichi** -* un **systĂšme de diagnostics structurĂ©s** -* un **DSL pour documenter les erreurs** -* un **pipeline d’extraction de documentation** - -Les erreurs deviennent : - -> non seulement des Ă©checs, -> mais des **unitĂ©s de connaissance documentĂ©es**. - -## đŸ§± Ce que fournit la bibliothĂšque - -### 1ïžâƒŁ Un modĂšle d’erreur plus riche - -Les erreurs portent : +**GĂ©rer l’échec** +À vous de choisir comment elle voyage : levĂ©e comme une exception classique, ou renvoyĂ©e comme un rĂ©sultat explicite que l’appelant inspecte sans try/catch. -* un code d’erreur stable -* un horodatage -* trois messages distincts — un rĂ©sumĂ© public court (obligatoire), un dĂ©tail public optionnel et un message de diagnostic interne (obligatoire) -* des donnĂ©es de contexte -* des diagnostics structurĂ©s +**Documenter l’erreur** +Chaque erreur peut porter, dans le code, son explication, sa rĂšgle et ses causes probables. FirstClassErrors en gĂ©nĂšre un catalogue lisible, extrait de ce mĂȘme code — il ne dĂ©rive donc jamais. -L’exception, elle, n’expose que son `.Error`, et est conçue pour ĂȘtre : +**VĂ©rifier le modĂšle** +Des analyseurs Roslyn contrĂŽlent ces rĂšgles dĂšs la compilation : un code d’erreur dupliquĂ© ou une documentation incohĂ©rente deviennent des erreurs de build, dĂ©tectĂ©es avant la production. -* loguĂ©es de maniĂšre cohĂ©rente -* comprises par des humains -* exploitĂ©es par des outils +**Lier les entrĂ©es externes** +Un formulaire ou une requĂȘte JSON doit devenir un objet mĂ©tier valide. LĂ  oĂč le binding .NET classique redĂ©clare ces rĂšgles sĂ©parĂ©ment, dans un validateur ou des attributs, FirstClassErrors lie ces entrĂ©es directement Ă  vos objets valeur, en rĂ©utilisant leurs propres rĂšgles de construction au lieu de les dupliquer. -#### Trois messages, trois publics - -Une `Error` sĂ©pare dĂ©libĂ©rĂ©ment ce qui peut ĂȘtre montrĂ© Ă  un appelant de ce qui est destinĂ© aux dĂ©veloppeurs et au support : - -| Message | Obligatoire | Public | Exposition | -| --- | --- | --- | --- | -| `ShortMessage` | Oui | Utilisateurs finaux / clients d’API | Un rĂ©sumĂ© public court, exposable tel quel (par ex. le `title` d’un problem detail RFC 9457). | -| `DetailedMessage` | Non | Utilisateurs finaux / clients d’API | Un dĂ©tail public maĂźtrisĂ© (par ex. le `detail` d’un problem detail RFC 9457), exposĂ© **uniquement** si l’application le dĂ©cide explicitement. Ne doit contenir aucune information sensible ou interne. | -| `DiagnosticMessage` | Oui | Logs, support, dĂ©veloppeurs | Le message de diagnostic interne. Il peut contenir des dĂ©tails techniques/opĂ©rationnels (identifiants, valeurs fautives, Ă©tat interne) ; il n’est **jamais** exposĂ© aux clients externes par dĂ©faut. | - -Le cƓur du modĂšle est agnostique vis-Ă -vis d’HTTP : `DiagnosticMessage` n’est jamais utilisĂ© comme corps de rĂ©ponse HTTP par dĂ©faut, et `type` et `status` restent l’affaire de l’application. Lorsqu’une erreur est exposĂ©e en HTTP, son `Code` est le `type` RFC 9457 naturel : exposez-le comme un URI stable tel que `urn:problem:{service}:{code}`, oĂč `{code}` est le code d’erreur en minuscules et en kebab-case — par exemple `urn:problem:temperature-simulator:temperature-below-absolute-zero` ou `urn:problem:banking-api:money-transfer-amount-not-positive` — afin que les clients puissent aiguiller sur le type de problĂšme. Lorsque vous transformez une erreur en exception avec `.ToException()`, le `Exception.Message` obtenu est le `DiagnosticMessage` — le texte destinĂ© aux dĂ©veloppeurs et aux logs. - -### 2ïžâƒŁ Des diagnostics structurĂ©s - -Chaque erreur peut dĂ©clarer des **causes possibles** et des **pistes d’analyse** : - -* Qu’est-ce qui a pu provoquer cette erreur ? -* Est-elle plutĂŽt liĂ©e aux donnĂ©es d’entrĂ©e, au systĂšme, ou aux deux ? -* Par oĂč commencer l’investigation ? - -Les diagnostics orientent l’analyse sans figer les processus opĂ©rationnels. - -### 3ïžâƒŁ Un DSL pour dĂ©crire les erreurs +## 🚹 Le problĂšme -Les erreurs sont documentĂ©es directement dans le code via une API fluide : +Une erreur de production est rarement utile lorsqu’elle se rĂ©sume Ă  un type et une chaĂźne de caractĂšres : -```csharp -return DescribeError.WithTitle("Temperature below absolute zero") - .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.") - .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.") - .WithDiagnostics(ValueObjectDiagnostic.Diagnostic) - .WithExamples( - () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin), - () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius)); +```text +OpĂ©ration invalide. ``` -Il ne s’agit pas de simples commentaires — c’est de la **documentation structurĂ©e et exĂ©cutable**. - -### 4ïžâƒŁ Extraction de la documentation - -La bibliothĂšque fournit un mĂ©canisme pour analyser les assemblies et extraire toute la documentation d’erreurs dĂ©clarĂ©e : - -* liĂ©e aux classes de factories d’erreur -* liĂ©e aux mĂ©thodes factory -* enrichie par des exemples -* prĂȘte Ă  ĂȘtre rendue - -Cela permet de gĂ©nĂ©rer : - -* des catalogues d’erreurs en Markdown ou JSON (ou tout format personnalisĂ© via un renderer) -* de la documentation orientĂ©e support -* une documentation vivante gĂ©nĂ©rĂ©e depuis le code -* des catalogues multilingues (optionnel) — voir [Internationalisation](Internationalisation.fr.md) +Les dĂ©veloppeurs et le support doivent encore dĂ©couvrir : -## 🔁 Exception ou pas ? À vous de choisir. +- quelle situation s’est rĂ©ellement produite ; +- quelle rĂšgle a Ă©tĂ© violĂ©e ; +- quels faits appartiennent Ă  cette occurrence ; +- ce qui a pu la provoquer ; +- par oĂč commencer l’investigation. -La bibliothĂšque supporte Ă  la fois : +Lorsque cette connaissance est rĂ©partie entre les logs, les tickets, les commentaires et la mĂ©moire des personnes, elle dĂ©rive du code. -* **les erreurs levĂ©es** (flux classique par exceptions) -* **les erreurs transportĂ©es sans ĂȘtre levĂ©es** via `Outcome` et `Outcome` +## 💡 L’approche FirstClassErrors -Cela vous permet de traiter la mĂȘme erreur, au choix : - -> comme un signal d’exĂ©cution que vous levez -> ou comme une donnĂ©e structurĂ©e que vous transportez - -selon le contexte (domaine, validation, pipelines, etc.). - -## 📩 Installation - -```bash -dotnet add package FirstClassErrors -``` - -Cible **.NET Standard 2.0**. Les analyseurs Roslyn sont intĂ©grĂ©s au package — aucune installation sĂ©parĂ©e. - -## đŸ§© Exemple - -Extrait du projet `FirstClassErrors.Usage` : +Une factory donne une identitĂ© stable Ă  la situation d’erreur, centralise sa construction et y attache une documentation structurĂ©e qui alimentera un catalogue gĂ©nĂ©rĂ©, lisible par des humains : ```csharp -[ProvidesErrorsFor(nameof(Temperature))] -public static class InvalidTemperatureError { +[ProvidesErrorsFor(nameof(Amount))] +public static class InvalidAmountOperationError { - [DocumentedBy(nameof(BelowAbsoluteZeroDocumentation))] - internal static DomainError BelowAbsoluteZero(decimal invalidValue, TemperatureUnit invalidValueUnit) { + [DocumentedBy(nameof(CurrencyMismatchDocumentation))] + internal static DomainError CurrencyMismatch(Amount left, Amount right) { return DomainError.Create( - Code.TemperatureBelowAbsoluteZero, - diagnosticMessage: $"Failed to instantiate temperature: the value {invalidValue} {invalidValueUnit} is below absolute zero.") + ErrorCode.Create("AMOUNT_CURRENCY_MISMATCH"), + diagnosticMessage: $"Impossible d’additionner {left} et {right} car leurs devises diffĂšrent.") .WithPublicMessage( - shortMessage: "Temperature is below absolute zero.", - detailedMessage: "The provided temperature is below absolute zero, which is not physically valid."); + shortMessage: "Les montants utilisent des devises diffĂ©rentes.", + detailedMessage: "Les deux montants doivent utiliser la mĂȘme devise."); } - private static ErrorDocumentation BelowAbsoluteZeroDocumentation() { - return DescribeError.WithTitle("Temperature below absolute zero") - .WithDescription("This error occurs when trying to instantiate a temperature with a value that is below absolute zero.") - .WithRule("Temperature cannot go below absolute zero because absolute zero is the point where particles have minimum possible energy.") - .WithDiagnostics(ValueObjectDiagnostic.Diagnostic) - .WithExamples( - () => BelowAbsoluteZero(-1, TemperatureUnit.Kelvin), - () => BelowAbsoluteZero(-280, TemperatureUnit.Celsius)); + private static ErrorDocumentation CurrencyMismatchDocumentation() { + return DescribeError.WithTitle("IncohĂ©rence de devise") + .WithDescription("Cette erreur survient lorsqu’une opĂ©ration combine des montants exprimĂ©s dans des devises diffĂ©rentes.") + .WithRule("Une opĂ©ration monĂ©taire doit utiliser une devise commune.") + .WithDiagnostic( + "Les montants ont atteint l’opĂ©ration sans avoir Ă©tĂ© convertis dans une devise commune.", + ErrorOrigin.Internal, + "VĂ©rifiez Ă  quel endroit les montants auraient dĂ» ĂȘtre convertis avant cette opĂ©ration.") + .WithExamples(() => CurrencyMismatch( + new Amount(10, Currency.EUR), + new Amount(12, Currency.USD))); } +} +``` - private static class Code { - public static readonly ErrorCode TemperatureBelowAbsoluteZero = ErrorCode.Create("TEMPERATURE_BELOW_ABSOLUTE_ZERO"); - } +Le code mĂ©tier reste centrĂ© sur l’intention : + +```csharp +if (Currency != other.Currency) { + throw InvalidAmountOperationError.CurrencyMismatch(this, other).ToException(); } ``` -La factory retourne une `Error` structurĂ©e. Lorsque vous devez la lever, vous la transformez en exception avec `.ToException()` : +La factory retourne une `Error` (`DomainError` en est une des catĂ©gories), donc un Ă©chec attendu peut employer le mĂȘme modĂšle sans lever d’exception : ```csharp -throw InvalidTemperatureError.BelowAbsoluteZero(-1, TemperatureUnit.Kelvin).ToException(); +return Outcome.Failure( + InvalidAmountOperationError.CurrencyMismatch(left, right)); ``` -Ici, l’erreur, sa signification, sa rĂšgle, ses diagnostics et des exemples de messages sont dĂ©finis ensemble — dans le code. +À partir de ces factories, FirstClassErrors peut gĂ©nĂ©rer un catalogue lisible par les dĂ©veloppeurs, le support et l’exploitation. -## 🎯 Pour qui ? +## 📩 Installation -FirstClassErrors est particuliĂšrement utile si : +```bash +dotnet add package FirstClassErrors +``` -* vous dĂ©veloppez des systĂšmes mĂ©tiers complexes -* vous vous souciez de la supportabilitĂ© -* vous voulez une gestion d’erreurs cohĂ©rente -* vous souhaitez une documentation qui ne dĂ©rive pas du code -* vous concevez avec une approche orientĂ©e domaine +Le package cible **.NET Standard 2.0**. Les analyseurs Roslyn sont inclus automatiquement ; aucun package d’analyse supplĂ©mentaire n’est nĂ©cessaire. -## 🔍 Analyseurs +Pour gĂ©nĂ©rer la documentation, installez le CLI : -FirstClassErrors est livrĂ© avec un ensemble d’analyseurs Roslyn (identifiants de rĂšgle `FCExxx`) **inclus dans le package NuGet** — rĂ©fĂ©rencez le package et ils s’exĂ©cutent au build, sans installation supplĂ©mentaire. Ils dĂ©tectent, avant toute exĂ©cution, les erreurs que le runtime ou le pipeline de documentation ne feraient sinon apparaĂźtre que tard, voire silencieusement : codes d’erreur dupliquĂ©s, rĂ©fĂ©rences `[DocumentedBy]` qui ne rĂ©solvent pas, erreurs documentĂ©es qui n’atteignent jamais le catalogue, et plus encore. +```bash +dotnet tool install --global FirstClassErrors.Cli +``` -> **PrĂ©requis compilateur :** les analyseurs inclus sont compilĂ©s contre Roslyn 4.8 ; ils se chargent donc dans tout hĂŽte Ă  partir du **SDK .NET 8 / Visual Studio 2022 17.8**. Les SDK/IDE plus anciens ne peuvent pas les charger. +Suivez ensuite le guide [Premiers pas](GettingStarted.fr.md) pour crĂ©er et gĂ©nĂ©rer votre premiĂšre erreur documentĂ©e. -Voir la [rĂ©fĂ©rence des rĂšgles d’analyse](analyzers/README.fr.md). +## 🎯 Quand la bibliothĂšque est adaptĂ©e -## 🔐 ChaĂźne d’approvisionnement +FirstClassErrors est particuliĂšrement utile dans du code applicatif ou mĂ©tier durable lorsque : -Chaque package publiĂ© est construit et poussĂ© par [`release.yml`](../.github/workflows/release.yml) avec deux garanties vĂ©rifiables : +- les erreurs reprĂ©sentent des rĂšgles, des contraintes ou des dĂ©faillances de frontiĂšre ; +- plusieurs Ă©quipes ou systĂšmes dĂ©pendent de codes d’erreur stables ; +- le support et l’exploitation investiguent des incidents de production ; +- la documentation doit rester alignĂ©e avec le comportement. -- **Provenance de build signĂ©e** ([SLSA](https://slsa.dev)) — chaque package est attestĂ© au moment du build, liant son empreinte Ă  ce dĂ©pĂŽt, au commit exact et Ă  l’exĂ©cution du workflow. Les octets attestĂ©s sont attachĂ©s Ă  la [GitHub Release](https://github.com/Reefact/first-class-errors/releases) correspondante ; tĂ©lĂ©chargez-y le `.nupkg` et vĂ©rifiez avec : +Pour un prototype, un petit utilitaire ou du code technique bas niveau, les exceptions standards peuvent suffire. Consultez [Quand ne pas utiliser FirstClassErrors](WhenNotToUseFirstClassErrors.fr.md). - ```bash - gh attestation verify FirstClassErrors..nupkg --repo Reefact/first-class-errors - ``` +## 🔍 Analyseurs et chaĂźne d’approvisionnement - (nuget.org sert une copie signĂ©e par le dĂ©pĂŽt, d’empreinte diffĂ©rente : vĂ©rifiez **celle-ci** plutĂŽt avec `dotnet nuget verify`.) +Le package contient des rĂšgles Roslyn aux identifiants stables `FCExxx`. Elles dĂ©tectent notamment les codes dupliquĂ©s ou mal formĂ©s, les liaisons de documentation invalides, l’absence d’exemples et certains mauvais usages de l’API. Consultez la [rĂ©fĂ©rence des analyseurs](analyzers/README.fr.md). -- **SBOM embarquĂ©** — chaque package contient son inventaire logiciel SPDX (*software bill of materials*) Ă  `_manifest/spdx_2.2/manifest.spdx.json`, recensant les fichiers livrĂ©s et les composants tiers utilisĂ©s pour le construire. +Les packages publiĂ©s contiennent une provenance de build signĂ©e et un SBOM SPDX embarquĂ©. Les informations de vĂ©rification sont disponibles dans [SECURITY.fr.md](SECURITY.fr.md). -## 🐛 Retours & contributions +## 🐛 Retours et contributions -Vous avez trouvĂ© un bug ou souhaitez proposer une fonctionnalitĂ© ? Ouvrez une issue sur le [gestionnaire d’issues GitHub](https://github.com/Reefact/first-class-errors/issues). Les contributions sont les bienvenues — voir [CONTRIBUTING.fr.md](./CONTRIBUTING.fr.md) pour commencer. +Vous avez trouvĂ© un bug ou souhaitez proposer une fonctionnalitĂ© ? Ouvrez une issue sur le [gestionnaire d’issues GitHub](https://github.com/Reefact/first-class-errors/issues). Les contributions sont les bienvenues ; consultez [CONTRIBUTING.fr.md](CONTRIBUTING.fr.md). -Pour les vulnĂ©rabilitĂ©s de **sĂ©curitĂ©**, merci de suivre le processus privĂ© dĂ©crit dans [SECURITY.fr.md](./SECURITY.fr.md) plutĂŽt que d’ouvrir une issue publique. +Pour les vulnĂ©rabilitĂ©s de sĂ©curitĂ©, suivez le processus privĂ© dĂ©crit dans [SECURITY.fr.md](SECURITY.fr.md). -## 📚 Étapes suivantes +## 📚 Documentation -Consultez la documentation complĂšte : +### DĂ©couvrir - [Premiers pas](GettingStarted.fr.md) - [Principes de conception](DesignPrinciples.fr.md) - [Quand ne pas utiliser FirstClassErrors](WhenNotToUseFirstClassErrors.fr.md) + +### Comprendre le modĂšle + - [Concepts fondamentaux](CoreConcepts.fr.md) +- [Taxonomie et composition des erreurs](ErrorTaxonomy.fr.md) - [Guide du contexte d’erreur](ErrorContext.fr.md) + +### Écrire et utiliser des erreurs + - [Guide d’écriture des erreurs](WritingErrorsGuide.fr.md) - [Cas d’usage](UsagePatterns.fr.md) - [Bonnes pratiques](BestPractices.fr.md) - [Guide des tests](Testing.fr.md) + +### GĂ©nĂ©rer et exploiter le catalogue + - [IntĂ©gration CI/CD et exploitation](OperationalIntegration.fr.md) - Versionnage du catalogue - - [Vue d'ensemble & workflow](CatalogVersioning.fr.md) + - [Vue d’ensemble et workflow](CatalogVersioning.fr.md) - [RĂ©fĂ©rence des commandes](CatalogVersioningReference.fr.md) - [IntĂ©gration CI/CD](CatalogVersioningCI.fr.md) - [Architecture du pipeline de documentation](ArchitectureOfTheDocumentationPipeline.fr.md) - [Écrire son propre renderer](WritingACustomRenderer.fr.md) - [Internationalisation](Internationalisation.fr.md) -- [Comparaison avec les librairies de gestion d’erreurs](ComparisonWithOtherLibraries.fr.md) + +### Évaluer et rĂ©soudre les problĂšmes + +- [Comparaison avec les bibliothĂšques de gestion d’erreurs](ComparisonWithOtherLibraries.fr.md) - [RĂšgles d’analyse (FCExxx)](analyzers/README.fr.md) - [FAQ](FAQ.fr.md)