From ffcaf6f570dd72dec8d47830f97497c24e34863a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 09:39:34 +0000 Subject: [PATCH 1/3] ci(gendoc): regenerate the tool's error catalog on every pull request Add a dedicated workflow that builds fce and FirstClassErrors.GenDoc on every push to a pull-request branch, regenerates the human-readable catalog (Markdown, split layout) into doc/generated/gendoc/catalog, and reports the pending change against a committed baseline (errors-baseline.json) into errors-diff.md -- informational only, this workflow never blocks a pull request. Both are committed back onto the pull request's own branch, never main, so the regenerated content goes through normal review and merges like any other change. Bootstrap the initial baseline and catalog: as of this commit neither existed, so 'fce catalog update' explicitly accepts the current 16-error surface as the starting contract. Refs: #142 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0148FgVG3MkKn2kudM2TkmGa --- .github/workflows/gendoc-docs.yml | 93 ++++++++ doc/generated/README.md | 11 +- doc/generated/gendoc/.gitkeep | 2 - doc/generated/gendoc/catalog/README.md | 20 ++ .../catalog/documentation-request-errors.md | 10 + .../catalog/documentation-toolchain-errors.md | 14 ++ .../catalog/gendoc-assembly-not-found.md | 38 ++++ .../gendoc/catalog/gendoc-opt-in-ambiguous.md | 40 ++++ .../catalog/gendoc-process-start-failed.md | 36 +++ .../catalog/gendoc-process-timed-out.md | 39 ++++ .../gendoc-project-enumeration-failed.md | 38 ++++ .../catalog/gendoc-solution-build-failed.md | 38 ++++ .../catalog/gendoc-solution-not-found.md | 38 ++++ .../gendoc-solution-path-unsupported.md | 38 ++++ .../gendoc-target-assembly-not-found.md | 40 ++++ .../gendoc-target-path-resolution-failed.md | 36 +++ .../gendoc/catalog/gendoc-worker-failed.md | 38 ++++ .../catalog/gendoc-worker-not-deployed.md | 38 ++++ .../catalog/gendoc-worker-output-missing.md | 36 +++ .../gendoc-worker-output-unreadable.md | 36 +++ .../catalog/gendoc-worker-path-invalid.md | 38 ++++ .../catalog/gendoc-worker-run-failed.md | 36 +++ doc/generated/gendoc/errors-baseline.json | 213 ++++++++++++++++++ doc/generated/gendoc/errors-diff.md | 3 + 24 files changed, 965 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/gendoc-docs.yml delete mode 100644 doc/generated/gendoc/.gitkeep create mode 100644 doc/generated/gendoc/catalog/README.md create mode 100644 doc/generated/gendoc/catalog/documentation-request-errors.md create mode 100644 doc/generated/gendoc/catalog/documentation-toolchain-errors.md create mode 100644 doc/generated/gendoc/catalog/gendoc-assembly-not-found.md create mode 100644 doc/generated/gendoc/catalog/gendoc-opt-in-ambiguous.md create mode 100644 doc/generated/gendoc/catalog/gendoc-process-start-failed.md create mode 100644 doc/generated/gendoc/catalog/gendoc-process-timed-out.md create mode 100644 doc/generated/gendoc/catalog/gendoc-project-enumeration-failed.md create mode 100644 doc/generated/gendoc/catalog/gendoc-solution-build-failed.md create mode 100644 doc/generated/gendoc/catalog/gendoc-solution-not-found.md create mode 100644 doc/generated/gendoc/catalog/gendoc-solution-path-unsupported.md create mode 100644 doc/generated/gendoc/catalog/gendoc-target-assembly-not-found.md create mode 100644 doc/generated/gendoc/catalog/gendoc-target-path-resolution-failed.md create mode 100644 doc/generated/gendoc/catalog/gendoc-worker-failed.md create mode 100644 doc/generated/gendoc/catalog/gendoc-worker-not-deployed.md create mode 100644 doc/generated/gendoc/catalog/gendoc-worker-output-missing.md create mode 100644 doc/generated/gendoc/catalog/gendoc-worker-output-unreadable.md create mode 100644 doc/generated/gendoc/catalog/gendoc-worker-path-invalid.md create mode 100644 doc/generated/gendoc/catalog/gendoc-worker-run-failed.md create mode 100644 doc/generated/gendoc/errors-baseline.json create mode 100644 doc/generated/gendoc/errors-diff.md diff --git a/.github/workflows/gendoc-docs.yml b/.github/workflows/gendoc-docs.yml new file mode 100644 index 0000000..9bf9379 --- /dev/null +++ b/.github/workflows/gendoc-docs.yml @@ -0,0 +1,93 @@ +name: gendoc-docs + +# Keeps doc/generated/gendoc (GenDoc's own error catalog) in sync with a pull request's branch on every +# push, so the regenerated content travels through normal review like any other change — never pushed +# straight to main. See doc/handwritten/for-users/CatalogVersioningReference.en.md for the underlying `fce +# catalog update`/`diff` mechanics, and release.yml for the release-time gate that turns a breaking change +# reported here into a required major version bump of the cli train. +on: + pull_request: + branches: + - main + +# Cancel a superseded run of the same pull request. +concurrency: + group: gendoc-docs-${{ github.event.pull_request.number }} + cancel-in-progress: true + +# Least privilege: read-only at the top level; the single job below widens only what it needs. +permissions: + contents: read + +env: + DOTNET_NOLOGO: 'true' + DOTNET_CLI_TELEMETRY_OPTOUT: 'true' + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true' + +jobs: + publish: + name: Regenerate GenDoc's error catalog + runs-on: ubuntu-latest + # A pull request from a fork gets a read-only GITHUB_TOKEN regardless of the permissions below (GitHub's + # own security default), so the commit step would fail there anyway. Every pull request in this + # repository's history is same-repo, but skip outright rather than run a doomed build first. + if: github.event.pull_request.head.repo.full_name == github.repository + timeout-minutes: 15 + permissions: + # Push the regenerated catalog back onto the pull request's own branch (never main). + contents: write + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7 + with: + ref: ${{ github.head_ref }} + + - name: Setup .NET + uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5 + with: + dotnet-version: '10.0.x' + + - name: Build the tooling + run: dotnet build FirstClassErrors.Cli/FirstClassErrors.Cli.csproj -c Release + + - name: Regenerate the catalog and its diff report + run: | + set -euo pipefail + FCE="dotnet FirstClassErrors.Cli/bin/Release/net8.0/fce.dll" + GENDOC_DLL="FirstClassErrors.GenDoc/bin/Release/net8.0/FirstClassErrors.GenDoc.dll" + BASELINE="doc/generated/gendoc/errors-baseline.json" + + # catalog/ is entirely renderer output: wipe it first so a removed error's page never lingers + # (fce generate only writes files, it never deletes a stale one from a previous run). + rm -rf doc/generated/gendoc/catalog + $FCE generate \ + --assemblies "$GENDOC_DLL" \ + --format markdown --layout split --service-name gendoc \ + --output doc/generated/gendoc/catalog + + # Informational only (--fail-on none): this workflow never blocks a pull request. Whether a + # breaking change is acceptable is enforced later, at release time, against the major version + # bump of the cli train (release.yml) -- not here. + $FCE catalog diff \ + --assemblies "$GENDOC_DLL" \ + --baseline "$BASELINE" \ + --report markdown --fail-on none \ + > doc/generated/gendoc/errors-diff.md + + - name: Commit the regenerated catalog to this branch + run: | + set -euo pipefail + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if [ -z "$(git status --porcelain -- doc/generated/gendoc)" ]; then + echo "GenDoc's error catalog is unchanged; nothing to publish." + exit 0 + fi + + git add doc/generated/gendoc + git commit -m "docs(gendoc): regenerate the tool's own error catalog" + # Pushes made with the default GITHUB_TOKEN do not re-trigger workflows (GitHub's own loop + # guard), so this never causes another 'pull_request: synchronize' event and another run of + # this job. + git push origin HEAD:${{ github.head_ref }} diff --git a/doc/generated/README.md b/doc/generated/README.md index 3f1f7db..c787b76 100644 --- a/doc/generated/README.md +++ b/doc/generated/README.md @@ -4,8 +4,15 @@ This folder holds **living documentation produced automatically by CI/CD**. Do not edit its contents by hand: anything here is regenerated (and overwritten) on the next generation run. -- [`gendoc/`](gendoc) — error catalog emitted by the GenDoc toolchain - (`FirstClassErrors.GenDoc` / `fce generate`). +- [`gendoc/`](gendoc) — GenDoc's own error catalog (`FirstClassErrors.GenDoc` documenting itself with + `fce generate`), regenerated on every pull request by the `gendoc-docs` workflow: + - [`catalog/`](gendoc/catalog) — the human-readable catalog (Markdown, one page per error). + - `errors-baseline.json` — the versioned contract: the catalog as of the last `cli` release. Only + changed by `fce catalog update`, run automatically after a successful `cli` release. + - `errors-diff.md` — the catalog's pending changes against that baseline (informational; see + [Catalog Versioning](../handwritten/for-users/CatalogVersioning.en.md)). A breaking change reported + here must be matched by a major version bump at the next `cli` release, or `release.yml` refuses to + publish it. Handwritten documentation lives under [`../handwritten`](../handwritten): [`for-users`](../handwritten/for-users) and diff --git a/doc/generated/gendoc/.gitkeep b/doc/generated/gendoc/.gitkeep deleted file mode 100644 index 6889834..0000000 --- a/doc/generated/gendoc/.gitkeep +++ /dev/null @@ -1,2 +0,0 @@ -# Placeholder so the CI/CD output directory is tracked before the first -# generation run. GenDoc writes the rendered error catalog here. diff --git a/doc/generated/gendoc/catalog/README.md b/doc/generated/gendoc/catalog/README.md new file mode 100644 index 0000000..1088139 --- /dev/null +++ b/doc/generated/gendoc/catalog/README.md @@ -0,0 +1,20 @@ +# Error Catalog + +- [DocumentationRequest errors](./documentation-request-errors.md) + - [Requested assembly not found](./gendoc-assembly-not-found.md) + - [Documentation opt-in ambiguous](./gendoc-opt-in-ambiguous.md) + - [Solution file not found](./gendoc-solution-not-found.md) + - [Solution path not supported](./gendoc-solution-path-unsupported.md) + - [Project build output not found](./gendoc-target-assembly-not-found.md) + - [Configured worker path invalid](./gendoc-worker-path-invalid.md) +- [DocumentationToolchain errors](./documentation-toolchain-errors.md) + - [Toolchain process failed to start](./gendoc-process-start-failed.md) + - [Toolchain process timed out](./gendoc-process-timed-out.md) + - [Solution project enumeration failed](./gendoc-project-enumeration-failed.md) + - [Solution build failed](./gendoc-solution-build-failed.md) + - [Target path resolution failed](./gendoc-target-path-resolution-failed.md) + - [Documentation worker failed](./gendoc-worker-failed.md) + - [Documentation worker not deployed](./gendoc-worker-not-deployed.md) + - [Documentation worker output missing](./gendoc-worker-output-missing.md) + - [Documentation worker output unreadable](./gendoc-worker-output-unreadable.md) + - [Documentation worker run failed](./gendoc-worker-run-failed.md) diff --git a/doc/generated/gendoc/catalog/documentation-request-errors.md b/doc/generated/gendoc/catalog/documentation-request-errors.md new file mode 100644 index 0000000..372f445 --- /dev/null +++ b/doc/generated/gendoc/catalog/documentation-request-errors.md @@ -0,0 +1,10 @@ +# DocumentationRequest errors + +Validation of a documentation-generation request: the solution, assemblies, worker path and opt-in markers it designates. + +- [Requested assembly not found](./gendoc-assembly-not-found.md) +- [Documentation opt-in ambiguous](./gendoc-opt-in-ambiguous.md) +- [Solution file not found](./gendoc-solution-not-found.md) +- [Solution path not supported](./gendoc-solution-path-unsupported.md) +- [Project build output not found](./gendoc-target-assembly-not-found.md) +- [Configured worker path invalid](./gendoc-worker-path-invalid.md) diff --git a/doc/generated/gendoc/catalog/documentation-toolchain-errors.md b/doc/generated/gendoc/catalog/documentation-toolchain-errors.md new file mode 100644 index 0000000..4547830 --- /dev/null +++ b/doc/generated/gendoc/catalog/documentation-toolchain-errors.md @@ -0,0 +1,14 @@ +# DocumentationToolchain errors + +The toolchain the documentation generator drives: the .NET SDK commands it spawns and the extraction worker process it launches per assembly. + +- [Toolchain process failed to start](./gendoc-process-start-failed.md) +- [Toolchain process timed out](./gendoc-process-timed-out.md) +- [Solution project enumeration failed](./gendoc-project-enumeration-failed.md) +- [Solution build failed](./gendoc-solution-build-failed.md) +- [Target path resolution failed](./gendoc-target-path-resolution-failed.md) +- [Documentation worker failed](./gendoc-worker-failed.md) +- [Documentation worker not deployed](./gendoc-worker-not-deployed.md) +- [Documentation worker output missing](./gendoc-worker-output-missing.md) +- [Documentation worker output unreadable](./gendoc-worker-output-unreadable.md) +- [Documentation worker run failed](./gendoc-worker-run-failed.md) diff --git a/doc/generated/gendoc/catalog/gendoc-assembly-not-found.md b/doc/generated/gendoc/catalog/gendoc-assembly-not-found.md new file mode 100644 index 0000000..85e9be1 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-assembly-not-found.md @@ -0,0 +1,38 @@ +# Requested assembly not found + +- **Code:** `GENDOC_ASSEMBLY_NOT_FOUND` +- **Source:** `DocumentationRequest` + +A documentation-generation request explicitly designates an assembly that does not exist on disk. The full path, as resolved by the generator, is carried in the error context. + +> **Business rule:** Every assembly explicitly designated by a generation request must exist on disk. + +## Diagnostics + +- **The assembly was never built, or was built to a different configuration or target framework than the path assumes.** — _origin:_ External — Build the project first and compare the path in the error context with the actual build output directory. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-assembly-not-found", + "title": "A requested assembly was not found.", + "detail": "One of the assemblies passed to the documentation generator does not exist on disk.", + "code": "GENDOC_ASSEMBLY_NOT_FOUND" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationRequest] Assembly not found: '/src/app/bin/Release/net8.0/Application.dll'. error.code=GENDOC_ASSEMBLY_NOT_FOUND +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `AssemblyPath` | `System.String` | Full path of the assembly being documented. | `/src/app/bin/Release/net8.0/Application.dll` | + diff --git a/doc/generated/gendoc/catalog/gendoc-opt-in-ambiguous.md b/doc/generated/gendoc/catalog/gendoc-opt-in-ambiguous.md new file mode 100644 index 0000000..ec9dc47 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-opt-in-ambiguous.md @@ -0,0 +1,40 @@ +# Documentation opt-in ambiguous + +- **Code:** `GENDOC_OPT_IN_AMBIGUOUS` +- **Source:** `DocumentationRequest` + +The generator reads the documentation opt-in property literally from the project XML, without MSBuild evaluation. When the property is defined more than once, or gated behind an MSBuild Condition (directly or via a Choose/When branch), its effective value cannot be known, so the generator refuses to guess and skips the project. The project path, the property name, and the reason are carried in the error context. + +> **Business rule:** The opt-in property must be declared at most once, literally and unconditionally, in each project file. + +## Diagnostics + +- **The property is declared in several PropertyGroups, or under a Condition attribute or a Choose/When branch.** — _origin:_ External — Inspect the project file named in the error context; keep a single unconditional declaration, or document the built assembly explicitly instead. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-opt-in-ambiguous", + "title": "A project's documentation opt-in is ambiguous.", + "detail": "Declare the opt-in property once, literally and unconditionally, in the project file — or document the assembly explicitly.", + "code": "GENDOC_OPT_IN_AMBIGUOUS" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationRequest] Cannot determine the opt-in for project '/src/app/Application/Application.csproj': the 'GenerateErrorDocumentation' property is defined 2 times in the project XML, which GenDoc reads without MSBuild evaluation. Declare it once, literally and unconditionally in the project file, or document the built assembly explicitly. error.code=GENDOC_OPT_IN_AMBIGUOUS +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `ProjectPath` | `System.String` | Full path of the project file being processed. | `/src/app/Application/Application.csproj` | +| `OptInProperty` | `System.String` | Name of the MSBuild property read as the documentation opt-in marker. | `GenerateErrorDocumentation` | +| `AmbiguityReason` | `System.String` | Why the opt-in property's effective value cannot be determined from the project XML. | `defined 2 times` | + diff --git a/doc/generated/gendoc/catalog/gendoc-process-start-failed.md b/doc/generated/gendoc/catalog/gendoc-process-start-failed.md new file mode 100644 index 0000000..fbe2c11 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-process-start-failed.md @@ -0,0 +1,36 @@ +# Toolchain process failed to start + +- **Code:** `GENDOC_PROCESS_START_FAILED` +- **Source:** `DocumentationToolchain` + +The generator drives the .NET SDK and its extraction worker through child processes. One of them could not be started at all. The executable name is carried in the error context. + +## Diagnostics + +- **The 'dotnet' host is not installed or not on the PATH of the process running the generation.** — _origin:_ External — Run 'dotnet --info' in the same environment (shell, CI step, service account) as the generation. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-process-start-failed", + "title": "A required process could not be started.", + "detail": "A child process required by the documentation generation could not be started.", + "code": "GENDOC_PROCESS_START_FAILED" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] Failed to start process 'dotnet'. error.code=GENDOC_PROCESS_START_FAILED +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `ProcessFileName` | `System.String` | Executable name of the child process the generator tried to run. | `dotnet` | + diff --git a/doc/generated/gendoc/catalog/gendoc-process-timed-out.md b/doc/generated/gendoc/catalog/gendoc-process-timed-out.md new file mode 100644 index 0000000..1405eaf --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-process-timed-out.md @@ -0,0 +1,39 @@ +# Toolchain process timed out + +- **Code:** `GENDOC_PROCESS_TIMED_OUT` +- **Source:** `DocumentationToolchain` + +A child process of the generation (an SDK command or the extraction worker) exceeded its configured timeout and was killed. The command, its target and the timeout are carried in the error context; the output captured before the kill is appended to the diagnostic message. It is transient: the run can be retried. + +## Diagnostics + +- **The machine is under load, or a cold NuGet cache made the first build far slower than usual.** — _origin:_ External — Retry the run; compare its duration with the configured build, SDK-query and worker timeouts. +- **A documented assembly's example factory hangs (extraction executes the documentation methods of the target).** — _origin:_ InternalOrExternal — Check which assembly was being processed when the timeout hit; review its documentation examples for blocking calls. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-process-timed-out", + "title": "A documentation process timed out.", + "detail": "The operation exceeded its configured timeout and was killed; it can be retried.", + "code": "GENDOC_PROCESS_TIMED_OUT" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] Process 'dotnet build /src/app/Application.sln' timed out after 00:10:00 and was killed. Determining projects to restore... error.code=GENDOC_PROCESS_TIMED_OUT +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `Command` | `System.String` | Short description of the child-process command that was running. | `dotnet build /src/app/Application.sln` | +| `Target` | `System.String` | Path of the solution, project or assembly the timed-out command was operating on. | `/src/app/Application.sln` | +| `Timeout` | `System.TimeSpan` | Configured timeout the child process exceeded. | `00:10:00` | + diff --git a/doc/generated/gendoc/catalog/gendoc-project-enumeration-failed.md b/doc/generated/gendoc/catalog/gendoc-project-enumeration-failed.md new file mode 100644 index 0000000..f5de2f8 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-project-enumeration-failed.md @@ -0,0 +1,38 @@ +# Solution project enumeration failed + +- **Code:** `GENDOC_PROJECT_ENUMERATION_FAILED` +- **Source:** `DocumentationToolchain` + +The generator enumerates a solution's projects by running 'dotnet sln list'. That command failed, so no project could be discovered. The solution path and the exit code are carried in the error context; the command's error output is appended to the diagnostic message. + +## Diagnostics + +- **The solution file is malformed or references projects that no longer exist.** — _origin:_ External — Run 'dotnet sln list' by hand and read its error output. +- **The .NET SDK on the machine is missing or too old for the solution format (for example .slnx support).** — _origin:_ External — Check 'dotnet --version' against the solution format and the repository's SDK requirements. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-project-enumeration-failed", + "title": "The solution's projects could not be listed.", + "detail": "The 'dotnet sln list' command failed for the requested solution.", + "code": "GENDOC_PROJECT_ENUMERATION_FAILED" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] Failed to list the projects of solution '/src/app/Application.sln' (exit code 1). The solution file is invalid. error.code=GENDOC_PROJECT_ENUMERATION_FAILED +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `SolutionPath` | `System.String` | Full path of the solution file the generation request designates. | `/src/app/Application.sln` | +| `ExitCode` | `System.Int32` | Exit code returned by the child process. | `1` | + diff --git a/doc/generated/gendoc/catalog/gendoc-solution-build-failed.md b/doc/generated/gendoc/catalog/gendoc-solution-build-failed.md new file mode 100644 index 0000000..0059e38 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-solution-build-failed.md @@ -0,0 +1,38 @@ +# Solution build failed + +- **Code:** `GENDOC_SOLUTION_BUILD_FAILED` +- **Source:** `DocumentationToolchain` + +The generator builds the solution before documenting it (unless the build step is disabled). The build failed, so no assembly could be documented. The solution path and the exit code are carried in the error context; the build output is appended to the diagnostic message. + +## Diagnostics + +- **The solution under documentation has compile errors.** — _origin:_ External — Read the build output in the diagnostic message; build the solution by hand to reproduce. +- **Package restore failed (offline machine, feed outage, or authentication).** — _origin:_ InternalOrExternal — Check the restore section of the build output and the reachability of the configured NuGet feeds. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-solution-build-failed", + "title": "The solution build failed.", + "detail": "The solution under documentation did not build; the build output is in the diagnostic message.", + "code": "GENDOC_SOLUTION_BUILD_FAILED" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] dotnet build failed for solution '/src/app/Application.sln' (exit code 1). CS1002: ; expected error.code=GENDOC_SOLUTION_BUILD_FAILED +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `SolutionPath` | `System.String` | Full path of the solution file the generation request designates. | `/src/app/Application.sln` | +| `ExitCode` | `System.Int32` | Exit code returned by the child process. | `1` | + diff --git a/doc/generated/gendoc/catalog/gendoc-solution-not-found.md b/doc/generated/gendoc/catalog/gendoc-solution-not-found.md new file mode 100644 index 0000000..b5b2cb6 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-solution-not-found.md @@ -0,0 +1,38 @@ +# Solution file not found + +- **Code:** `GENDOC_SOLUTION_NOT_FOUND` +- **Source:** `DocumentationRequest` + +A documentation-generation request designates a solution file that does not exist on disk. The full path, as resolved by the generator, is carried in the error context. + +> **Business rule:** A generation request must designate an existing solution file. + +## Diagnostics + +- **The path is wrong or relative to an unexpected working directory (a CI step often runs from a different directory than a developer shell).** — _origin:_ External — Compare the resolved path in the error context with the actual solution location; check the working directory of the caller. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-solution-not-found", + "title": "The solution file was not found.", + "detail": "The path passed to the documentation generator does not point to an existing solution file.", + "code": "GENDOC_SOLUTION_NOT_FOUND" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationRequest] Solution file not found: '/src/app/Application.sln'. error.code=GENDOC_SOLUTION_NOT_FOUND +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `SolutionPath` | `System.String` | Full path of the solution file the generation request designates. | `/src/app/Application.sln` | + diff --git a/doc/generated/gendoc/catalog/gendoc-solution-path-unsupported.md b/doc/generated/gendoc/catalog/gendoc-solution-path-unsupported.md new file mode 100644 index 0000000..f206239 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-solution-path-unsupported.md @@ -0,0 +1,38 @@ +# Solution path not supported + +- **Code:** `GENDOC_SOLUTION_PATH_UNSUPPORTED` +- **Source:** `DocumentationRequest` + +A documentation-generation request designates a file that is not a .sln or .slnx solution. Solution filters (.slnf) are deliberately not supported: the 'dotnet sln' commands the generator relies on do not process them. + +> **Business rule:** A generation request must designate a .sln or .slnx solution file. + +## Diagnostics + +- **A project file, a solution filter (.slnf), or another artifact was passed instead of the solution.** — _origin:_ External — Check the path in the error context; pass the .sln/.slnx file, or document built assemblies directly instead. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-solution-path-unsupported", + "title": "The solution path is not supported.", + "detail": "The documentation generator accepts .sln and .slnx solution files.", + "code": "GENDOC_SOLUTION_PATH_UNSUPPORTED" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationRequest] Expected a .sln or .slnx file path, got: '/src/app/Application.slnf'. error.code=GENDOC_SOLUTION_PATH_UNSUPPORTED +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `SolutionPath` | `System.String` | Full path of the solution file the generation request designates. | `/src/app/Application.slnf` | + diff --git a/doc/generated/gendoc/catalog/gendoc-target-assembly-not-found.md b/doc/generated/gendoc/catalog/gendoc-target-assembly-not-found.md new file mode 100644 index 0000000..62182ec --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-target-assembly-not-found.md @@ -0,0 +1,40 @@ +# Project build output not found + +- **Code:** `GENDOC_TARGET_ASSEMBLY_NOT_FOUND` +- **Source:** `DocumentationRequest` + +The generator resolved a project's build output path (MSBuild TargetPath), but no assembly exists there. Both the project path and the resolved target path are carried in the error context. + +> **Business rule:** Every documented project must have been built for the requested configuration and target framework. + +## Diagnostics + +- **The solution was not built before generation (for example the build step was skipped), so the output is missing.** — _origin:_ External — Build the solution first, or let the generator build it by enabling its build step. +- **The generation request targets a different configuration or framework than the one that was built.** — _origin:_ External — Compare the resolved TargetPath in the error context with the directory that was actually built. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-target-assembly-not-found", + "title": "A project's build output was not found.", + "detail": "The project's resolved target assembly does not exist on disk.", + "code": "GENDOC_TARGET_ASSEMBLY_NOT_FOUND" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationRequest] Target assembly not found for project '/src/app/Application/Application.csproj'. Resolved TargetPath='/src/app/Application/bin/Release/net8.0/Application.dll'. error.code=GENDOC_TARGET_ASSEMBLY_NOT_FOUND +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `ProjectPath` | `System.String` | Full path of the project file being processed. | `/src/app/Application/Application.csproj` | +| `TargetPath` | `System.String` | Build output path resolved for the project (MSBuild TargetPath). | `/src/app/Application/bin/Release/net8.0/Application.dll` | + diff --git a/doc/generated/gendoc/catalog/gendoc-target-path-resolution-failed.md b/doc/generated/gendoc/catalog/gendoc-target-path-resolution-failed.md new file mode 100644 index 0000000..b4991c5 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-target-path-resolution-failed.md @@ -0,0 +1,36 @@ +# Target path resolution failed + +- **Code:** `GENDOC_TARGET_PATH_RESOLUTION_FAILED` +- **Source:** `DocumentationToolchain` + +The generator resolves each project's build output path by querying the .NET SDK ('dotnet msbuild -getProperty:TargetPath'). That query threw, so the project cannot be located and is skipped (or the generation stops, per the configured failure behavior). The project path is carried in the error context. + +## Diagnostics + +- **The project file does not evaluate (broken import, missing SDK workload, malformed XML).** — _origin:_ External — Run 'dotnet msbuild -getProperty:TargetPath' by hand and read its error output. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-target-path-resolution-failed", + "title": "A project's target path could not be resolved.", + "detail": "The build output path of a project could not be resolved through the .NET SDK.", + "code": "GENDOC_TARGET_PATH_RESOLUTION_FAILED" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] Failed to resolve target path for project '/src/app/Application/Application.csproj'. error.code=GENDOC_TARGET_PATH_RESOLUTION_FAILED +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `ProjectPath` | `System.String` | Full path of the project file being processed. | `/src/app/Application/Application.csproj` | + diff --git a/doc/generated/gendoc/catalog/gendoc-worker-failed.md b/doc/generated/gendoc/catalog/gendoc-worker-failed.md new file mode 100644 index 0000000..e91b3ef --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-worker-failed.md @@ -0,0 +1,38 @@ +# Documentation worker failed + +- **Code:** `GENDOC_WORKER_FAILED` +- **Source:** `DocumentationToolchain` + +The extraction worker runs once per documented assembly, in its own process, against that assembly's dependency graph. It exited with a non-zero code for one assembly. The assembly path and the exit code are carried in the error context; the worker's error output is appended to the diagnostic message. + +## Diagnostics + +- **The target assembly failed to load in the worker (missing dependency, mismatched FirstClassErrors version).** — _origin:_ External — Read the worker's error output in the diagnostic message; check the target's deps.json next to the assembly. +- **A documentation method or example factory of the target threw while the worker executed it.** — _origin:_ External — Read the worker's error output; run the target's documentation methods in a unit test to reproduce. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-worker-failed", + "title": "The documentation worker failed.", + "detail": "The extraction worker exited with an error for one of the documented assemblies.", + "code": "GENDOC_WORKER_FAILED" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] The documentation worker failed (exit code 1) for '/src/app/bin/Release/net8.0/Application.dll'. Fatal error while extracting documentation. error.code=GENDOC_WORKER_FAILED +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `AssemblyPath` | `System.String` | Full path of the assembly being documented. | `/src/app/bin/Release/net8.0/Application.dll` | +| `ExitCode` | `System.Int32` | Exit code returned by the child process. | `1` | + diff --git a/doc/generated/gendoc/catalog/gendoc-worker-not-deployed.md b/doc/generated/gendoc/catalog/gendoc-worker-not-deployed.md new file mode 100644 index 0000000..c954a6c --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-worker-not-deployed.md @@ -0,0 +1,38 @@ +# Documentation worker not deployed + +- **Code:** `GENDOC_WORKER_NOT_DEPLOYED` +- **Source:** `DocumentationToolchain` + +No explicit worker path is configured, and the extraction worker was not found next to the tool — the conventional location it is deployed to. The probed directory is carried in the error context. + +> **Business rule:** The extraction worker ships next to the tool; an installation without it cannot extract documentation. + +## Diagnostics + +- **The tool was copied or repackaged without 'FirstClassErrors.GenDoc.Worker.dll' (an incomplete manual install).** — _origin:_ Internal — Inspect the probed directory named in the error context; reinstall the tool, or configure an explicit worker path. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-worker-not-deployed", + "title": "The documentation worker is missing.", + "detail": "The extraction worker was not found next to the tool; the installation is incomplete.", + "code": "GENDOC_WORKER_NOT_DEPLOYED" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] The documentation worker 'FirstClassErrors.GenDoc.Worker.dll' could not be located in '/tools/fce'. Set SolutionGenerationOptions.WorkerAssemblyPath, or ensure the worker is deployed next to the tool. error.code=GENDOC_WORKER_NOT_DEPLOYED +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `ProbedDirectory` | `System.String` | Directory probed for the documentation worker assembly. | `/tools/fce` | + diff --git a/doc/generated/gendoc/catalog/gendoc-worker-output-missing.md b/doc/generated/gendoc/catalog/gendoc-worker-output-missing.md new file mode 100644 index 0000000..c1f72a7 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-worker-output-missing.md @@ -0,0 +1,36 @@ +# Documentation worker output missing + +- **Code:** `GENDOC_WORKER_OUTPUT_MISSING` +- **Source:** `DocumentationToolchain` + +The extraction worker exited successfully but the result file it was asked to write does not exist. The assembly path is carried in the error context. + +## Diagnostics + +- **The temporary directory is not writable, or an antivirus or cleanup job removed the file between the worker's exit and its harvesting.** — _origin:_ External — Check the permissions and free space of the temporary directory used by the generation. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-worker-output-missing", + "title": "The documentation worker produced no output.", + "detail": "The extraction worker exited successfully but its result file is missing.", + "code": "GENDOC_WORKER_OUTPUT_MISSING" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] The documentation worker produced no output for '/src/app/bin/Release/net8.0/Application.dll'. error.code=GENDOC_WORKER_OUTPUT_MISSING +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `AssemblyPath` | `System.String` | Full path of the assembly being documented. | `/src/app/bin/Release/net8.0/Application.dll` | + diff --git a/doc/generated/gendoc/catalog/gendoc-worker-output-unreadable.md b/doc/generated/gendoc/catalog/gendoc-worker-output-unreadable.md new file mode 100644 index 0000000..f14f207 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-worker-output-unreadable.md @@ -0,0 +1,36 @@ +# Documentation worker output unreadable + +- **Code:** `GENDOC_WORKER_OUTPUT_UNREADABLE` +- **Source:** `DocumentationToolchain` + +The extraction worker exited successfully and wrote a result file, but the file does not deserialize into an extraction result. The assembly path is carried in the error context. + +## Diagnostics + +- **The worker and the generator come from different tool versions and no longer agree on the result format.** — _origin:_ Internal — Check that the worker next to the tool belongs to the same installation; reinstall the tool if in doubt. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-worker-output-unreadable", + "title": "The documentation worker produced unreadable output.", + "detail": "The extraction worker's result file could not be read as an extraction result.", + "code": "GENDOC_WORKER_OUTPUT_UNREADABLE" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] The documentation worker produced unreadable output for '/src/app/bin/Release/net8.0/Application.dll'. error.code=GENDOC_WORKER_OUTPUT_UNREADABLE +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `AssemblyPath` | `System.String` | Full path of the assembly being documented. | `/src/app/bin/Release/net8.0/Application.dll` | + diff --git a/doc/generated/gendoc/catalog/gendoc-worker-path-invalid.md b/doc/generated/gendoc/catalog/gendoc-worker-path-invalid.md new file mode 100644 index 0000000..a645359 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-worker-path-invalid.md @@ -0,0 +1,38 @@ +# Configured worker path invalid + +- **Code:** `GENDOC_WORKER_PATH_INVALID` +- **Source:** `DocumentationRequest` + +A documentation-generation request explicitly configures the path of the extraction worker, but no file exists there. The configured path is carried in the error context. + +> **Business rule:** A configured worker path must point to an existing FirstClassErrors.GenDoc.Worker.dll. + +## Diagnostics + +- **The configured path is stale — the worker was moved, or the path was written for another machine or installation layout.** — _origin:_ External — Check the path in the error context; remove the explicit setting to fall back to the worker deployed next to the tool. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-worker-path-invalid", + "title": "The configured documentation worker was not found.", + "detail": "The configured worker path must point to an existing FirstClassErrors.GenDoc.Worker.dll.", + "code": "GENDOC_WORKER_PATH_INVALID" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationRequest] The configured documentation worker was not found at '/tools/fce/FirstClassErrors.GenDoc.Worker.dll'. error.code=GENDOC_WORKER_PATH_INVALID +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `WorkerPath` | `System.String` | Configured path of the documentation worker assembly. | `/tools/fce/FirstClassErrors.GenDoc.Worker.dll` | + diff --git a/doc/generated/gendoc/catalog/gendoc-worker-run-failed.md b/doc/generated/gendoc/catalog/gendoc-worker-run-failed.md new file mode 100644 index 0000000..ea9a6c6 --- /dev/null +++ b/doc/generated/gendoc/catalog/gendoc-worker-run-failed.md @@ -0,0 +1,36 @@ +# Documentation worker run failed + +- **Code:** `GENDOC_WORKER_RUN_FAILED` +- **Source:** `DocumentationToolchain` + +Launching the extraction worker, or harvesting its result, threw an unexpected runtime exception (an I/O failure, a permission error…). The assembly path is carried in the error context; the runtime cause travels with the raised exception. + +## Diagnostics + +- **A file-system or permission problem around the temporary result file or the worker binary.** — _origin:_ InternalOrExternal — Read the inner exception attached to the failure; check the temporary directory and the tool's installation directory. + +## Examples + +**Public response (RFC 9457)** + +```json +{ + "type": "urn:problem:gendoc:gendoc-worker-run-failed", + "title": "The documentation worker could not be run.", + "detail": "Running the extraction worker failed unexpectedly for one of the documented assemblies.", + "code": "GENDOC_WORKER_RUN_FAILED" +} +``` + +**Diagnostic (internal — not for external exposure)** + +```text +2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] Failed to run the documentation worker for '/src/app/bin/Release/net8.0/Application.dll'. error.code=GENDOC_WORKER_RUN_FAILED +``` + +## Context + +| Key | Type | Description | Example values | +| --- | --- | --- | --- | +| `AssemblyPath` | `System.String` | Full path of the assembly being documented. | `/src/app/bin/Release/net8.0/Application.dll` | + diff --git a/doc/generated/gendoc/errors-baseline.json b/doc/generated/gendoc/errors-baseline.json new file mode 100644 index 0000000..edf8a8d --- /dev/null +++ b/doc/generated/gendoc/errors-baseline.json @@ -0,0 +1,213 @@ +{ + "schema": 1, + "errors": [ + { + "code": "GENDOC_ASSEMBLY_NOT_FOUND", + "source": "DocumentationRequest", + "title": "Requested assembly not found", + "context": [ + { + "key": "AssemblyPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_OPT_IN_AMBIGUOUS", + "source": "DocumentationRequest", + "title": "Documentation opt-in ambiguous", + "context": [ + { + "key": "AmbiguityReason", + "valueType": "System.String" + }, + { + "key": "OptInProperty", + "valueType": "System.String" + }, + { + "key": "ProjectPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_PROCESS_START_FAILED", + "source": "DocumentationToolchain", + "title": "Toolchain process failed to start", + "context": [ + { + "key": "ProcessFileName", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_PROCESS_TIMED_OUT", + "source": "DocumentationToolchain", + "title": "Toolchain process timed out", + "context": [ + { + "key": "Command", + "valueType": "System.String" + }, + { + "key": "Target", + "valueType": "System.String" + }, + { + "key": "Timeout", + "valueType": "System.TimeSpan" + } + ] + }, + { + "code": "GENDOC_PROJECT_ENUMERATION_FAILED", + "source": "DocumentationToolchain", + "title": "Solution project enumeration failed", + "context": [ + { + "key": "ExitCode", + "valueType": "System.Int32" + }, + { + "key": "SolutionPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_SOLUTION_BUILD_FAILED", + "source": "DocumentationToolchain", + "title": "Solution build failed", + "context": [ + { + "key": "ExitCode", + "valueType": "System.Int32" + }, + { + "key": "SolutionPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_SOLUTION_NOT_FOUND", + "source": "DocumentationRequest", + "title": "Solution file not found", + "context": [ + { + "key": "SolutionPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_SOLUTION_PATH_UNSUPPORTED", + "source": "DocumentationRequest", + "title": "Solution path not supported", + "context": [ + { + "key": "SolutionPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_TARGET_ASSEMBLY_NOT_FOUND", + "source": "DocumentationRequest", + "title": "Project build output not found", + "context": [ + { + "key": "ProjectPath", + "valueType": "System.String" + }, + { + "key": "TargetPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_TARGET_PATH_RESOLUTION_FAILED", + "source": "DocumentationToolchain", + "title": "Target path resolution failed", + "context": [ + { + "key": "ProjectPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_WORKER_FAILED", + "source": "DocumentationToolchain", + "title": "Documentation worker failed", + "context": [ + { + "key": "AssemblyPath", + "valueType": "System.String" + }, + { + "key": "ExitCode", + "valueType": "System.Int32" + } + ] + }, + { + "code": "GENDOC_WORKER_NOT_DEPLOYED", + "source": "DocumentationToolchain", + "title": "Documentation worker not deployed", + "context": [ + { + "key": "ProbedDirectory", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_WORKER_OUTPUT_MISSING", + "source": "DocumentationToolchain", + "title": "Documentation worker output missing", + "context": [ + { + "key": "AssemblyPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_WORKER_OUTPUT_UNREADABLE", + "source": "DocumentationToolchain", + "title": "Documentation worker output unreadable", + "context": [ + { + "key": "AssemblyPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_WORKER_PATH_INVALID", + "source": "DocumentationRequest", + "title": "Configured worker path invalid", + "context": [ + { + "key": "WorkerPath", + "valueType": "System.String" + } + ] + }, + { + "code": "GENDOC_WORKER_RUN_FAILED", + "source": "DocumentationToolchain", + "title": "Documentation worker run failed", + "context": [ + { + "key": "AssemblyPath", + "valueType": "System.String" + } + ] + } + ] +} diff --git a/doc/generated/gendoc/errors-diff.md b/doc/generated/gendoc/errors-diff.md new file mode 100644 index 0000000..c0229ae --- /dev/null +++ b/doc/generated/gendoc/errors-diff.md @@ -0,0 +1,3 @@ +## Error catalog changes + +No catalog changes. From bbaad05b8bbb0c8beeb4b36108932e3e6c63999e Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 09:39:41 +0000 Subject: [PATCH 2/3] ci(cli): block a release when GenDoc's breaking change isn't major GenDoc ships bundled inside the fce tool, not as its own package, so a breaking change to its error catalog is a breaking change of the cli train. Before packing, compare the catalog against the committed baseline with 'fce catalog diff --fail-on breaking': if it reports a breaking change, require the version being published to bump the major component over the previous cli-v tag, or fail the release. Runs on a dry run too, catching a missed bump before a real attempt. After a successful (non-dry-run) cli publish, 'fce catalog update' accepts the released catalog as the new baseline and pushes it directly to main -- the one direct-to-main write in this pipeline, justified because it only ever follows a deliberate, already-published release, never an ordinary pull request. Refs: #142 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0148FgVG3MkKn2kudM2TkmGa --- .github/workflows/release.yml | 80 +++++++++++++++++++++++++++++++++++ 1 file changed, 80 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0923f30..fa54584 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -129,6 +129,59 @@ jobs: - name: Test run: dotnet test FirstClassErrors.sln -c Release --no-build --logger "console;verbosity=normal" + # GenDoc ships bundled inside the fce tool (no package of its own -- see pack.sh), so a breaking + # change to its own error catalog is a breaking change of the cli train. doc/generated/gendoc/ + # accumulates the pending diff against errors-baseline.json (the catalog as of the last cli + # release) on every pull request -- see gendoc-docs.yml. Enforce it here, once, at the one point a + # breaking change actually ships: refuse the release unless the version being published bumps the + # major component over the previous cli-v tag. Runs on a dry run too (everything up to attestation + # does, per the philosophy above), so a misconfigured or forgotten major bump is caught before a + # real release attempt, not during one. + - name: Require a major bump for GenDoc breaking changes + if: steps.version.outputs.component == 'cli' + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + PREFIX="cli-v" + CURRENT_TAG="${PREFIX}${VERSION}" + # Same lookup as tools/packaging/release-notes.sh: most recent same-train tag that is not the + # one being published. Empty on the train's first-ever release. + PREVIOUS_TAG="$(git tag --list "${PREFIX}*" --sort=-version:refname | grep -Fxv "$CURRENT_TAG" | head -n1 || true)" + + set +e + dotnet FirstClassErrors.Cli/bin/Release/net8.0/fce.dll catalog diff \ + --assemblies FirstClassErrors.GenDoc/bin/Release/net8.0/FirstClassErrors.GenDoc.dll \ + --baseline doc/generated/gendoc/errors-baseline.json \ + --fail-on breaking + DIFF_EXIT=$? + set -e + + if [ "$DIFF_EXIT" -eq 0 ]; then + echo "ok: GenDoc's error catalog has no breaking change since the last cli release." + exit 0 + fi + if [ "$DIFF_EXIT" -ne 2 ]; then + echo "::error::fce catalog diff failed unexpectedly (exit $DIFF_EXIT); see the output above." + exit 1 + fi + + # DIFF_EXIT = 2: at least one breaking change. Only acceptable if this release bumps the major + # version -- the major component is always the first dot-segment, pre-release suffix included. + if [ -z "$PREVIOUS_TAG" ]; then + echo "ok: no previous $PREFIX tag; this is the train's first release, nothing to compare the major version against." + exit 0 + fi + NEW_MAJOR="${VERSION%%.*}" + PREVIOUS_VERSION="${PREVIOUS_TAG#"$PREFIX"}" + PREVIOUS_MAJOR="${PREVIOUS_VERSION%%.*}" + + if [ "$NEW_MAJOR" -le "$PREVIOUS_MAJOR" ]; then + echo "::error::GenDoc's error catalog has a breaking change since $PREVIOUS_TAG, but $CURRENT_TAG does not bump the major version ($PREVIOUS_MAJOR -> $NEW_MAJOR). Publish $((PREVIOUS_MAJOR + 1)).0.0 instead, or revert the breaking change." + exit 1 + fi + echo "ok: GenDoc's error catalog has a breaking change since $PREVIOUS_TAG, matched by the major version bump ($PREVIOUS_MAJOR -> $NEW_MAJOR)." + # Pack only the train this release targets (lib -> FirstClassErrors + .Testing; cli -> the fce tool), # so a lib release never republishes the CLI and vice versa. The analyzer is bundled inside the main # package and the GenDoc worker inside the CLI tool; the samples are not published. @@ -234,3 +287,30 @@ jobs: --title "$TAG" --notes-file release-notes.md --target "$GITHUB_SHA" --prerelease="$PRERELEASE" \ || { gh release upload "$TAG" "${assets[@]}" --clobber \ && gh release edit "$TAG" --prerelease="$PRERELEASE"; } + + # Closes the loop opened by "Require a major bump...": now that the catalog above has actually been + # published, explicitly accept it as the new contract (fce catalog update) so the next pull request's + # diff starts from zero again. Real publishes only -- a dry run must never touch the real baseline. + # Best-effort: the actual release already succeeded by this point, so a rare race against another + # push to main (this step pushes directly, the one direct-to-main write in this pipeline, justified + # because it only ever follows a deliberate, already-published release) degrades to a warning rather + # than failing an otherwise-successful release; a maintainer can rerun `fce catalog update` by hand. + - name: Refresh GenDoc's error-catalog baseline + if: steps.version.outputs.component == 'cli' && (github.event_name == 'push' || inputs.dry_run == false) + run: | + set -euo pipefail + dotnet FirstClassErrors.Cli/bin/Release/net8.0/fce.dll catalog update \ + --assemblies FirstClassErrors.GenDoc/bin/Release/net8.0/FirstClassErrors.GenDoc.dll \ + --baseline doc/generated/gendoc/errors-baseline.json + + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + if [ -z "$(git status --porcelain -- doc/generated/gendoc/errors-baseline.json)" ]; then + echo "Baseline already reflects the published catalog; nothing to refresh." + exit 0 + fi + + git add doc/generated/gendoc/errors-baseline.json + git commit -m "docs(gendoc): accept the released error-catalog contract (${{ steps.version.outputs.component }}-v${{ steps.version.outputs.version }})" + git push origin HEAD:main || echo "::warning::Could not push the refreshed baseline to main (the release itself already succeeded); run 'fce catalog update' by hand." From f40e0b8d063de02124026f6169dcfb03859a814d Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 17 Jul 2026 09:59:44 +0000 Subject: [PATCH 3/3] docs(gendoc): record ADR-0010 for the catalog-versioning contract Add ADR-0010: a breaking change to GenDoc's own error catalog requires a major version bump of the cli train, enforced at release time. Accept it, and accept ADR-0009 (GenDoc modeling its own failures as first-class errors) alongside it, since the two decisions ship together. Refs: #167 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_0148FgVG3MkKn2kudM2TkmGa --- ...lings-failures-as-first-class-errors.fr.md | 4 +- ...toolings-failures-as-first-class-errors.md | 4 +- ...rror-catalog-as-a-versioned-contract.fr.md | 140 ++++++++++++++++++ ...s-error-catalog-as-a-versioned-contract.md | 130 ++++++++++++++++ doc/handwritten/for-maintainers/adr/README.md | 3 +- 5 files changed, 276 insertions(+), 5 deletions(-) create mode 100644 doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md create mode 100644 doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md diff --git a/doc/handwritten/for-maintainers/adr/0009-report-the-toolings-failures-as-first-class-errors.fr.md b/doc/handwritten/for-maintainers/adr/0009-report-the-toolings-failures-as-first-class-errors.fr.md index d78a5fc..8fc8386 100644 --- a/doc/handwritten/for-maintainers/adr/0009-report-the-toolings-failures-as-first-class-errors.fr.md +++ b/doc/handwritten/for-maintainers/adr/0009-report-the-toolings-failures-as-first-class-errors.fr.md @@ -2,8 +2,8 @@ 🌍 🇬🇧 [English](0009-report-the-toolings-failures-as-first-class-errors.md) · 🇫🇷 Français (ce fichier) -**Statut :** Proposé -**Date :** 2026-07-16 +**Statut :** Accepté +**Date :** 2026-07-17 **Décideurs :** Reefact ## Contexte diff --git a/doc/handwritten/for-maintainers/adr/0009-report-the-toolings-failures-as-first-class-errors.md b/doc/handwritten/for-maintainers/adr/0009-report-the-toolings-failures-as-first-class-errors.md index f771bb9..5f9706a 100644 --- a/doc/handwritten/for-maintainers/adr/0009-report-the-toolings-failures-as-first-class-errors.md +++ b/doc/handwritten/for-maintainers/adr/0009-report-the-toolings-failures-as-first-class-errors.md @@ -2,8 +2,8 @@ 🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0009-report-the-toolings-failures-as-first-class-errors.fr.md) -**Status:** Proposed -**Date:** 2026-07-16 +**Status:** Accepted +**Date:** 2026-07-17 **Decision Makers:** Reefact ## Context diff --git a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md new file mode 100644 index 0000000..04e3661 --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md @@ -0,0 +1,140 @@ +# ADR-0010 | Traiter le catalogue d'erreurs de GenDoc comme un contrat versionné + +🌍 🇬🇧 [English](0010-treat-gendocs-error-catalog-as-a-versioned-contract.md) · 🇫🇷 Français (ce fichier) + +**Statut :** Accepté +**Date :** 2026-07-17 +**Décideurs :** Reefact + +## Contexte + +L'ADR-0009 a établi que `FirstClassErrors.GenDoc` documente ses propres échecs +comme des erreurs de première classe, en donnant à chacun un code stable préfixé +`GENDOC_` et un contexte structuré. Ces codes et ces clés de contexte sont émis +aux appelants à l'exécution et constituent les identités sur lesquelles +s'appuient les consommateurs externes — pipelines CI, intégrateurs, support. + +GenDoc n'a pas de package NuGet propre : il est embarqué dans l'outil `fce`, +publié sur le train `cli` (`tools/packaging/pack.sh`). Un changement du catalogue +d'erreurs propre à GenDoc — un code renommé ou supprimé, une clé de contexte +retirée ou dont le type change — est donc un changement de ce qu'émet le package +`cli`, indiscernable de l'extérieur de tout autre changement de compatibilité de +ce package. + +La bibliothèque livre déjà les mécanismes pour traiter un catalogue comme un +contrat versionné : `fce catalog update` enregistre une baseline, `fce catalog +diff` la compare, et la comparaison classe chaque changement en Cassant, +Compatible ou Informationnel (un code supprimé ou une clé de contexte +supprimée/retypée est Cassant). Jusqu'ici ces commandes étaient offertes aux +consommateurs documentant leurs propres erreurs, mais jamais appliquées au +catalogue de GenDoc lui-même : rien n'enregistrait la baseline de GenDoc, et rien +ne vérifiait un changement au regard de la version que le train `cli` s'apprêtait +à publier. Les deux trains suivent le versionnage sémantique, et le dépôt impose +déjà les Conventional Commits, mais aucun de ces mécanismes ne relie un changement +cassant du *catalogue d'erreurs* au *numéro de version* qui le publie. + +## Décision + +Un changement cassant du catalogue d'erreurs propre à GenDoc, mesuré par `fce +catalog diff` contre une baseline committée, exige un incrément de version majeure +du train `cli`, imposé au moment de la publication. + +## Justification + +* **La surface d'échec est un contrat publié : elle doit être versionnée comme + tel.** L'ADR-0009 a fait des codes de GenDoc des identités stables dont + dépendent les consommateurs ; une identité stable qui peut disparaître en + silence sous un incrément de version d'apparence compatible n'est pas réellement + stable. Le versionnage sémantique est la promesse que le package `cli` fait + déjà, et un code supprimé ou renommé est exactement le genre de rupture que + cette promesse existe pour signaler. +* **Imposer au release, pas à la pull request.** Un changement cassant du + catalogue n'est pas fautif en soi — un changement délibéré est précisément ce à + quoi sert une version majeure. Seule sa publication *silencieuse*, sous une + version qui promet la compatibilité, est le problème. Bloquer les pull requests + entraverait le développement incrémental normal ; imposer au release cible le + seul point où la promesse de compatibilité est réellement faite, et laisse le + travail quotidien libre. +* **Comparer au dernier release, pas à une cible mouvante.** La baseline n'avance + que lorsqu'un release `cli` publie. Entre deux releases elle reste fixe, de + sorte que le diff répond toujours à « qu'est-ce qui a changé depuis la dernière + chose réellement publiée » — la question à laquelle le numéro de version doit + répondre — quel que soit le nombre de pull requests intercalées. +* **Réutiliser les mécanismes de contrat existants, sans nouveau jugement.** La + classification Cassant de `fce catalog diff` est déjà définie et testée ; cette + décision la relie à la version du release plutôt que d'inventer une seconde + notion de ce qu'être « cassant » signifie pour les erreurs propres à l'outil. + +## Alternatives envisagées + +### S'en remettre aux Conventional Commits et à la vigilance des relecteurs + +Envisagée parce que le dépôt exige déjà un marqueur `!`/`BREAKING CHANGE:` sur les +commits cassants, vérifié en CI. Rejetée parce que ce marqueur est rédigé à la +main d'après l'intention du commit, alors qu'une rupture du catalogue peut être un +effet de bord non voulu (un refactor qui supprime une clé de contexte) ; rien ne +reliait le marqueur à une mesure mécanique du catalogue, de sorte qu'une rupture +silencieuse pouvait encore être publiée sous un incrément mineur. + +### Contrôler la pull request plutôt que le release + +Envisagée parce qu'elle fait apparaître la rupture au plus tôt. Rejetée parce +qu'un changement cassant est légitime en cours de développement tant que le +release final porte l'incrément majeur ; le bloquer par pull request pénaliserait +l'itération normale et forcerait des décisions de version prématurées, alors que +le contrôle au release attrape la même rupture au seul moment où elle compte. + +### Publier GenDoc comme son propre package sur son propre train + +Envisagée parce qu'un train dédié permettrait de versionner le catalogue +indépendamment. Rejetée comme disproportionnée : GenDoc n'a pas de consommateur +autonome (il ne tourne qu'à l'intérieur de `fce`), et le modèle d'outillage de +l'ADR-0002 le garde délibérément embarqué ; un nouveau train ajouterait une +machinerie de release sans bénéfice pour aucun consommateur. + +## Conséquences + +### Positives + +* Un changement cassant des erreurs propres à GenDoc ne peut plus être publié sous + une version `cli` non majeure : le release échoue jusqu'à ce que la version ou + le changement soit réconcilié. +* Le catalogue gagne une baseline committée et un rapport de diff par pull + request, de sorte que l'impact de compatibilité en attente est visible à la + relecture. +* La documentation vivante que la CI régénère s'appuie sur un contrat explicite, + ancré au release, plutôt que sur un instantané au mieux. + +### Négatives + +* Publier le train `cli` dépend désormais d'une baseline committée et d'une étape + de diff ; un mainteneur doit comprendre qu'accepter un changement cassant + signifie incrémenter la version majeure (ou revenir en arrière), pas contourner + le contrôle. +* La baseline est rafraîchie par un push direct sur `main` après un release + réussi — une écriture automatisée hors du flux normal de pull request, limitée à + ce moment précis. + +### Risques + +* Une baseline périmée ou éditée à la main pourrait mal mesurer un changement. + Mitigation : la baseline n'est jamais écrite que par `fce catalog update`, + exécuté par le release après une publication réelle, de sorte qu'elle reflète + toujours le dernier catalogue publié. + +## Actions de suivi + +* Aucune au-delà du câblage des workflows eux-mêmes (`gendoc-docs.yml` et le + contrôle dans `release.yml`) ; le mécanisme vit dans les workflows et les + commandes `fce catalog` que la documentation de référence couvre déjà. + +## Références + +* ADR-0009 — GenDoc modélisant ses propres échecs comme des erreurs de première + classe, les codes que ce contrat versionne. +* ADR-0002 — le modèle de runtime de l'outillage qui garde GenDoc embarqué dans le + train `cli` plutôt que publié séparément. +* Issue [#167](https://github.com/Reefact/first-class-errors/issues/167) — la + demande à laquelle cette décision répond. +* [Référence du versionnage de catalogue](../../for-users/CatalogVersioningReference.fr.md) + — les mécanismes `fce catalog update`/`diff` réutilisés ici. diff --git a/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md new file mode 100644 index 0000000..0a3626b --- /dev/null +++ b/doc/handwritten/for-maintainers/adr/0010-treat-gendocs-error-catalog-as-a-versioned-contract.md @@ -0,0 +1,130 @@ +# ADR-0010 | Treat GenDoc's error catalog as a versioned contract + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](0010-treat-gendocs-error-catalog-as-a-versioned-contract.fr.md) + +**Status:** Accepted +**Date:** 2026-07-17 +**Decision Makers:** Reefact + +## Context + +ADR-0009 established that `FirstClassErrors.GenDoc` documents its own failures as +first-class errors, giving each a stable `GENDOC_`-prefixed code and a structured +context. Those codes and context keys are emitted to callers at runtime and are +the identities external consumers — CI pipelines, integrators, support — match on. + +GenDoc has no NuGet package of its own: it ships bundled inside the `fce` tool, +released on the `cli` train (`tools/packaging/pack.sh`). A change to GenDoc's own +error catalog — a code renamed or removed, a context key dropped or retyped — +is therefore a change to what the `cli` package emits, indistinguishable, from the +outside, from any other compatibility change of that package. + +The library already ships the mechanics to treat a catalog as a versioned +contract: `fce catalog update` records a baseline snapshot, `fce catalog diff` +compares against it, and the comparison classifies each change as Breaking, +Compatible, or Informational (a removed code or a removed/retyped context key is +Breaking). Until now these were offered to consumers documenting their own errors, +but never applied to GenDoc's own catalog: nothing recorded GenDoc's baseline, and +nothing checked a change to it against the version the `cli` train was about to +publish. The two trains follow semantic versioning, and the repository already +enforces Conventional Commits, but neither mechanism connects a breaking change of +the *error catalog* to the *version number* that ships it. + +## Decision + +A breaking change to GenDoc's own error catalog, measured by `fce catalog diff` +against a committed baseline, requires a major version bump of the `cli` train, +enforced at release time. + +## Rationale + +* **The failure surface is a published contract, so it must be versioned like + one.** ADR-0009 made GenDoc's codes stable identities that consumers depend on; + a stable identity that can silently disappear under a compatible-looking version + bump is not actually stable. Semantic versioning is the promise the `cli` + package already makes, and a removed or renamed code is exactly the kind of + break that promise exists to signal. +* **Enforce at the release, not at the pull request.** A breaking change to an + error catalog is not wrong in itself — an intentional one is precisely what a + major version is for. Only shipping it *silently*, under a version that promises + compatibility, is the failure. Gating pull requests would fight normal + incremental development; gating the release targets the single point where the + compatibility promise is actually made, and leaves day-to-day work unblocked. +* **Compare against the last release, not a moving target.** The baseline advances + only when a `cli` release publishes. Between releases it stays fixed, so the + diff always answers "what changed since the last thing actually shipped" — + which is the question the version number must answer — regardless of how many + pull requests landed in between. +* **Reuse the existing contract mechanics, add no new judgement.** `fce catalog + diff`'s Breaking classification is already defined and tested; this decision + wires it to the release version rather than inventing a second notion of what + "breaking" means for the tool's own errors. + +## Alternatives Considered + +### Leave it to Conventional Commits and reviewer discipline + +Considered because the repository already requires a `!`/`BREAKING CHANGE:` +marker on breaking commits, checked in CI. Rejected because that marker is +authored by hand from the commit's intent, while a catalog break can be an +unintended side effect (a refactor that drops a context key); nothing tied the +marker to a mechanical measurement of the catalog, so a silent break could still +ship under a minor bump. + +### Gate the pull request instead of the release + +Considered because it surfaces the break earliest. Rejected because a breaking +change is legitimate mid-development as long as the eventual release carries the +major bump; blocking it per pull request would penalize normal iteration and +force premature version decisions, while the release gate catches the same break +at the only moment it actually matters. + +### Publish GenDoc as its own package on its own train + +Considered because a dedicated train would let the catalog version independently. +Rejected as disproportionate: GenDoc has no standalone consumer (it runs only +inside `fce`), and ADR-0002's tooling model deliberately keeps it bundled; a new +train would add release machinery for no consumer benefit. + +## Consequences + +### Positive + +* A breaking change to GenDoc's own errors cannot ship under a non-major `cli` + version: the release fails until the version or the change is reconciled. +* The catalog gains a committed baseline and a per-pull-request diff report, so + the pending compatibility impact is visible during review. +* The living documentation the CI regenerates is backed by an explicit, + release-anchored contract rather than a best-effort snapshot. + +### Negative + +* Releasing the `cli` train now depends on a committed baseline and a diff step; + a maintainer must understand that accepting a breaking change means bumping the + major version (or reverting), not overriding the gate. +* The baseline is refreshed by a direct push to `main` after a successful + release — one automated write outside the normal pull-request flow, scoped to + that moment. + +### Risks + +* A stale or hand-edited baseline could mis-measure a change. Mitigation: the + baseline is only ever written by `fce catalog update`, run by the release after + a real publish, so it always reflects the last shipped catalog. + +## Follow-up Actions + +* None beyond the workflow wiring itself (`gendoc-docs.yml` and the `release.yml` + gate); the mechanism lives in the workflows and the `fce catalog` commands the + reference documentation already covers. + +## References + +* ADR-0009 — GenDoc modeling its own failures as first-class errors, the codes + this contract versions. +* ADR-0002 — the tooling runtime model that keeps GenDoc bundled in the `cli` + train rather than shipping standalone. +* Issue [#167](https://github.com/Reefact/first-class-errors/issues/167) — the + request this decision answers. +* [Catalog Versioning Reference](../../for-users/CatalogVersioningReference.en.md) + — the `fce catalog update`/`diff` mechanics reused here. diff --git a/doc/handwritten/for-maintainers/adr/README.md b/doc/handwritten/for-maintainers/adr/README.md index 032fa55..437bc5b 100644 --- a/doc/handwritten/for-maintainers/adr/README.md +++ b/doc/handwritten/for-maintainers/adr/README.md @@ -182,4 +182,5 @@ Optional supporting material: | [ADR-0006](0006-supply-arbitrary-test-values-from-a-seedable-source.md) | Supply arbitrary test values from a single seedable source | Accepted | | [ADR-0007](0007-name-the-binder-terminals-new-and-create.md) | Name the binder terminals New and Create | Accepted | | [ADR-0008](0008-bind-nullable-value-type-properties-through-a-struct-constrained-overload.md) | Bind nullable value-type properties through a struct-constrained overload | Proposed | -| [ADR-0009](0009-report-the-toolings-failures-as-first-class-errors.md) | Report the tooling's failures as first-class errors | Proposed | +| [ADR-0009](0009-report-the-toolings-failures-as-first-class-errors.md) | Report the tooling's failures as first-class errors | Accepted | +| [ADR-0010](0010-treat-gendocs-error-catalog-as-a-versioned-contract.md) | Treat GenDoc's error catalog as a versioned contract | Accepted |