diff --git a/.github/workflows/rc-adopter-gate.yml b/.github/workflows/rc-adopter-gate.yml
new file mode 100644
index 0000000..c076863
--- /dev/null
+++ b/.github/workflows/rc-adopter-gate.yml
@@ -0,0 +1,44 @@
+name: RC Adopter Gate
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: Published Codes RC version
+ required: true
+ default: 0.4.0-RC1
+
+permissions:
+ contents: read
+
+jobs:
+ adopters:
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ with:
+ ref: v${{ inputs.version }}
+ persist-credentials: false
+
+ - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
+ with:
+ distribution: temurin
+ java-version: "21"
+
+ - name: Verify external adopter gate
+ run: >-
+ bash scripts/verify-rc-adopters.sh
+ "${{ inputs.version }}"
+ build/reports/rc1-adopters.md
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
+ if: always()
+ with:
+ name: rc1-adopter-gate-manual
+ path: |
+ build/reports/rc1-adopters.md
+ build/rc-adopters/*.log
+ if-no-files-found: warn
+ retention-days: 30
diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml
new file mode 100644
index 0000000..b17515c
--- /dev/null
+++ b/.github/workflows/release-candidate.yml
@@ -0,0 +1,180 @@
+name: Release Candidate
+
+on:
+ push:
+ tags:
+ - "v*.*.*-RC*"
+
+permissions:
+ contents: read
+
+concurrency:
+ group: maven-central-release
+ cancel-in-progress: false
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ timeout-minutes: 90
+ outputs:
+ version: ${{ steps.version.outputs.value }}
+
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+
+ - id: version
+ name: Verify release-candidate tag
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ version="$(sh ./scripts/version.sh)"
+
+ [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-RC[1-9][0-9]*$ ]] || {
+ echo "Invalid release-candidate version: $version"
+ exit 1
+ }
+
+ test "${GITHUB_REF_NAME}" = "v${version}"
+
+ git fetch --no-tags origin main
+
+ test "$(git rev-parse "${GITHUB_SHA}^{commit}")" = \
+ "$(git rev-parse "origin/main^{commit}")"
+
+ echo "value=$version" >> "$GITHUB_OUTPUT"
+
+ - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
+ with:
+ distribution: temurin
+ java-version: "17"
+
+ - uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92
+ with:
+ cache-provider: basic
+
+ - name: Verify release-candidate tree
+ run: |
+ python scripts/verify-compatibility-matrix.py
+ ./gradlew clean verifyAll --stacktrace
+
+ - name: Publish clean release-candidate artifacts
+ run: bash scripts/prepare-compatibility-repository.sh build/compatibility-maven
+
+ - name: Verify published-artifact consumers before Central
+ shell: bash
+ env:
+ MAVEN_REPO_LOCAL: ${{ github.workspace }}/build/compatibility-maven
+ run: |
+ set -euo pipefail
+ version="$(sh ./scripts/version.sh)"
+
+ ./gradlew \
+ -p smoke-test-java \
+ "-Dmaven.repo.local=${MAVEN_REPO_LOCAL}" \
+ clean check \
+ --stacktrace
+
+ ./gradlew \
+ -p smoke-test-kotlin \
+ "-Dmaven.repo.local=${MAVEN_REPO_LOCAL}" \
+ clean check \
+ --stacktrace
+
+ mvn --batch-mode --no-transfer-progress \
+ "-Dmaven.repo.local=${MAVEN_REPO_LOCAL}" \
+ -f smoke-test-maven-java/pom.xml \
+ "-Dcodes.version=$version" \
+ clean verify
+
+ bash ./scripts/verify-kotlin-nullability.sh 2.4.10
+
+ - name: Publish all Codes artifacts to Maven Central
+ run: >-
+ ./gradlew
+ :publishToMavenCentral
+ :codes-spring:publishToMavenCentral
+ :codes-grpc-java:publishToMavenCentral
+ --no-configuration-cache
+ --stacktrace
+ env:
+ ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
+ ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
+ ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }}
+ ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }}
+
+ real-adopters:
+ needs: publish
+ runs-on: ubuntu-latest
+ timeout-minutes: 45
+
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ with:
+ persist-credentials: false
+
+ - uses: actions/setup-java@03ad4de0992f5dab5e18fcb136590ce7c4a0ac95
+ with:
+ distribution: temurin
+ java-version: "21"
+
+ - name: Verify two external adopters from Maven Central
+ run: >-
+ bash scripts/verify-rc-adopters.sh
+ "${{ needs.publish.outputs.version }}"
+ build/reports/rc1-adopters.md
+
+ - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a
+ if: always()
+ with:
+ name: rc1-adopter-gate
+ path: |
+ build/reports/rc1-adopters.md
+ build/rc-adopters/*.log
+ if-no-files-found: warn
+ retention-days: 30
+
+ github-prerelease:
+ needs: [publish, real-adopters]
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ permissions:
+ contents: write
+
+ steps:
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
+ with:
+ persist-credentials: false
+
+ - name: Create GitHub prerelease
+ shell: bash
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+
+ version="${GITHUB_REF_NAME#v}"
+
+ awk -v version="$version" '
+ $0 == "## " version {
+ capture = 1
+ next
+ }
+ capture && /^## / {
+ exit
+ }
+ capture {
+ print
+ }
+ ' CHANGELOG.md > release-notes.md
+
+ test -s release-notes.md
+
+ gh release create "$GITHUB_REF_NAME" \
+ --verify-tag \
+ --prerelease \
+ --title "Codes $version" \
+ --notes-file release-notes.md
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index fd2b7f1..dbfdf4a 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -4,6 +4,7 @@ on:
push:
tags:
- "v*.*.*"
+ - "!v*.*.*-RC*"
permissions:
contents: read
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ebc4d4b..95d4f98 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,11 +2,13 @@
## Unreleased
+## 0.4.0-RC1
+
### Added
* Added Maven publications for `codes-spring` and `codes-grpc-java`.
* Added adapter API snapshots and exact published dependency budgets.
-* Added clean Gradle and Maven consumer checks that resolve all three artifacts from Maven Local.
+* Added clean Gradle and Maven consumer checks that resolve all three artifacts from isolated publication repositories.
* Added application-owned Spring problem-type URI mappings.
* Added a thin failed-`Outcome` to Spring `ErrorResponseException` bridge.
* Added Spring RFC 9457 golden response contracts plus MVC and WebFlux compatibility checks.
@@ -15,10 +17,13 @@
* Added a production compatibility matrix for Spring 6/7, minimum/current gRPC, Java 17/21/25, Java/Kotlin consumers, Gradle/Maven, and Linux/Windows/macOS.
* Added adapter JSpecify consumer verification and adapter coverage gates.
* Added published-POM verification against clean isolated Maven repositories.
+* Added ten-minute Spring and gRPC boundary examples.
+* Added a reproducible release-candidate adopter gate against two pinned external codebases.
+* Added a dedicated release-candidate workflow that publishes to Maven Central before running the external adopter gate.
### Changed
-* Aligned all publishable modules on the shared `0.4.0-SNAPSHOT` version.
+* Set all publishable modules to `0.4.0-RC1`.
* Corrected Spring problem details so reusable outcome messages are titles for explicitly mapped problem types and occurrence details use RFC `detail`.
* Kept stable Codes identity in the Spring `code` extension for every mapped failure.
* Reworked the Spring orders reference to consume `codes-spring` instead of duplicating adapter behavior.
@@ -26,6 +31,7 @@
* Preserved exposed structured issues through `google.rpc.BadRequest` without normalizing coded issue identity.
* Reworked the gRPC orders reference to consume `codes-grpc-java` instead of constructing rich error details manually.
* Moved publication consumer verification to freshly emptied Maven repositories so compatibility checks cannot be satisfied by stale local artifacts.
+* Documented cases where Codes should not be introduced.
## 0.3.1
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..5eb9912
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,48 @@
+# Code of Conduct
+
+Keep project interaction technical, professional, and useful.
+
+## Expected behavior
+
+- Criticize code, design, tests, or reasoning. Do not attack the person.
+- Keep issues, reviews, and discussions on topic.
+- Back technical claims with evidence when possible.
+- Accept review feedback and rejected proposals without making it personal.
+- Respect privacy.
+
+Strong disagreement is fine.
+
+Harassment is not.
+
+## Unacceptable behavior
+
+The following is not accepted:
+
+- harassment or discrimination
+- threats or intimidation
+- personal attacks
+- trolling or deliberate provocation
+- publishing private information
+- sustained bad-faith disruption
+- spam, including automated or AI-generated issue and pull-request spam
+- abusive language directed at contributors or maintainers
+
+Technical criticism is not a Code of Conduct violation merely because it is direct.
+
+Keep it about the work.
+
+## Enforcement
+
+The maintainer may remove content, close issues or pull requests, restrict participation, or temporarily or permanently block contributors.
+
+Serious abuse does not require a warning before action is taken.
+
+## Reporting conduct problems
+
+If a report contains harassment, threats, private information, or anything else that should not be public, contact the maintainer privately using the contact information on the maintainer's GitHub profile.
+
+Do not create a public issue containing sensitive personal information.
+
+## One rule
+
+Be useful. Be respectful. Keep it about the work.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 3506400..51eccdc 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,29 +1,97 @@
# Contributing
-## Before changing semantics
+Keep changes focused, tested, and justified.
-Changes to standard outcome membership, standard outcome state, or a built-in HTTP/gRPC mapping change the semantic contract. Update the relevant fixture under `compatibility/` in the same pull request and explain why the semantic change is required.
+## Before changing contracts
+
+Codes has public contracts across three artifacts:
+
+- `codes`
+- `codes-spring`
+- `codes-grpc-java`
+
+Changes to standard outcome membership, standard outcome state, or built-in HTTP/gRPC mappings change the semantic contract.
+
+Public API changes require updating the matching snapshot under `api/`.
+
+Spring or gRPC wire changes require updating the matching snapshot under `compatibility/`.
+
+Do not update a snapshot just because a test failed. Understand the change and explain why the contract should move.
+
+The core artifact must remain free of runtime dependencies.
+
+Adapters must stay thin and explicit. Do not introduce auto-configuration, serialization frameworks, registries, hidden mapping rules, or unrelated abstractions as part of an adapter change.
## Build
+Run the full repository gate.
+
+Windows:
+
+```powershell
+.\gradlew.bat clean verifyAll --stacktrace
+```
+
+Linux/macOS:
+
```bash
-./gradlew clean verifyAll
-./gradlew :publishToMavenLocal :codes-spring:publishToMavenLocal :codes-grpc-java:publishToMavenLocal
-./scripts/verify-local-publications.sh
-./gradlew -p smoke-test-java clean check
-./gradlew -p smoke-test-kotlin clean check
+./gradlew clean verifyAll --stacktrace
```
-Maven consumer checks:
+For publication or dependency changes, also verify all three artifacts from a clean isolated Maven repository.
+
+Windows:
+
+```powershell
+.\scripts\prepare-compatibility-repository.ps1 `
+ -Repository .\build\compatibility-maven
+```
+
+Linux/macOS:
```bash
-version=$(./scripts/version.sh)
-mvn -f smoke-test-maven-java/pom.xml -Dcodes.version="$version" clean verify
-./scripts/verify-kotlin-nullability.sh 2.4.10
+bash scripts/prepare-compatibility-repository.sh build/compatibility-maven
```
+The CI compatibility workflow covers the supported Java, Kotlin, Spring, gRPC, Gradle/Maven consumer, and operating-system matrix.
+
## Pull requests
-Keep changes focused. Include tests for behavior changes. Public API changes require updating the matching snapshot under `api/`; semantic changes require updating the matching compatibility snapshot.
+Keep pull requests small enough to review.
+
+Include tests for behavior changes.
+
+Explain any:
+
+- public API change
+- semantic or wire change
+- dependency change
+- compatibility change
+
+Do not mix unrelated cleanup with a behavioral change.
+
+Do not weaken a compatibility, coverage, dependency, or publication check just to make CI green.
+
+## AI-assisted contributions
+
+AI-generated work without human understanding is **not** allowed.
+
+Contributions require a human who reviews, understands, and takes responsibility for the work.
+
+If AI was used, the contributor must:
+
+- understand every submitted change
+- verify factual and compatibility claims
+- run the relevant tests
+- be able to explain the design and tradeoffs
+- remove generated code that is unnecessary or outside scope
+- take responsibility for regressions
+
+The following may be rejected without detailed review:
-Do not add framework dependencies to the core artifact.
+- unreviewed generated code
+- prompt dumps
+- fabricated tests, benchmarks, or compatibility claims
+- large generated rewrites without a concrete reason
+- generated issue or pull-request text containing claims the contributor did not verify
+- "the AI said it works" as technical justification
diff --git a/README.md b/README.md
index e7afe29..3f855a8 100644
--- a/README.md
+++ b/README.md
@@ -18,25 +18,38 @@ com.example.payments:PAYMENT_DECLINED
## Install
-Gradle:
+`0.4.0-RC1` is a release candidate.
+
+Core:
```kotlin
dependencies {
- implementation("io.github.aalsanie:codes:0.3.1")
+ implementation("io.github.aalsanie:codes:0.4.0-RC1")
}
```
-Maven:
+Spring:
-```xml
-
- io.github.aalsanie
- codes
- 0.3.1
-
+```kotlin
+dependencies {
+ implementation("io.github.aalsanie:codes-spring:0.4.0-RC1")
+}
+```
+
+gRPC Java:
+
+```kotlin
+dependencies {
+ implementation("io.github.aalsanie:codes-grpc-java:0.4.0-RC1")
+}
```
-Java 17+. Zero runtime dependencies. Kotlin applications consume the same Java API with JSpecify nullability metadata.
+All artifacts require Java 17+. The core artifact has zero runtime dependencies. The Spring and gRPC artifacts depend only on the boundary libraries they adapt.
+
+For a boundary-first walkthrough:
+
+* [Spring in ten minutes](docs/ten-minute-spring.md)
+* [gRPC Java in ten minutes](docs/ten-minute-grpc.md)
## Custom outcomes
@@ -147,11 +160,29 @@ check(outcome.code == StandardOutcomes.NOT_FOUND.code)
check(status?.value == 404)
```
+## When not to use Codes
+
+Do not add Codes only to standardize a single controller's error body. Framework-native errors are usually enough for a small application with one boundary.
+
+Codes is also the wrong tool when:
+
+* the application does not need a stable outcome identity outside one protocol boundary;
+* you want a `Result`, `Either`, validation framework, exception hierarchy, or business workflow engine;
+* you want Spring Boot auto-configuration, exception scanning, annotations, or hidden mapping conventions;
+* an existing public error schema is fixed and migration cost is larger than the value of cross-boundary identity;
+* you need protocol adapters beyond the ones Codes actually provides and do not want to own that adapter;
+* you need a central outcome registry, governance service, code generator, or schema distribution system;
+* the application has not yet decided which domain outcomes are stable enough to become machine identities.
+
+Codes is useful when the identity itself matters independently of HTTP or gRPC. If that is not true, another abstraction is probably unnecessary.
+
## Reference
+
* [Semantic contract](docs/semantic-contract.md)
* [HTTP and gRPC mappings](docs/protocol-mappings.md)
* [Compatibility policy](docs/compatibility-policy.md)
* [Artifact contracts](docs/artifact-contracts.md)
+* [RC1 real-adopter gate](docs/rc1-adopter-gate.md)
## License
diff --git a/SECURITY.md b/SECURITY.md
index aac6568..38b9d92 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -1,15 +1,47 @@
# Security
-## Reporting
+## Reporting a vulnerability
-Do not open a public issue for a vulnerability that could put users at risk before a fix is available.
+Do **not** disclose a suspected vulnerability in a public GitHub issue, pull request, discussion, or comment.
-Use GitHub's private vulnerability reporting for this repository when available. If private reporting is unavailable, contact the maintainer through the contact information on the GitHub profile and provide the affected version, impact, reproduction steps, and any proposed mitigation.
+Use GitHub's private vulnerability reporting / Security Advisories for this repository.
+
+If private reporting is temporarily unavailable, contact the maintainer using the contact information published on the maintainer's GitHub profile and clearly mark the message as a security report.
+
+Do not use a public issue as a fallback.
+
+Please include, when applicable:
+
+- affected Codes version
+- affected artifact: `codes`, `codes-spring`, or `codes-grpc-java`
+- affected surface: core API, Spring boundary, gRPC boundary, publication, or build tooling
+- Java and Kotlin versions
+- Spring or gRPC version
+- Gradle or Maven version
+- operating system
+- minimal reproduction steps
+- impact and required preconditions
+- any proof-of-concept material needed to reproduce safely
+- any known mitigation
+
+The maintainer will review valid private reports, coordinate remediation, and publish a security advisory when disclosure is appropriate.
+
+Please avoid public disclosure until a fix or coordinated disclosure date is available.
## Supported versions
-Security fixes are applied to the latest released minor line. Pre-1.0 versions may require upgrading to receive a fix.
+Security fixes target the latest stable release line.
+
+Pre-release versions may receive a fix through a newer release candidate or the final release rather than a patch to the affected pre-release.
+
+Older release lines are not guaranteed to receive security fixes.
## Boundary data
-Codes treats occurrence `detail` and `Issue` content as application-controlled data. Framework adapters do not expose those fields by default. See `docs/boundary-exposure.md`.
+Codes can carry application-controlled occurrence `detail` and structured `Issue` content.
+
+The Spring and gRPC adapters use conservative defaults and do not expose protected occurrence detail unless the application explicitly opts in.
+
+A case where safe defaults expose protected data is security-relevant and should be reported privately.
+
+Suspected compromise of a published artifact, signature, or release process should also be reported privately.
diff --git a/adoption/rc1/integration-reliability-platform.patch b/adoption/rc1/integration-reliability-platform.patch
new file mode 100644
index 0000000..e979e18
--- /dev/null
+++ b/adoption/rc1/integration-reliability-platform.patch
@@ -0,0 +1,159 @@
+--- a/build.gradle.kts
++++ b/build.gradle.kts
+@@ -6,6 +6,8 @@
+
+ group = "io.github.aalsanie"
+ version = "0.0.1-SNAPSHOT"
++
++val codesVersion = providers.gradleProperty("codesVersion").orElse("0.4.0-RC1")
+
+ java {
+ toolchain {
+@@ -18,6 +20,7 @@
+ }
+
+ dependencies {
++ implementation("io.github.aalsanie:codes-spring:${codesVersion.get()}")
+ implementation("org.springframework.boot:spring-boot-starter-data-jpa")
+ implementation("org.springframework.boot:spring-boot-starter-flyway")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
+--- a/src/main/java/io/github/aalsanie/irp/common/api/GlobalExceptionHandler.java
++++ b/src/main/java/io/github/aalsanie/irp/common/api/GlobalExceptionHandler.java
+@@ -1,5 +1,8 @@
+ package io.github.aalsanie.irp.common.api;
+
++import io.github.aalsanie.codes.Outcome;
++import io.github.aalsanie.codes.StandardOutcomes;
++import io.github.aalsanie.codes.spring.OutcomeProblemDetailMapper;
+ import io.github.aalsanie.irp.connections.DuplicateConnectionException;
+ import io.github.aalsanie.irp.events.DuplicateInboundEventException;
+ import io.github.aalsanie.irp.events.EventNotFoundException;
+@@ -7,6 +10,7 @@
+ import io.github.aalsanie.irp.events.InvalidEventProcessingStatus;
+ import jakarta.servlet.http.HttpServletRequest;
+ import org.springframework.http.HttpStatus;
++import org.springframework.http.ProblemDetail;
+ import org.springframework.http.ResponseEntity;
+ import org.springframework.web.bind.annotation.ExceptionHandler;
+ import org.springframework.web.bind.annotation.RestControllerAdvice;
+@@ -15,6 +19,8 @@
+
+ @RestControllerAdvice
+ public class GlobalExceptionHandler {
++ private static final OutcomeProblemDetailMapper CODES_PROBLEMS =
++ OutcomeProblemDetailMapper.safeDefaults();
+
+ @ExceptionHandler(value = {DuplicateConnectionException.class})
+ public ResponseEntity handleException(DuplicateConnectionException exception,
+@@ -53,14 +59,16 @@
+ }
+
+ @ExceptionHandler(value = {DuplicateInboundEventException.class})
+- public ResponseEntity handleException(DuplicateInboundEventException exception, HttpServletRequest request) {
+- HttpStatus status = HttpStatus.CONFLICT;
+- ApiErrorResponse response = new ApiErrorResponse(Instant.now(),
+- status.value(),
+- exception.getMessage(),
+- status.getReasonPhrase(),
+- request.getRequestURI());
+- return ResponseEntity.status(HttpStatus.CONFLICT).body(response);
++ public ResponseEntity handleException(DuplicateInboundEventException exception) {
++ ProblemDetail problem = CODES_PROBLEMS
++ .map(Outcome.of(StandardOutcomes.ALREADY_EXISTS))
++ .orNull();
++
++ if (problem == null) {
++ throw new IllegalStateException("ALREADY_EXISTS has no HTTP mapping", exception);
++ }
++
++ return ResponseEntity.status(problem.getStatus()).body(problem);
+ }
+
+ @ExceptionHandler(value = {InvalidEventProcessingStatus.class})
+--- /dev/null
++++ b/src/test/java/io/github/aalsanie/irp/CodesSpringAdoptionTest.java
+@@ -0,0 +1,84 @@
++package io.github.aalsanie.irp;
++
++import io.github.aalsanie.irp.connections.ConnectionStatus;
++import io.github.aalsanie.irp.connections.IntegrationConnection;
++import io.github.aalsanie.irp.connections.IntegrationConnectionRepository;
++import io.github.aalsanie.irp.events.EventRepository;
++import org.junit.jupiter.api.BeforeEach;
++import org.junit.jupiter.api.Test;
++import org.springframework.beans.factory.annotation.Autowired;
++import org.springframework.boot.test.context.SpringBootTest;
++import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
++import org.springframework.context.annotation.Import;
++import org.springframework.http.MediaType;
++import org.springframework.test.web.servlet.MockMvc;
++
++import java.time.Instant;
++import java.util.UUID;
++
++import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
++import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
++import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
++import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
++
++@SpringBootTest
++@AutoConfigureMockMvc
++@Import(TestcontainersConfiguration.class)
++class CodesSpringAdoptionTest {
++
++ @Autowired
++ private MockMvc mockMvc;
++
++ @Autowired
++ private EventRepository eventRepository;
++
++ @Autowired
++ private IntegrationConnectionRepository connectionRepository;
++
++ @BeforeEach
++ void setUp() {
++ eventRepository.deleteAll();
++ connectionRepository.deleteAll();
++ }
++
++ @Test
++ void duplicateInboundEventKeepsStableCodesIdentityWithoutOccurrenceDetail()
++ throws Exception {
++ IntegrationConnection connection = connectionRepository.saveAndFlush(
++ new IntegrationConnection(
++ UUID.randomUUID(),
++ "pilot",
++ "stripe",
++ ConnectionStatus.ACTIVE,
++ Instant.now()
++ )
++ );
++
++ String request = """
++ {
++ "externalEventId": "evt_rc1",
++ "eventType": "payment.succeeded",
++ "payload": {
++ "paymentId": "pay_rc1"
++ }
++ }
++ """;
++
++ String endpoint = "/api/v1/connections/" + connection.getId() + "/events";
++
++ mockMvc.perform(post(endpoint)
++ .contentType(MediaType.APPLICATION_JSON)
++ .content(request))
++ .andExpect(status().isCreated());
++
++ mockMvc.perform(post(endpoint)
++ .contentType(MediaType.APPLICATION_JSON)
++ .content(request))
++ .andExpect(status().isConflict())
++ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON))
++ .andExpect(jsonPath("$.status").value(409))
++ .andExpect(jsonPath("$.code")
++ .value("io.github.aalsanie.codes.standard:ALREADY_EXISTS"))
++ .andExpect(jsonPath("$.detail").doesNotExist());
++ }
++}
diff --git a/adoption/rc1/manifest.json b/adoption/rc1/manifest.json
new file mode 100644
index 0000000..12a6dbf
--- /dev/null
+++ b/adoption/rc1/manifest.json
@@ -0,0 +1,29 @@
+{
+ "version": "0.4.0-RC1",
+ "pilots": [
+ {
+ "id": "integration-reliability-platform",
+ "repository": "https://github.com/aalsanie/integration-reliability-platform.git",
+ "commit": "1f99ad8ec4b0b7da2a20ab20101a6adb677cb735",
+ "patch": "adoption/rc1/integration-reliability-platform.patch",
+ "boundary": "Spring MVC",
+ "differentiator": "incremental replacement of a handwritten HTTP error mapping",
+ "manual_mapping_removed": "1 handwritten duplicate-event HTTP status/payload mapping",
+ "dependency_conflicts": "None expected; the application already uses Spring Boot 4.1.0 and owns Spring dependency management.",
+ "missing_api": "None identified before execution.",
+ "expected_identity": "io.github.aalsanie.codes.standard:ALREADY_EXISTS"
+ },
+ {
+ "id": "patient-mgmt-microservices",
+ "repository": "https://github.com/pratham2402/patient-mgmt-microservices.git",
+ "commit": "26645990986a4f17b755ec17ed390c9e90112d36",
+ "patch": "adoption/rc1/patient-mgmt-microservices.patch",
+ "boundary": "Spring MVC + gRPC Java",
+ "differentiator": "the same INVALID_ARGUMENT identity is exercised through two real protocol boundaries",
+ "manual_mapping_removed": "1 handwritten Spring validation Map response; the gRPC validation path did not previously exist",
+ "dependency_conflicts": "The application pins gRPC 1.69.0, below the Codes 1.75.0 floor. The pilot upgrades the application gRPC line and protoc gRPC plugin to 1.75.0 and imports the 1.75.0 gRPC BOM.",
+ "missing_api": "None identified before execution.",
+ "expected_identity": "io.github.aalsanie.codes.standard:INVALID_ARGUMENT"
+ }
+ ]
+}
diff --git a/adoption/rc1/patient-mgmt-microservices.patch b/adoption/rc1/patient-mgmt-microservices.patch
new file mode 100644
index 0000000..d292568
--- /dev/null
+++ b/adoption/rc1/patient-mgmt-microservices.patch
@@ -0,0 +1,365 @@
+--- a/patient-service/pom.xml
++++ b/patient-service/pom.xml
+@@ -28,8 +28,14 @@
+
+
+ 21
++ 0.4.0-RC1
+
+
++
++ io.github.aalsanie
++ codes-spring
++ ${codes.version}
++
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+--- a/patient-service/src/main/java/com/pm/patientservice/exception/GlobalExceptionHandler.java
++++ b/patient-service/src/main/java/com/pm/patientservice/exception/GlobalExceptionHandler.java
+@@ -1,31 +1,57 @@
+ package com.pm.patientservice.exception;
+
+-import jakarta.validation.constraints.Email;
++import io.github.aalsanie.codes.Issue;
++import io.github.aalsanie.codes.Outcome;
++import io.github.aalsanie.codes.StandardOutcomes;
++import io.github.aalsanie.codes.spring.OutcomeProblemDetailMapper;
++import io.github.aalsanie.codes.spring.SpringHttpStatusMapper;
++import io.github.aalsanie.codes.spring.SpringOutcomeExposure;
++import io.github.aalsanie.codes.spring.SpringProblemTypeUriMapper;
+ import org.slf4j.Logger;
+ import org.slf4j.LoggerFactory;
++import org.springframework.http.ProblemDetail;
+ import org.springframework.http.ResponseEntity;
+ import org.springframework.web.bind.MethodArgumentNotValidException;
+ import org.springframework.web.bind.annotation.ControllerAdvice;
+ import org.springframework.web.bind.annotation.ExceptionHandler;
+
++import java.util.ArrayList;
+ import java.util.HashMap;
++import java.util.List;
+ import java.util.Map;
+
+ @ControllerAdvice
+ public class GlobalExceptionHandler {
+
+ private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
++ private static final OutcomeProblemDetailMapper VALIDATION_PROBLEMS =
++ new OutcomeProblemDetailMapper(
++ SpringHttpStatusMapper.standard(),
++ SpringOutcomeExposure.publicErrors(),
++ SpringProblemTypeUriMapper.empty()
++ );
+
+ @ExceptionHandler(MethodArgumentNotValidException.class)
+- public ResponseEntity