From ca6cd6a9b7fab82c961c53cf8173b49641c7da17 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Wed, 9 Sep 2026 18:13:13 +0300 Subject: [PATCH 1/5] update(docs): integration gate --- LICENSE | 2 +- README.md | 2 +- docs/{rc1-adopter-gate.md => rc1-integration-gate.md} | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename docs/{rc1-adopter-gate.md => rc1-integration-gate.md} (98%) diff --git a/LICENSE b/LICENSE index d645695..964183e 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright [yyyy] [name of copyright owner] + Copyright 2026 Ahmad Al-Sanie Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/README.md b/README.md index c630b0d..cd0d321 100644 --- a/README.md +++ b/README.md @@ -180,7 +180,7 @@ Codes is useful when the identity itself matters independently of HTTP or gRPC. * [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) +* [RC1 external integration gate](docs/rc1-integration-gate.md) ## License diff --git a/docs/rc1-adopter-gate.md b/docs/rc1-integration-gate.md similarity index 98% rename from docs/rc1-adopter-gate.md rename to docs/rc1-integration-gate.md index 36a2bb6..e676bd4 100644 --- a/docs/rc1-adopter-gate.md +++ b/docs/rc1-integration-gate.md @@ -1,4 +1,4 @@ -# 0.4.0-RC1 real-adopter gate +# RC1 external integration gate The RC is not accepted because it has downloads, stars, or because the Codes repository's own reference applications compile. From 8cd3faf85531b8a0fd0ccb9ece54bba8b0dc984f Mon Sep 17 00:00:00 2001 From: aalsanie Date: Wed, 9 Sep 2026 18:34:42 +0300 Subject: [PATCH 2/5] update(docs): integration gate --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index 964183e..d645695 100644 --- a/LICENSE +++ b/LICENSE @@ -187,7 +187,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2026 Ahmad Al-Sanie + Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. From 44175ffbcbcae2a773def40dadee6f0823652592 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Wed, 9 Sep 2026 18:58:48 +0300 Subject: [PATCH 3/5] docs: clean up public documentation and RC verification --- .github/workflows/release-candidate.yml | 16 +- CHANGELOG.md | 56 +-- README.md | 39 +- .../integration-reliability-platform.patch | 159 ++++++++ compatibility/external/manifest.json | 24 ++ .../external/patient-mgmt-microservices.patch | 365 ++++++++++++++++++ docs/artifact-contracts.md | 15 - docs/compatibility-policy.md | 37 +- docs/rc1-integration-gate.md | 89 ----- docs/semantic-contract.md | 4 +- docs/ten-minute-grpc.md | 2 +- docs/ten-minute-spring.md | 4 +- scripts/verify-rc-integrations.sh | 216 +++++++++++ 13 files changed, 824 insertions(+), 202 deletions(-) create mode 100644 compatibility/external/integration-reliability-platform.patch create mode 100644 compatibility/external/manifest.json create mode 100644 compatibility/external/patient-mgmt-microservices.patch delete mode 100644 docs/artifact-contracts.md delete mode 100644 docs/rc1-integration-gate.md create mode 100644 scripts/verify-rc-integrations.sh diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index b17515c..8e8d678 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -106,7 +106,7 @@ jobs: ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_IN_MEMORY_KEY }} ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_IN_MEMORY_KEY_PASSWORD }} - real-adopters: + external-integrations: needs: publish runs-on: ubuntu-latest timeout-minutes: 45 @@ -121,24 +121,24 @@ jobs: distribution: temurin java-version: "21" - - name: Verify two external adopters from Maven Central + - name: Verify external integrations from Maven Central run: >- - bash scripts/verify-rc-adopters.sh + bash scripts/verify-rc-integrations.sh "${{ needs.publish.outputs.version }}" - build/reports/rc1-adopters.md + build/reports/rc-integrations.md - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a if: always() with: - name: rc1-adopter-gate + name: rc-external-integrations path: | - build/reports/rc1-adopters.md - build/rc-adopters/*.log + build/reports/rc-integrations.md + build/rc-integrations/*.log if-no-files-found: warn retention-days: 30 github-prerelease: - needs: [publish, real-adopters] + needs: [publish, external-integrations] runs-on: ubuntu-latest timeout-minutes: 10 permissions: diff --git a/CHANGELOG.md b/CHANGELOG.md index 5047050..c2ae6ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,69 +6,43 @@ ### 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 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. -* Added decoded gRPC wire contracts covering safe, public, and explicitly exposed `google.rpc.Status` payloads. -* Added gRPC `StatusRuntimeException` trailer round-trip verification. -* 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. -* Added a final-release promotion gate that requires a successful same-line RC and allows only version substitution between the accepted RC and final release. +* Added `codes-spring` for mapping failed outcomes to Spring RFC 9457 `ProblemDetail`. +* Added application-owned problem-type URI mappings and explicit Spring exposure policies. +* Added conversion from failed outcomes to Spring `ErrorResponseException`. +* Added `codes-grpc-java` for mapping failed outcomes to `google.rpc.Status` with stable identity in `ErrorInfo`. +* Added conversion to gRPC `StatusRuntimeException` and structured request-field issues through `BadRequest` for `INVALID_ARGUMENT` and `OUT_OF_RANGE`. +* Added Spring and gRPC integration guides. ### Changed -* 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. -* Enforced lossless Codes identity compatibility with `google.rpc.ErrorInfo.domain` and `ErrorInfo.reason`. -* Restricted exposed `google.rpc.BadRequest` issues to `INVALID_ARGUMENT` and `OUT_OF_RANGE`, requiring request-field paths and rejecting incompatible representations. -* 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. +* Set all published artifacts to `0.4.0-RC1`. +* Spring problem details preserve Codes identity in the `code` extension, use the reusable outcome message as the title for explicitly mapped problem types, and keep occurrence detail in RFC `detail`. +* gRPC mappings reject lossy `ErrorInfo` identities and incompatible `BadRequest` issue representations instead of normalizing or coercing them. ## 0.3.1 ### Changed * Restored the published core to zero runtime dependencies. -* Kept JSpecify as a compile-time-only dependency for nullability metadata. +* Kept JSpecify as compile-time-only nullability metadata. * Preserved Kotlin nullability semantics across the supported Kotlin compiler matrix. -### Added - -* Added publication verification that prevents runtime dependencies from being introduced. -* Added verification that the published Maven POM remains dependency-free. - ## 0.3.0 ### Changed * Reimplemented the published core in Java 17. -* Removed the Kotlin standard library from the core dependency graph. -* Added `org.jspecify:jspecify:1.0.0` as the core's only dependency for nullability annotations. -* Replaced Kotlin-specific ABI compatibility with a deterministic public Java API snapshot. +* Removed the Kotlin standard library from the core runtime dependency graph. +* Added JSpecify nullability metadata for Java and Kotlin consumers. * Preserved the 17 standard outcomes, `OutcomeState` assignments, and built-in HTTP and gRPC mappings from `0.2.0`. -### Added - -* Added Gradle and Maven consumer verification for Java and Kotlin. -* Added Kotlin compiler compatibility and JSpecify nullability contract verification. -* Added publication checks for the expected runtime and Maven dependency contract. - ### Removed * Removed Kotlin compiler-generated API such as `Companion`, default-argument bridges, and `OutcomeState.entries`. * Removed the Kotlin `Outcome.toException` extension; use `OutcomeExceptions.toException(...)` or `new OutcomeException(...)`. ### Breaking + `0.3.0` is source and binary incompatible with `0.2.x` for callers that depend on the removed Kotlin-generated API or `Outcome.toException`. The standard outcome identities, states, and built-in protocol mappings are unchanged from `0.2.0`. @@ -85,10 +59,6 @@ The standard outcome identities, states, and built-in protocol mappings are unch * Removed `StandardOutcomes.CREATED`, `ACCEPTED`, `NO_CONTENT`, and `PAYLOAD_TOO_LARGE`. -### Added - -* Added semantic compatibility checks for standard outcome codes, states, and built-in mappings. - ## 0.1.0 * Initial public release. diff --git a/README.md b/README.md index cd0d321..ca08864 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,23 @@ [![CI](https://github.com/aalsanie/codes/actions/workflows/ci.yml/badge.svg)](https://github.com/aalsanie/codes/actions/workflows/ci.yml) [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) -Codes provides stable application outcome identities and explicit boundary mappings for JVM applications. Applications keep their own domain result or error model and use Codes where multiple parts of a system need to agree on outcome meaning without coupling that meaning to HTTP, gRPC, serialization, or a framework. - -A domain outcome can keep the same identity across boundaries: +Codes provides stable application outcome identities and explicit boundary mappings for JVM applications. Applications keep their existing domain result or error model; Codes gives the system one machine identity for an outcome while HTTP and gRPC remain boundary decisions. ```text -com.example.payments:PAYMENT_DECLINED - | - +-- HTTP 422 - +-- gRPC FAILED_PRECONDITION - +-- logs/metrics keep PAYMENT_DECLINED +com.example.orders:ORDER_NOT_FOUND + -> HTTP 404 + -> gRPC NOT_FOUND + -> metric label + -> log identity + -> test assertion ``` +The same `OutcomeCode` can be used wherever application code, observability, and protocol boundaries need to agree on the outcome. + ## Install +`0.4.0-RC1` is the current pre-release on Maven Central. + Core: ```kotlin @@ -120,7 +123,7 @@ ValidationResult validation = ValidationResult.invalid( Outcome outcome = validation.toOutcome(StandardOutcomes.INVALID_ARGUMENT); ``` -`ValidationResult` is a small convenience for aggregating issues. It is not intended to replace an application's result, validation, or functional programming model. +`ValidationResult` is a small convenience for aggregating issues and converting them into an outcome. ## HTTP @@ -158,29 +161,11 @@ 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 external integration gate](docs/rc1-integration-gate.md) ## License diff --git a/compatibility/external/integration-reliability-platform.patch b/compatibility/external/integration-reliability-platform.patch new file mode 100644 index 0000000..505165b --- /dev/null +++ b/compatibility/external/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/CodesSpringIntegrationTest.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 CodesSpringIntegrationTest { ++ ++ @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(), ++ "integration-test", ++ "stripe", ++ ConnectionStatus.ACTIVE, ++ Instant.now() ++ ) ++ ); ++ ++ String request = """ ++ { ++ "externalEventId": "evt_codes", ++ "eventType": "payment.succeeded", ++ "payload": { ++ "paymentId": "pay_codes" ++ } ++ } ++ """; ++ ++ 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/compatibility/external/manifest.json b/compatibility/external/manifest.json new file mode 100644 index 0000000..d05a599 --- /dev/null +++ b/compatibility/external/manifest.json @@ -0,0 +1,24 @@ +{ + "integrations": [ + { + "id": "integration-reliability-platform", + "repository": "https://github.com/aalsanie/integration-reliability-platform.git", + "commit": "1f99ad8ec4b0b7da2a20ab20101a6adb677cb735", + "patch": "compatibility/external/integration-reliability-platform.patch", + "boundary": "Spring MVC", + "scope": "replace one handwritten duplicate-event HTTP mapping with codes-spring", + "dependency_notes": "The application uses Spring Boot 4.1.0 and owns Spring dependency management.", + "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": "compatibility/external/patient-mgmt-microservices.patch", + "boundary": "Spring MVC + gRPC Java", + "scope": "exercise INVALID_ARGUMENT through Spring MVC and gRPC Java", + "dependency_notes": "The repository pins gRPC 1.69.0; the integration patch updates that line to the Codes 1.75.0 compatibility floor.", + "expected_identity": "io.github.aalsanie.codes.standard:INVALID_ARGUMENT" + } + ] +} diff --git a/compatibility/external/patient-mgmt-microservices.patch b/compatibility/external/patient-mgmt-microservices.patch new file mode 100644 index 0000000..7f78791 --- /dev/null +++ b/compatibility/external/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> handleValidationException( ++ public ResponseEntity handleValidationException( + MethodArgumentNotValidException ex) { + +- Map errors = new HashMap<>(); ++ List issues = new ArrayList<>(); ++ ex.getBindingResult().getFieldErrors().forEach(error -> { ++ String message = error.getDefaultMessage() == null ++ ? "Invalid value." ++ : error.getDefaultMessage(); ++ issues.add(Issue.at(error.getField(), message)); ++ }); + +- ex.getBindingResult().getFieldErrors().forEach( +- error -> errors.put(error.getField(), error.getDefaultMessage())); ++ ProblemDetail problem = VALIDATION_PROBLEMS ++ .map(Outcome.of(StandardOutcomes.INVALID_ARGUMENT, null, issues)) ++ .orNull(); + +- return ResponseEntity.badRequest().body(errors); ++ if (problem == null) { ++ throw new IllegalStateException("INVALID_ARGUMENT has no HTTP mapping"); ++ } ++ ++ return ResponseEntity.status(problem.getStatus()).body(problem); + } + + @ExceptionHandler(EmailAlreadyExistsException.class) +--- /dev/null ++++ b/patient-service/src/test/java/com/pm/patientservice/exception/CodesValidationBoundaryTest.java +@@ -0,0 +1,55 @@ ++package com.pm.patientservice.exception; ++ ++import jakarta.validation.Valid; ++import jakarta.validation.constraints.NotBlank; ++import org.junit.jupiter.api.Test; ++import org.springframework.beans.factory.annotation.Autowired; ++import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; ++import org.springframework.context.annotation.Import; ++import org.springframework.http.MediaType; ++import org.springframework.web.bind.annotation.PostMapping; ++import org.springframework.web.bind.annotation.RequestBody; ++import org.springframework.web.bind.annotation.RestController; ++import org.springframework.test.web.servlet.MockMvc; ++ ++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; ++ ++@WebMvcTest(controllers = CodesValidationBoundaryTest.TestController.class) ++@Import(GlobalExceptionHandler.class) ++class CodesValidationBoundaryTest { ++ ++ @Autowired ++ private MockMvc mockMvc; ++ ++ @Test ++ void validationKeepsCodesIdentityAndStructuredIssuesAtHttpBoundary() ++ throws Exception { ++ mockMvc.perform(post("/codes-test/validate") ++ .contentType(MediaType.APPLICATION_JSON) ++ .content(""" ++ { ++ "email": "" ++ } ++ """)) ++ .andExpect(status().isBadRequest()) ++ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON)) ++ .andExpect(jsonPath("$.status").value(400)) ++ .andExpect(jsonPath("$.code") ++ .value("io.github.aalsanie.codes.standard:INVALID_ARGUMENT")) ++ .andExpect(jsonPath("$.issues[0].path").value("email")) ++ .andExpect(jsonPath("$.detail").doesNotExist()); ++ } ++ ++ @RestController ++ static class TestController { ++ @PostMapping("/codes-test/validate") ++ void validate(@Valid @RequestBody TestRequest request) { ++ } ++ } ++ ++ record TestRequest(@NotBlank String email) { ++ } ++} +--- a/billing-service/pom.xml ++++ b/billing-service/pom.xml +@@ -28,8 +28,26 @@ + + + 21 ++ 0.4.0-RC1 ++ 1.75.0 + ++ ++ ++ ++ io.grpc ++ grpc-bom ++ ${grpc.version} ++ pom ++ import ++ ++ ++ + ++ ++ io.github.aalsanie ++ codes-grpc-java ++ ${codes.version} ++ + + org.springframework.boot + spring-boot-starter-web +@@ -45,17 +63,17 @@ + + io.grpc + grpc-netty-shaded +- 1.69.0 ++ ${grpc.version} + + + io.grpc + grpc-protobuf +- 1.69.0 ++ ${grpc.version} + + + io.grpc + grpc-stub +- 1.69.0 ++ ${grpc.version} + + + org.apache.tomcat +@@ -103,7 +121,7 @@ + com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} + grpc-java + +- io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier} ++ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} + + + +--- a/billing-service/src/main/java/com/pm/billingservice/grpc/BillingGrpcService.java ++++ b/billing-service/src/main/java/com/pm/billingservice/grpc/BillingGrpcService.java +@@ -1,21 +1,58 @@ + package com.pm.billingservice.grpc; + ++import billing.BillingRequest; + import billing.BillingResponse; + import billing.BillingServiceGrpc.BillingServiceImplBase; ++import io.github.aalsanie.codes.Issue; ++import io.github.aalsanie.codes.Outcome; ++import io.github.aalsanie.codes.StandardOutcomes; ++import io.github.aalsanie.codes.grpc.GoogleRpcOutcomeMapper; ++import io.github.aalsanie.codes.grpc.GrpcOutcomeExceptions; ++import io.github.aalsanie.codes.grpc.GrpcOutcomeExposure; ++import io.github.aalsanie.codes.protocol.grpc.GrpcOutcomeMapper; ++import io.grpc.StatusRuntimeException; + import io.grpc.stub.StreamObserver; + import net.devh.boot.grpc.server.service.GrpcService; + import org.slf4j.Logger; + import org.slf4j.LoggerFactory; + ++import java.util.ArrayList; ++import java.util.List; ++ + @GrpcService + public class BillingGrpcService extends BillingServiceImplBase { + private static final Logger log = LoggerFactory.getLogger(BillingGrpcService.class); ++ private static final GoogleRpcOutcomeMapper ERRORS = ++ new GoogleRpcOutcomeMapper( ++ GrpcOutcomeMapper.standard(), ++ GrpcOutcomeExposure.publicErrors() ++ ); + + @Override +- public void createBillingAccount(billing.BillingRequest billingRequest, ++ public void createBillingAccount(BillingRequest billingRequest, + StreamObserver responseObserver) { + + log.info("createBillingAccount request received {}", billingRequest.toString()); ++ ++ List issues = validationIssues(billingRequest); ++ if (!issues.isEmpty()) { ++ Outcome outcome = Outcome.of( ++ StandardOutcomes.INVALID_ARGUMENT, ++ null, ++ issues ++ ); ++ ++ StatusRuntimeException error = GrpcOutcomeExceptions ++ .toStatusRuntimeException(outcome, ERRORS) ++ .orNull(); ++ ++ if (error == null) { ++ throw new IllegalStateException("INVALID_ARGUMENT has no gRPC mapping"); ++ } ++ ++ responseObserver.onError(error); ++ return; ++ } + + // Business logic - e.g. save to database, perform calculations, etc + +@@ -27,4 +64,20 @@ + responseObserver.onNext(response); + responseObserver.onCompleted(); + } ++ ++ private static List validationIssues(BillingRequest request) { ++ List issues = new ArrayList<>(); ++ ++ if (request.getPatientId().isBlank()) { ++ issues.add(Issue.at("patientId", "Patient id is required.")); ++ } ++ if (request.getName().isBlank()) { ++ issues.add(Issue.at("name", "Name is required.")); ++ } ++ if (request.getEmail().isBlank()) { ++ issues.add(Issue.at("email", "Email is required.")); ++ } ++ ++ return issues; ++ } + } +--- /dev/null ++++ b/billing-service/src/test/java/com/pm/billingservice/grpc/BillingGrpcServiceCodesTest.java +@@ -0,0 +1,79 @@ ++package com.pm.billingservice.grpc; ++ ++import billing.BillingRequest; ++import billing.BillingResponse; ++import com.google.protobuf.Any; ++import com.google.rpc.BadRequest; ++import com.google.rpc.ErrorInfo; ++import io.grpc.StatusRuntimeException; ++import io.grpc.protobuf.StatusProto; ++import io.grpc.stub.StreamObserver; ++import org.junit.jupiter.api.Test; ++ ++import java.util.concurrent.atomic.AtomicReference; ++ ++import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertInstanceOf; ++import static org.junit.jupiter.api.Assertions.assertNotNull; ++import static org.junit.jupiter.api.Assertions.assertNull; ++ ++class BillingGrpcServiceCodesTest { ++ ++ @Test ++ void invalidRequestKeepsCodesIdentityAndStructuredIssuesInTrailers() ++ throws Exception { ++ AtomicReference failure = new AtomicReference<>(); ++ AtomicReference response = new AtomicReference<>(); ++ ++ StreamObserver observer = new StreamObserver<>() { ++ @Override ++ public void onNext(BillingResponse value) { ++ response.set(value); ++ } ++ ++ @Override ++ public void onError(Throwable throwable) { ++ failure.set(throwable); ++ } ++ ++ @Override ++ public void onCompleted() { ++ } ++ }; ++ ++ new BillingGrpcService().createBillingAccount( ++ BillingRequest.getDefaultInstance(), ++ observer ++ ); ++ ++ assertNull(response.get()); ++ ++ StatusRuntimeException exception = ++ assertInstanceOf(StatusRuntimeException.class, failure.get()); ++ ++ com.google.rpc.Status status = StatusProto.fromThrowable(exception); ++ assertNotNull(status); ++ assertEquals(3, status.getCode()); ++ ++ ErrorInfo identity = null; ++ BadRequest badRequest = null; ++ ++ for (Any detail : status.getDetailsList()) { ++ if (detail.is(ErrorInfo.class)) { ++ identity = detail.unpack(ErrorInfo.class); ++ } else if (detail.is(BadRequest.class)) { ++ badRequest = detail.unpack(BadRequest.class); ++ } ++ } ++ ++ assertNotNull(identity); ++ assertEquals("io.github.aalsanie.codes.standard", identity.getDomain()); ++ assertEquals("INVALID_ARGUMENT", identity.getReason()); ++ ++ assertNotNull(badRequest); ++ assertEquals(3, badRequest.getFieldViolationsCount()); ++ assertEquals("patientId", badRequest.getFieldViolations(0).getField()); ++ assertEquals("name", badRequest.getFieldViolations(1).getField()); ++ assertEquals("email", badRequest.getFieldViolations(2).getField()); ++ } ++} diff --git a/docs/artifact-contracts.md b/docs/artifact-contracts.md deleted file mode 100644 index ce7e3f0..0000000 --- a/docs/artifact-contracts.md +++ /dev/null @@ -1,15 +0,0 @@ -# Artifact contracts - -Codes `0.4.0` has three independently consumable Maven artifacts with one shared version. - -| Artifact | Purpose | Direct published dependency budget | -| --- | --- | --- | -| `io.github.aalsanie:codes` | Framework-independent outcome model and protocol mappings | No dependencies | -| `io.github.aalsanie:codes-spring` | Spring HTTP boundary mapping | `codes` and `spring-web`, both compile scope | -| `io.github.aalsanie:codes-grpc-java` | gRPC Java and `google.rpc` boundary mapping | `codes`, `grpc-api`, and `proto-google-common-protos` at compile scope; `grpc-protobuf` at runtime scope | - -The adapter budgets are exact, not maximums. A direct dependency addition or scope change must update the relevant enforced POM contract and explain why it belongs in the public artifact. - -The core remains framework-independent and dependency-free. JSpecify is compile-only metadata and must not appear in the published core POM or runtime classpath. - -Public API snapshots live under `api/`. Semantic fixtures for the core catalog and built-in mappings live under `compatibility/`. Both are reviewed contracts rather than generated files that CI silently updates. diff --git a/docs/compatibility-policy.md b/docs/compatibility-policy.md index b7e7faa..3251359 100644 --- a/docs/compatibility-policy.md +++ b/docs/compatibility-policy.md @@ -4,26 +4,35 @@ Codes is pre-1.0. Minor releases may contain source or binary breaking changes. All published Codes artifacts target Java 17 and support Java and Kotlin consumers. -The published `codes` core artifact has zero runtime dependencies. `codes-spring` and `codes-grpc-java` depend only on the boundary libraries documented in their artifact contracts and verified published POM budgets. +The `codes` core artifact has zero runtime dependencies. `codes-spring` and `codes-grpc-java` depend on the boundary libraries they adapt. -The following are part of the semantic contract: +## 0.4.x compatibility -* standard outcome codes; -* standard `OutcomeState` assignments; -* built-in HTTP mappings; -* built-in gRPC mappings. +The `0.4.x` line is verified with: + +* Java 17, 21, and 25; +* Spring Framework 6.0.0 and 7.0.9; +* gRPC Java 1.75.0 and 1.83.1; +* Kotlin 1.9.24, 2.0.21, 2.1.21, 2.2.20, and 2.4.10; +* Gradle and Maven consumers; +* Linux, Windows, and macOS. + +Spring Framework 6.0.0 is the Codes compatibility floor for the Spring 6 generation. Spring 7.0.9 is the current Spring 7 verification baseline. -Public Java API compatibility is checked independently for: +gRPC Java 1.75.0 is the Codes compatibility floor. gRPC Java 1.83.1 is the current verification baseline. -* `api/codes.api`; -* `api/codes-spring.api`; -* `api/codes-grpc-java.api`. +These are Codes compatibility statements, not upstream maintenance or security-support declarations. -Boundary wire contracts are checked independently from Java API compatibility: +## Public contract -* rendered Spring RFC 9457 problem responses are frozen by `compatibility/spring-http-problems.snapshot`; -* decoded `google.rpc.Status` payloads are frozen by `compatibility/grpc-google-rpc-status.snapshot`. +The machine-readable contract includes: + +* `OutcomeCode` identity; +* standard outcome membership; +* standard `OutcomeState` assignments; +* built-in HTTP mappings; +* built-in gRPC mappings. -The executable compatibility matrix under `compatibility/` covers the supported Spring and gRPC baselines, Java runtimes, Kotlin compilers, Gradle and Maven consumers, and supported CI operating systems. Published POM checks protect the dependency contract of each artifact. +The Spring adapter preserves Codes identity in the RFC 9457 `code` extension. The gRPC adapter preserves identity in `google.rpc.ErrorInfo.domain` and `ErrorInfo.reason`. Human-readable messages are not machine identity and may change without changing `OutcomeCode`. diff --git a/docs/rc1-integration-gate.md b/docs/rc1-integration-gate.md deleted file mode 100644 index e676bd4..0000000 --- a/docs/rc1-integration-gate.md +++ /dev/null @@ -1,89 +0,0 @@ -# RC1 external integration gate - -The RC is not accepted because it has downloads, stars, or because the Codes repository's own reference applications compile. - -The gate uses two independent codebases outside this repository at pinned commits and applies small application patches that consume the published Maven Central artifacts. - -## Pilot 1 — Integration Reliability Platform - -Repository: `aalsanie/integration-reliability-platform` - -Pinned commit: `1f99ad8ec4b0b7da2a20ab20101a6adb677cb735` - -Environment: - -* Java 21 -* Spring Boot 4.1.0 -* Gradle -* existing Spring MVC error boundary -* existing Testcontainers integration suite - -The pilot replaces one existing handwritten duplicate-event HTTP mapping with `codes-spring`. The existing application remains responsible for deciding that `DuplicateInboundEventException` means `ALREADY_EXISTS`. - -Expected identity: - -```text -io.github.aalsanie.codes.standard:ALREADY_EXISTS -``` - -The pilot includes a real MockMvc integration test against the application boundary. - -## Pilot 2 — Patient Management Microservices - -Repository: `pratham2402/patient-mgmt-microservices` - -Pinned commit: `26645990986a4f17b755ec17ed390c9e90112d36` - -Environment: - -* Java 21 -* Spring Boot 3.5.5 -* Maven -* REST patient service -* gRPC billing service - -This pilot exercises the actual differentiator. - -The patient service replaces its handwritten validation `Map` response with `codes-spring`. The billing service adds validation through `codes-grpc-java`. - -Both boundaries use: - -```text -io.github.aalsanie.codes.standard:INVALID_ARGUMENT -``` - -The HTTP side verifies the RFC 9457 `code` property and structured issues. The gRPC side decodes a real `StatusRuntimeException` and verifies `ErrorInfo` plus `BadRequest`. - -The repository currently pins gRPC 1.69.0. Codes declares 1.75.0 as its `0.4.x` compatibility floor, so the pilot upgrades the application's gRPC dependency line and protoc gRPC plugin to 1.75.0. That is recorded as an adopter dependency conflict; it is not hidden with a Codes patch. - -## What the automated report records - -For each pilot the gate records: - -* repository and pinned commit; -* exact patch; -* pass/fail result; -* automated clone + patch + clean build/test elapsed time; -* changed-line statistics; -* manual boundary mapping removed; -* dependency conflicts; -* missing Codes API observed by the successful integration; -* whether any custom Codes library patch was used. - -The elapsed time is machine integration time from a clean CI workspace. It is not presented as human coding time. - -A successful report may state `Missing API: None observed`. A failed build is not converted into that claim; it fails the gate for investigation. - -## What counts as success - -Both external repositories must: - -1. clone at the pinned commit; -2. accept the stored application patch with `git apply --check`; -3. resolve `0.4.0-RC1` from Maven Central; -4. build and test without a source or binary patch to Codes; -5. verify the intended boundary identity. - -Only then is the Step 5 exit gate green. - -The patches are reproducible adopter experiments. They are not claims that the upstream maintainers have merged Codes. diff --git a/docs/semantic-contract.md b/docs/semantic-contract.md index 7c491f3..d315017 100644 --- a/docs/semantic-contract.md +++ b/docs/semantic-contract.md @@ -115,8 +115,6 @@ validation.toOutcome(StandardOutcomes.INVALID_ARGUMENT); A valid result produces `StandardOutcomes.OK`. An invalid result produces the supplied failed definition. The supplied definition is validated even for a valid result so invalid conversion policy fails immediately. -`ValidationResult` is a convenience type. It is not the application result model and does not define control flow. - ## Protocol mappings `OutcomeMapper` maps an application outcome definition to a boundary representation. @@ -163,4 +161,4 @@ Human-readable text may evolve without changing outcome identity: Consumers must use `OutcomeCode`, not human-readable text, when behavior depends on identity. -`api/codes.api` protects the public Java API shape. `compatibility/` protects semantic catalog and mapping behavior. +Runtime, framework, language, and build-tool support is documented in the [compatibility policy](compatibility-policy.md). diff --git a/docs/ten-minute-grpc.md b/docs/ten-minute-grpc.md index d6ff967..c3c8fa6 100644 --- a/docs/ten-minute-grpc.md +++ b/docs/ten-minute-grpc.md @@ -1,6 +1,6 @@ # gRPC Java in ten minutes -This example adds Codes at a gRPC server boundary. It does not replace the service's domain model. +This example adds Codes at a gRPC server boundary while the service keeps its existing domain model. ## 1. Add the adapter diff --git a/docs/ten-minute-spring.md b/docs/ten-minute-spring.md index 679e781..adcbd1a 100644 --- a/docs/ten-minute-spring.md +++ b/docs/ten-minute-spring.md @@ -1,6 +1,6 @@ # Spring in ten minutes -This example adds Codes at the HTTP boundary only. It does not replace the application's exception or domain model. +This example adds Codes at the HTTP boundary while the application keeps its existing exception and domain model. ## 1. Add the adapter @@ -75,7 +75,7 @@ final class PaymentExceptionHandler { } ``` -Codes is not discovering exceptions. The application still decides which exception means which outcome. +The application decides which exception maps to which outcome. ## Result diff --git a/scripts/verify-rc-integrations.sh b/scripts/verify-rc-integrations.sh new file mode 100644 index 0000000..5e2be7d --- /dev/null +++ b/scripts/verify-rc-integrations.sh @@ -0,0 +1,216 @@ +#!/usr/bin/env bash +set -euo pipefail + +root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +manifest="$root_dir/compatibility/external/manifest.json" +version="${1:?usage: verify-rc-integrations.sh [report]}" +report="${2:-$root_dir/build/reports/rc-integrations.md}" +work_dir="${RC_INTEGRATION_WORK_DIR:-$root_dir/build/rc-integrations}" + +if [[ ! "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+-RC[1-9][0-9]*$ ]]; then + echo "Invalid release-candidate version: $version" >&2 + exit 1 +fi + +central_base="https://repo.maven.apache.org/maven2/io/github/aalsanie" +for artifact in codes codes-spring codes-grpc-java; do + url="$central_base/$artifact/$version/$artifact-$version.pom" + found=false + + for attempt in $(seq 1 40); do + if curl --silent --show-error --fail --head "$url" >/dev/null 2>&1; then + found=true + break + fi + sleep 15 + done + + if [[ "$found" != true ]]; then + echo "Published artifact did not become visible on Maven Central: $url" >&2 + exit 1 + fi +done + +rm -rf "$work_dir" +mkdir -p "$work_dir" "$(dirname "$report")" + +{ + echo "# Codes $version external integration verification" + echo + echo "Generated: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" + echo + echo "Each integration uses a pinned external repository, applies an application-only patch, resolves Codes from Maven Central, and runs the relevant build and tests." + echo +} > "$report" + +overall=0 + +integration_value() { + local integration_id="$1" + local field="$2" + python - "$manifest" "$integration_id" "$field" <<'PY' +import json +import sys +from pathlib import Path + +manifest = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) +integration = next(item for item in manifest["integrations"] if item["id"] == sys.argv[2]) +print(integration[sys.argv[3]]) +PY +} + +run_irp() { + local id="integration-reliability-platform" + local repo commit checkout patch_file start end elapsed status log + + repo="$(integration_value "$id" repository)" + commit="$(integration_value "$id" commit)" + patch_file="$root_dir/$(integration_value "$id" patch)" + checkout="$work_dir/$id" + log="$work_dir/$id.log" + + git clone --quiet --filter=blob:none --no-checkout "$repo" "$checkout" + git -C "$checkout" checkout --quiet --detach "$commit" + git -C "$checkout" apply --check "$patch_file" + git -C "$checkout" apply "$patch_file" + + start="$(date +%s)" + status="PASS" + + if ! ( + cd "$checkout" + bash ./gradlew \ + "-PcodesVersion=$version" \ + clean test \ + --no-build-cache \ + --stacktrace + + bash ./gradlew \ + "-PcodesVersion=$version" \ + dependencyInsight \ + --dependency codes-spring \ + --configuration runtimeClasspath \ + --no-build-cache + ) >"$log" 2>&1; then + status="FAIL" + overall=1 + fi + + end="$(date +%s)" + elapsed="$((end - start))" + + { + echo "## $id" + echo + echo "- Repository: \`$repo\`" + echo "- Commit: \`$commit\`" + echo "- Boundary: $(integration_value "$id" boundary)" + echo "- Scope: $(integration_value "$id" scope)" + echo "- Result: **$status**" + echo "- Clean build/test time: ${elapsed}s" + echo "- Dependency notes: $(integration_value "$id" dependency_notes)" + echo "- Expected identity: \`$(integration_value "$id" expected_identity)\`" + echo + echo "Patch statistics:" + echo + echo '```text' + git -C "$checkout" apply --stat "$patch_file" + echo '```' + echo + echo "Build log: \`$(basename "$log")\`" + echo + } >> "$report" +} + +run_patient() { + local id="patient-mgmt-microservices" + local repo commit checkout patch_file start end elapsed status log + + repo="$(integration_value "$id" repository)" + commit="$(integration_value "$id" commit)" + patch_file="$root_dir/$(integration_value "$id" patch)" + checkout="$work_dir/$id" + log="$work_dir/$id.log" + + git clone --quiet --filter=blob:none --no-checkout "$repo" "$checkout" + git -C "$checkout" checkout --quiet --detach "$commit" + git -C "$checkout" apply --check "$patch_file" + git -C "$checkout" apply "$patch_file" + + start="$(date +%s)" + status="PASS" + + if ! ( + cd "$checkout/patient-service" + bash ./mvnw \ + --batch-mode \ + --no-transfer-progress \ + "-Dcodes.version=$version" \ + clean test + + bash ./mvnw \ + --batch-mode \ + --no-transfer-progress \ + "-Dcodes.version=$version" \ + dependency:tree \ + "-Dincludes=io.github.aalsanie:*" + + cd "$checkout/billing-service" + bash ./mvnw \ + --batch-mode \ + --no-transfer-progress \ + "-Dcodes.version=$version" \ + clean test + + bash ./mvnw \ + --batch-mode \ + --no-transfer-progress \ + "-Dcodes.version=$version" \ + dependency:tree \ + "-Dincludes=io.github.aalsanie:*,io.grpc:*" + ) >"$log" 2>&1; then + status="FAIL" + overall=1 + fi + + end="$(date +%s)" + elapsed="$((end - start))" + + { + echo "## $id" + echo + echo "- Repository: \`$repo\`" + echo "- Commit: \`$commit\`" + echo "- Boundary: $(integration_value "$id" boundary)" + echo "- Scope: $(integration_value "$id" scope)" + echo "- Result: **$status**" + echo "- Clean build/test time: ${elapsed}s" + echo "- Dependency notes: $(integration_value "$id" dependency_notes)" + echo "- Expected identity: \`$(integration_value "$id" expected_identity)\`" + echo + echo "Patch statistics:" + echo + echo '```text' + git -C "$checkout" apply --stat "$patch_file" + echo '```' + echo + echo "Build log: \`$(basename "$log")\`" + echo + } >> "$report" +} + +run_irp +run_patient + +{ + echo "## Result" + echo + if [[ "$overall" -eq 0 ]]; then + echo "**PASS** — both external integrations built successfully and verified the expected boundary identity from Maven Central." + else + echo "**FAIL** — at least one external integration failed." + fi +} >> "$report" + +cat "$report" +exit "$overall" From d61bd01ed5705dc919557db8a325f38868a97d41 Mon Sep 17 00:00:00 2001 From: aalsanie Date: Wed, 9 Sep 2026 19:16:13 +0300 Subject: [PATCH 4/5] docs: clean up public documentation and RC verification --- .github/workflows/rc-adopter-gate.yml | 44 --- .gitignore | 2 + .../integration-reliability-platform.patch | 159 -------- adoption/rc1/manifest.json | 29 -- adoption/rc1/patient-mgmt-microservices.patch | 365 ------------------ scripts/verify-rc-adopters.sh | 241 ------------ 6 files changed, 2 insertions(+), 838 deletions(-) delete mode 100644 .github/workflows/rc-adopter-gate.yml delete mode 100644 adoption/rc1/integration-reliability-platform.patch delete mode 100644 adoption/rc1/manifest.json delete mode 100644 adoption/rc1/patient-mgmt-microservices.patch delete mode 100644 scripts/verify-rc-adopters.sh diff --git a/.github/workflows/rc-adopter-gate.yml b/.github/workflows/rc-adopter-gate.yml deleted file mode 100644 index c076863..0000000 --- a/.github/workflows/rc-adopter-gate.yml +++ /dev/null @@ -1,44 +0,0 @@ -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/.gitignore b/.gitignore index 0705a1b..a30689a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ out/ .DS_Store **/target/ +__pycache__/ +*.py[cod] diff --git a/adoption/rc1/integration-reliability-platform.patch b/adoption/rc1/integration-reliability-platform.patch deleted file mode 100644 index e979e18..0000000 --- a/adoption/rc1/integration-reliability-platform.patch +++ /dev/null @@ -1,159 +0,0 @@ ---- 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 deleted file mode 100644 index 12a6dbf..0000000 --- a/adoption/rc1/manifest.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "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 deleted file mode 100644 index d292568..0000000 --- a/adoption/rc1/patient-mgmt-microservices.patch +++ /dev/null @@ -1,365 +0,0 @@ ---- 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> handleValidationException( -+ public ResponseEntity handleValidationException( - MethodArgumentNotValidException ex) { - -- Map errors = new HashMap<>(); -+ List issues = new ArrayList<>(); -+ ex.getBindingResult().getFieldErrors().forEach(error -> { -+ String message = error.getDefaultMessage() == null -+ ? "Invalid value." -+ : error.getDefaultMessage(); -+ issues.add(Issue.at(error.getField(), message)); -+ }); - -- ex.getBindingResult().getFieldErrors().forEach( -- error -> errors.put(error.getField(), error.getDefaultMessage())); -+ ProblemDetail problem = VALIDATION_PROBLEMS -+ .map(Outcome.of(StandardOutcomes.INVALID_ARGUMENT, null, issues)) -+ .orNull(); - -- return ResponseEntity.badRequest().body(errors); -+ if (problem == null) { -+ throw new IllegalStateException("INVALID_ARGUMENT has no HTTP mapping"); -+ } -+ -+ return ResponseEntity.status(problem.getStatus()).body(problem); - } - - @ExceptionHandler(EmailAlreadyExistsException.class) ---- /dev/null -+++ b/patient-service/src/test/java/com/pm/patientservice/exception/CodesValidationBoundaryTest.java -@@ -0,0 +1,55 @@ -+package com.pm.patientservice.exception; -+ -+import jakarta.validation.Valid; -+import jakarta.validation.constraints.NotBlank; -+import org.junit.jupiter.api.Test; -+import org.springframework.beans.factory.annotation.Autowired; -+import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -+import org.springframework.context.annotation.Import; -+import org.springframework.http.MediaType; -+import org.springframework.web.bind.annotation.PostMapping; -+import org.springframework.web.bind.annotation.RequestBody; -+import org.springframework.web.bind.annotation.RestController; -+import org.springframework.test.web.servlet.MockMvc; -+ -+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; -+ -+@WebMvcTest(controllers = CodesValidationBoundaryTest.PilotController.class) -+@Import(GlobalExceptionHandler.class) -+class CodesValidationBoundaryTest { -+ -+ @Autowired -+ private MockMvc mockMvc; -+ -+ @Test -+ void validationKeepsCodesIdentityAndStructuredIssuesAtHttpBoundary() -+ throws Exception { -+ mockMvc.perform(post("/codes-pilot/validate") -+ .contentType(MediaType.APPLICATION_JSON) -+ .content(""" -+ { -+ "email": "" -+ } -+ """)) -+ .andExpect(status().isBadRequest()) -+ .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_PROBLEM_JSON)) -+ .andExpect(jsonPath("$.status").value(400)) -+ .andExpect(jsonPath("$.code") -+ .value("io.github.aalsanie.codes.standard:INVALID_ARGUMENT")) -+ .andExpect(jsonPath("$.issues[0].path").value("email")) -+ .andExpect(jsonPath("$.detail").doesNotExist()); -+ } -+ -+ @RestController -+ static class PilotController { -+ @PostMapping("/codes-pilot/validate") -+ void validate(@Valid @RequestBody PilotRequest request) { -+ } -+ } -+ -+ record PilotRequest(@NotBlank String email) { -+ } -+} ---- a/billing-service/pom.xml -+++ b/billing-service/pom.xml -@@ -28,8 +28,26 @@ - - - 21 -+ 0.4.0-RC1 -+ 1.75.0 - -+ -+ -+ -+ io.grpc -+ grpc-bom -+ ${grpc.version} -+ pom -+ import -+ -+ -+ - -+ -+ io.github.aalsanie -+ codes-grpc-java -+ ${codes.version} -+ - - org.springframework.boot - spring-boot-starter-web -@@ -45,17 +63,17 @@ - - io.grpc - grpc-netty-shaded -- 1.69.0 -+ ${grpc.version} - - - io.grpc - grpc-protobuf -- 1.69.0 -+ ${grpc.version} - - - io.grpc - grpc-stub -- 1.69.0 -+ ${grpc.version} - - - org.apache.tomcat -@@ -103,7 +121,7 @@ - com.google.protobuf:protoc:3.25.5:exe:${os.detected.classifier} - grpc-java - -- io.grpc:protoc-gen-grpc-java:1.68.1:exe:${os.detected.classifier} -+ io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier} - - - ---- a/billing-service/src/main/java/com/pm/billingservice/grpc/BillingGrpcService.java -+++ b/billing-service/src/main/java/com/pm/billingservice/grpc/BillingGrpcService.java -@@ -1,21 +1,58 @@ - package com.pm.billingservice.grpc; - -+import billing.BillingRequest; - import billing.BillingResponse; - import billing.BillingServiceGrpc.BillingServiceImplBase; -+import io.github.aalsanie.codes.Issue; -+import io.github.aalsanie.codes.Outcome; -+import io.github.aalsanie.codes.StandardOutcomes; -+import io.github.aalsanie.codes.grpc.GoogleRpcOutcomeMapper; -+import io.github.aalsanie.codes.grpc.GrpcOutcomeExceptions; -+import io.github.aalsanie.codes.grpc.GrpcOutcomeExposure; -+import io.github.aalsanie.codes.protocol.grpc.GrpcOutcomeMapper; -+import io.grpc.StatusRuntimeException; - import io.grpc.stub.StreamObserver; - import net.devh.boot.grpc.server.service.GrpcService; - import org.slf4j.Logger; - import org.slf4j.LoggerFactory; - -+import java.util.ArrayList; -+import java.util.List; -+ - @GrpcService - public class BillingGrpcService extends BillingServiceImplBase { - private static final Logger log = LoggerFactory.getLogger(BillingGrpcService.class); -+ private static final GoogleRpcOutcomeMapper ERRORS = -+ new GoogleRpcOutcomeMapper( -+ GrpcOutcomeMapper.standard(), -+ GrpcOutcomeExposure.publicErrors() -+ ); - - @Override -- public void createBillingAccount(billing.BillingRequest billingRequest, -+ public void createBillingAccount(BillingRequest billingRequest, - StreamObserver responseObserver) { - - log.info("createBillingAccount request received {}", billingRequest.toString()); -+ -+ List issues = validationIssues(billingRequest); -+ if (!issues.isEmpty()) { -+ Outcome outcome = Outcome.of( -+ StandardOutcomes.INVALID_ARGUMENT, -+ null, -+ issues -+ ); -+ -+ StatusRuntimeException error = GrpcOutcomeExceptions -+ .toStatusRuntimeException(outcome, ERRORS) -+ .orNull(); -+ -+ if (error == null) { -+ throw new IllegalStateException("INVALID_ARGUMENT has no gRPC mapping"); -+ } -+ -+ responseObserver.onError(error); -+ return; -+ } - - // Business logic - e.g. save to database, perform calculations, etc - -@@ -27,4 +64,20 @@ - responseObserver.onNext(response); - responseObserver.onCompleted(); - } -+ -+ private static List validationIssues(BillingRequest request) { -+ List issues = new ArrayList<>(); -+ -+ if (request.getPatientId().isBlank()) { -+ issues.add(Issue.at("patientId", "Patient id is required.")); -+ } -+ if (request.getName().isBlank()) { -+ issues.add(Issue.at("name", "Name is required.")); -+ } -+ if (request.getEmail().isBlank()) { -+ issues.add(Issue.at("email", "Email is required.")); -+ } -+ -+ return issues; -+ } - } ---- /dev/null -+++ b/billing-service/src/test/java/com/pm/billingservice/grpc/BillingGrpcServiceCodesTest.java -@@ -0,0 +1,79 @@ -+package com.pm.billingservice.grpc; -+ -+import billing.BillingRequest; -+import billing.BillingResponse; -+import com.google.protobuf.Any; -+import com.google.rpc.BadRequest; -+import com.google.rpc.ErrorInfo; -+import io.grpc.StatusRuntimeException; -+import io.grpc.protobuf.StatusProto; -+import io.grpc.stub.StreamObserver; -+import org.junit.jupiter.api.Test; -+ -+import java.util.concurrent.atomic.AtomicReference; -+ -+import static org.junit.jupiter.api.Assertions.assertEquals; -+import static org.junit.jupiter.api.Assertions.assertInstanceOf; -+import static org.junit.jupiter.api.Assertions.assertNotNull; -+import static org.junit.jupiter.api.Assertions.assertNull; -+ -+class BillingGrpcServiceCodesTest { -+ -+ @Test -+ void invalidRequestKeepsCodesIdentityAndStructuredIssuesInTrailers() -+ throws Exception { -+ AtomicReference failure = new AtomicReference<>(); -+ AtomicReference response = new AtomicReference<>(); -+ -+ StreamObserver observer = new StreamObserver<>() { -+ @Override -+ public void onNext(BillingResponse value) { -+ response.set(value); -+ } -+ -+ @Override -+ public void onError(Throwable throwable) { -+ failure.set(throwable); -+ } -+ -+ @Override -+ public void onCompleted() { -+ } -+ }; -+ -+ new BillingGrpcService().createBillingAccount( -+ BillingRequest.getDefaultInstance(), -+ observer -+ ); -+ -+ assertNull(response.get()); -+ -+ StatusRuntimeException exception = -+ assertInstanceOf(StatusRuntimeException.class, failure.get()); -+ -+ com.google.rpc.Status status = StatusProto.fromThrowable(exception); -+ assertNotNull(status); -+ assertEquals(3, status.getCode()); -+ -+ ErrorInfo identity = null; -+ BadRequest badRequest = null; -+ -+ for (Any detail : status.getDetailsList()) { -+ if (detail.is(ErrorInfo.class)) { -+ identity = detail.unpack(ErrorInfo.class); -+ } else if (detail.is(BadRequest.class)) { -+ badRequest = detail.unpack(BadRequest.class); -+ } -+ } -+ -+ assertNotNull(identity); -+ assertEquals("io.github.aalsanie.codes.standard", identity.getDomain()); -+ assertEquals("INVALID_ARGUMENT", identity.getReason()); -+ -+ assertNotNull(badRequest); -+ assertEquals(3, badRequest.getFieldViolationsCount()); -+ assertEquals("patientId", badRequest.getFieldViolations(0).getField()); -+ assertEquals("name", badRequest.getFieldViolations(1).getField()); -+ assertEquals("email", badRequest.getFieldViolations(2).getField()); -+ } -+} diff --git a/scripts/verify-rc-adopters.sh b/scripts/verify-rc-adopters.sh deleted file mode 100644 index 7deb9ff..0000000 --- a/scripts/verify-rc-adopters.sh +++ /dev/null @@ -1,241 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -root_dir="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" -manifest="$root_dir/adoption/rc1/manifest.json" -version="${1:-0.4.0-RC1}" -report="${2:-$root_dir/build/reports/rc1-adopters.md}" -work_dir="${RC_ADOPTER_WORK_DIR:-$root_dir/build/rc-adopters}" - -manifest_version="$(python - "$manifest" <<'PY' -import json -import sys -from pathlib import Path -print(json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))["version"]) -PY -)" - -if [[ "$version" != "$manifest_version" ]]; then - echo "RC adopter manifest is for $manifest_version, not $version." >&2 - exit 1 -fi - -central_base="https://repo.maven.apache.org/maven2/io/github/aalsanie" -for artifact in codes codes-spring codes-grpc-java; do - url="$central_base/$artifact/$version/$artifact-$version.pom" - found=false - - for attempt in $(seq 1 40); do - if curl --silent --show-error --fail --head "$url" >/dev/null 2>&1; then - found=true - break - fi - sleep 15 - done - - if [[ "$found" != true ]]; then - echo "Published artifact did not become visible on Maven Central: $url" >&2 - exit 1 - fi -done - -rm -rf "$work_dir" -mkdir -p "$work_dir" "$(dirname "$report")" - -{ - echo "# Codes $version real-adopter gate" - echo - echo "Generated: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" - echo - echo "Codes library patches used by pilots: **0**" - echo - echo "Both pilots resolve Codes from Maven Central. No composite build, Maven Local repository, or source substitution is used." - echo "Integration time below is automated clean clone + patch + build/test time, not human coding time." - echo -} > "$report" - -overall=0 - -pilot_value() { - local pilot_id="$1" - local field="$2" - python - "$manifest" "$pilot_id" "$field" <<'PY' -import json -import sys -from pathlib import Path - -manifest = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")) -pilot = next(p for p in manifest["pilots"] if p["id"] == sys.argv[2]) -print(pilot[sys.argv[3]]) -PY -} - -run_irp() { - local id="integration-reliability-platform" - local repo commit patch_dir patch_file start end elapsed status log - - repo="$(pilot_value "$id" repository)" - commit="$(pilot_value "$id" commit)" - patch_file="$root_dir/$(pilot_value "$id" patch)" - patch_dir="$work_dir/$id" - log="$work_dir/$id.log" - - git clone --quiet --filter=blob:none --no-checkout "$repo" "$patch_dir" - git -C "$patch_dir" checkout --quiet --detach "$commit" - git -C "$patch_dir" apply --check "$patch_file" - git -C "$patch_dir" apply "$patch_file" - - start="$(date +%s)" - status="PASS" - - if ! ( - cd "$patch_dir" - bash ./gradlew \ - "-PcodesVersion=$version" \ - clean test \ - --no-build-cache \ - --stacktrace - - bash ./gradlew \ - "-PcodesVersion=$version" \ - dependencyInsight \ - --dependency codes-spring \ - --configuration runtimeClasspath \ - --no-build-cache - ) >"$log" 2>&1; then - status="FAIL" - overall=1 - fi - - end="$(date +%s)" - elapsed="$((end - start))" - - { - echo "## $id" - echo - echo "- Repository: \`$repo\`" - echo "- Commit: \`$commit\`" - echo "- Boundary: $(pilot_value "$id" boundary)" - echo "- Differentiator: $(pilot_value "$id" differentiator)" - echo "- Result: **$status**" - echo "- Automated clean integration/build time: ${elapsed}s" - echo "- Manual mapping removed: $(pilot_value "$id" manual_mapping_removed)" - echo "- Dependency conflicts: $(pilot_value "$id" dependency_conflicts)" - if [[ "$status" == "PASS" ]]; then - echo "- Missing Codes API: None observed." - else - echo "- Missing Codes API: unresolved; inspect the failed build before classifying." - fi - echo "- Expected identity: \`$(pilot_value "$id" expected_identity)\`" - echo "- Custom Codes library patches: 0" - echo - echo "Patch statistics:" - echo - echo '```text' - git -C "$patch_dir" apply --stat "$patch_file" - echo '```' - echo - echo "Build log: \`$(basename "$log")\`" - echo - } >> "$report" -} - -run_patient() { - local id="patient-mgmt-microservices" - local repo commit patch_dir patch_file start end elapsed status log - - repo="$(pilot_value "$id" repository)" - commit="$(pilot_value "$id" commit)" - patch_file="$root_dir/$(pilot_value "$id" patch)" - patch_dir="$work_dir/$id" - log="$work_dir/$id.log" - - git clone --quiet --filter=blob:none --no-checkout "$repo" "$patch_dir" - git -C "$patch_dir" checkout --quiet --detach "$commit" - git -C "$patch_dir" apply --check "$patch_file" - git -C "$patch_dir" apply "$patch_file" - - start="$(date +%s)" - status="PASS" - - if ! ( - cd "$patch_dir/patient-service" - bash ./mvnw \ - --batch-mode \ - --no-transfer-progress \ - "-Dcodes.version=$version" \ - clean test - - bash ./mvnw \ - --batch-mode \ - --no-transfer-progress \ - "-Dcodes.version=$version" \ - dependency:tree \ - "-Dincludes=io.github.aalsanie:*" - - cd "$patch_dir/billing-service" - bash ./mvnw \ - --batch-mode \ - --no-transfer-progress \ - "-Dcodes.version=$version" \ - clean test - - bash ./mvnw \ - --batch-mode \ - --no-transfer-progress \ - "-Dcodes.version=$version" \ - dependency:tree \ - "-Dincludes=io.github.aalsanie:*,io.grpc:*" - ) >"$log" 2>&1; then - status="FAIL" - overall=1 - fi - - end="$(date +%s)" - elapsed="$((end - start))" - - { - echo "## $id" - echo - echo "- Repository: \`$repo\`" - echo "- Commit: \`$commit\`" - echo "- Boundary: $(pilot_value "$id" boundary)" - echo "- Differentiator: $(pilot_value "$id" differentiator)" - echo "- Result: **$status**" - echo "- Automated clean integration/build time: ${elapsed}s" - echo "- Manual mapping removed: $(pilot_value "$id" manual_mapping_removed)" - echo "- Dependency conflicts: $(pilot_value "$id" dependency_conflicts)" - if [[ "$status" == "PASS" ]]; then - echo "- Missing Codes API: None observed." - else - echo "- Missing Codes API: unresolved; inspect the failed build before classifying." - fi - echo "- Expected identity: \`$(pilot_value "$id" expected_identity)\`" - echo "- Custom Codes library patches: 0" - echo - echo "Patch statistics:" - echo - echo '```text' - git -C "$patch_dir" apply --stat "$patch_file" - echo '```' - echo - echo "Build log: \`$(basename "$log")\`" - echo - } >> "$report" -} - -run_irp -run_patient - -{ - echo "## Gate" - echo - if [[ "$overall" -eq 0 ]]; then - echo "**PASS** — both independent external integrations succeeded from Maven Central without custom Codes library patches." - else - echo "**FAIL** — at least one external integration failed." - fi -} >> "$report" - -cat "$report" -exit "$overall" From f3ec19ad43a93a3b235d9cdb7b9c83d29ee726bf Mon Sep 17 00:00:00 2001 From: aalsanie Date: Wed, 9 Sep 2026 19:23:19 +0300 Subject: [PATCH 5/5] chore: remove generated Python bytecode --- .../verify-release-promotion.cpython-311.pyc | Bin 14398 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 scripts/__pycache__/verify-release-promotion.cpython-311.pyc diff --git a/scripts/__pycache__/verify-release-promotion.cpython-311.pyc b/scripts/__pycache__/verify-release-promotion.cpython-311.pyc deleted file mode 100644 index 83f4f55957309769ce12874d4d8654a36436f619..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 14398 zcmbU|S!^5UnKR_@CPx%?Tb4AkWyefR$!BcKS7lqiEy=Ph+ln2U=8PmN5~<9Ld@xkY z7+zEkw2jhDHv$^%a*H-qngB8Suxk`p)IGMF?Zb9P7{J5;S{PXPp>HzM0$Dxn_x;0b zC^_x!X!!mA{P*?UzkcfTxhZ%8&;D`jKbtA)zwsmg*o+AM)lHhBKB9Pvr^l%YdWfbm zZW*@>Sx9IdvXYP)Vn}EkvXRg}WGA6x$Voz1E_9R7I_{b94tZ(HLJ3~M_X+gz8T^bV z!{%e9;V1C%Ss`JpA|EZrhWtD;RLR?h0v75N#oHfHyhEt^#0vG`XFNmIypwl*O!K>V z{{#C_4ZoYOe83FV@_YDd2w@K{OkNi2pjl)d>e!t`2GA=2pjnW{HqXdXe5f5a+5TK7ca@q`?a zV~M!b1K&(vL>^7nh{CuKk%Uk*66a%lL>AibO^gR^N$>H~T|K9|&-b2cpWu_O{_d_5 z7rHT8$0wpvhb+WH6S4S|EQChH$tY$tlxIkjVk|xaS*w*JVuT+T+9$=tq#(*ML5kw2 z@=tDm=&x{lKB5F_h~_Cc1vHG`qO*NsVj_X#?iR&_*b}ts&Tu#$nGnKZ-5m~3B>1Uu zjC;f3x2GcGM$V_ySJ?b&_wml_mnA`zu6K=2MEL7xcp(A}jtNm&x*kpNf^=Pqim^%f zx-E#Y;d`O{iojrvZrdmCi4JIsfDI6l@Q9=fe_6(vJ|TMZB?dmFbz5{)h`yyW5phJ) zt&%K?SVd=UB@*MhT@>UgF}})4Q@5a#s31x0#}gBi%qP(L zXN4YMgKRyzbtEPyEurHBLB^P;U6T34l&oVYh@#HO!d*FN)7`}x>sE0pE>=Q^I@90V zI{@eLOka1G3;qh2p5H~^MHuy=6 z0C0kVxf3%-G!OUGvvtX{RrPGsJlhmz8yP#SqA;v8lYq+6(gESN!&K<6UWbTz8a!)&G;wE@?u2c@H79d`_-2f^0WfV5nZj^vTo7^i z+cCQZfH8&F=cDsS=Z|JiAXGiCY549^m|X_mG(bV!`j&9dGz&atWEQ`G$ak$DQ8;Fv zUa-7pov{wXQGeh}Q5ncv`(w&$*s$0i}I1p4z`wpGKOM3cljJDSs3XZuK(dgC-wWT4j86&ehsG1p$hdR_BKb1Apsv&j z;OjJ{ux}V|hJUi>-;e(KsB-C=dVEMbJ_N}#@(+&B+dnw>;kmi5rY79$3#R18ZC)C;Y(rwa2)q7gVwP5mW&ySh(kND*c1SmQr2HJa}EE}C$st4 z!N3>!FA_gXsO?9!_M`BrvTtbY8|hQa4)@HycT*2ib8jy>>Q^Y*)=Os`i}a7(Mtqsw zs5D)8ECDcHmF>~k9>v*{1=Qa;+o>=$2Cn;tWAVs%7}%5q%n*obWJFqZ;i?mEv05Qd z0T03Wc8;)*0$#(oo$w>Ll!Qe!Q-mVc^R$Vr6zZoa^BG6_QGV{HEhE%4FqIS3jlO9n zWyzFa81$WcQjEE!XDnBg+ap{d!6DXSEa z3vwPs@eI<$g|d?OX&L!b?n#yPa~)P^+72^?zl;Uj;uua?zb(5QW5?oFI;R+%d2RqFTC#K>&K#|kw+d;Q(5e40WC&KeypwcsBg*$#hp* z%6iy2>r+3sbvgK{F$)TtG~!3^4(nEGe78zQmuD zvMWqaf>LHalGAom8K}KZ83!P?Gi4WFOF7p^YLq(HN8ps*++PwVW=^50l;ay(w!XAw zhq;~gS}r9YJj2^wGDhcLY6%EI$%+Gk3s1XJ)|9KrV5i+c21>s@DJtc@O^darG1yZs zAe{~(fH@b+$O8xt?)6C=v_+c*wP|n4lk(>FxvsPi(u0fi;3gv8nqFpa@)QsvPfxOX zDlX*Ns&R^?PA+NL&Lx{CVv+>P8<%4?p+p>5Zn4a3{l`OcB18@`GG0Sb;$jl8WZ6{d zAh&8c$R!*53WX*Ujj$XdW2!v=L-0qK$_Oi(8;2Vg%}(f}eC3Sb3WIGfUMr zEVQUKL9Hg3?#Vi;=h-F4))mTWi_pt8jq?{4d(@4cYRy5d=HQHbIZ!{}yckj&_NjsW zT44W-BiqyhfDlO6IXdgDdcWtr9;I>TqZ9vf=I3V~kEwgkXnW46jb~KvSwY{{{RtCTK*~(MciL1 z&ZCl+f!b>yvm&#;7Rp;dC<2OARP5GLM=%c|6<~Uj7uLt+i781Y*c=rF;Og5s9#}kv zqJS2b?HouV20`FZ>Epl*BLMkWrQPBH3>(zmRtN}giPtcJ7H>fy!I{k~z0HMF9#4cp zVx5YDT?8cD9GwvbC^~|55cxpvcCg=_zCZo$cOHCag<@=OI9OTl8(FsI{fYM`9wufJ zPuVR??3N6_ctT}&YV6JzR@%4gh1FW&{u2cNx7@vAg_@+gv4hqraa^~_iMNEfZcp4o zyO2o-EwJI=!N7qH!$|T^QEpy;8&aUF=JagX5M)3fGoRbd8#8Tjy{ygkhQN>ow{QO;gt>8vgKzY&MUtAcK$L zF(*$OXqMAVsjSSqQp{LkM8NXgM7}PQ{Zq_+7|%5GZRQSbTGwJ9#EOiatkVauSkf{x zR?reahW~~>?oh_;g5I8FBS^=e+$*$un1Z97(6wo1TV1U9N- z%L~d`0meK4=~F8KyX^+O?5mo6^Qmw1l5cb7)T4gIw^{Y=*L?etS-tZ3^m0|*hX>}b zs#ROHs;w(lNFy0V@B+*I2|+P!z1Y`Zf4yFEG41Q;p32!n8nPMM#?z&cu)`})x_^6C zX}W@M|79bhvR5_ss`6%3aYmtoua}J`IIMhwG?9m-iN43Xm8L#;pKST;7d7^x za^ypAov6rmr`Z#C( zHQRVBX#I7t4&uL|8jm+xf757#IBY#gG@uS*D2+x;)Dm%?u)ZHLV6g+cmI<$s^pj_O zddbsfK^&}f`3eyJC2Gr;Y41Q2|1Y(HKAWlHOH`dQaT+ZhQbj5C<;zqa5bLdFCCXG6 zP%jE}4(3^)ewhM=Etv@QkS2bn^4Z#6(gyOcHIE!9Ch_jiJ?5ED+fxisR&UAz(3i3U zWK%Yvt&VAjTvM!>asYoZi5b&Q8O_|eC*_pu^Knr7OeM=!q{6)Oux8GeoDsjd+TIm z4QqGAQ?es zg&lrbtBvMX`HfSwRQJ--O=YE=8^__&vp0cZ0-vu#zCpNT$9d4EGNH(o$3)Ng?yfIPW zll7(TmgeZzojZ1bW!{O>01{Q*2_|+>GDid*r2|$Hwu{QnN%O7 zUBs`UL8Rc<;HOP?W$;`8EyzD+x`2L-mz=gT!_h;F;SK*{$4hIJ@e@8@i_y9%@V$y98>%j9iC5?imG!c)EPZ6s;XoMgPN)mb@Vqii68%I1U z=nUHbz)>p5pcr*P3nIGr#AyH~SBr?glVqFWiVmZZRu;gVx}KNihgkPL0N|1UZ)N~p znmGU-3UG|hMHlv{9u8cGD>lrrQ$GLur`|g?cUJYbXx^60>r39?iUYHp`K(i_ccbRr zn6WQ;x30J_%bm|UuXP!DIy-ZE_C*ZWe%7qa(R#?J3Ti#S7=x2}Q|0faI3k(FJpM zz7*sYtxCjj&FMB?j10>sb$7AcKyo`rG+i<%E+TOe80{@^s*Hl9y{6e9t6LJ1?hxX) zV5U_bBss{6DNMnmp}Psj$K-@~kI2}%1BgzTsH~t8i1&17epNy61|%j~UJ{#( z+#>=#gJ0-*C&v;tME4kD;)xXJ$t@VE2|9wY3wRNOyGaE19LU-E%MYgbDU`eqKWPDQ z8cyPJWz~lpK5G7cv%>9GEB9!Xd(vl@z3l8+G%C#>QN6Ef-dEGz#2@f}{Jpr+6jC}) zsqATuJ^hsJU1EEczDp`Qps@qs5HPIvM~J|CNM#Re>|w=u*l+;Y4v^%1D!X4}_bbl* z&%J@!^H05-mb{yu)i-DBn^tV7@T1oM!fNw@ssJF}{bw)buboXjt!Q1UX#Gj((JiH- zRjoLnRUAm4&ITHkimUKuUNyoe^+u>%95f+W1dSsGgwa z38Jii#_%!Z=A<}JKl9W*^)xPd8s~de&vwnTJ$(Wz@e5F?HK!w7F)CeF*+GpRRGfn= zj0v*{j%Di_3BtiGh5+7T2-wqSS6Iqh3vO5b2E;=KkIEi{Dy z0Q;EB{bwgt)$rl=r-8PmKwD<;(IzF(rUv$EfxYQ-*_w??-~zmvAtQX^F~an@Wwv4d zwak>thBP(={!N(5$bzhT+BCG!(YE8pRJv65n8qGcoX5~^^UnQu<~mhJv*u{dY+Z6} z!-Bm=!3$3Y%N?J&3j0(?qvmML>{W~kgY@F5FV6k!++(l0y<6Mf{pEnN{UWwBh%II8 zO4ESxs_bQry{tGdqXslcbf-a@5T_^{)xv{z1EQ_m1^8XJSzF=hB;;obI!mDzM0m^R zWt?J~GC5MfqiE)9zyV97`Qk+zyhUIx37*M4|qdpky>2=N{Avz{-O1 zrw2T|p5-e( z_nWT?p_g>5pyTkBIXSV)G}FLCH;lfj+^-DUQRzIZNvRr;=U@ay1Lw5OT#BzP%eSX& zDZ9bV#R^lub!G_?`YX|4O!KSf8*qM~fNuL4{ETM|FMg57+zwdJHk9pAJ>R&t{{L%t z95ZgvRyICxmg+le_Rz^U!3at;qBSL4a#Mcv6a_9V8gx3)fXur`Q!-xU$vqF`)78j66q9WDC(DTNhkwRtQbdn|egF60O>%nx_7X< z{}Q@xdb%!jCu@nT{I%=t<}kVKB;Je2d!0~FMJH4Q1YN?+@)@0+NmNDjAfjq5L~<8! z?05m^3x)`L4!AIbh)BSv7DH$w*FfOlQ8~xAA+BvOxZ-+3+G8jy;oT{?3F8rrcWywr z$wPGJL?MaLCh;_sOWH#r6a&OV_`$$6fmf662|*kYLbu?aX3`f5#iS6*loCh zW}Xz0UmF)?8qO0Jd|~nhCq5Vo3riE{@(pl_VGf%}x7U~klEZ3gALf(HsZD|Vt5XzLmdEodn^7kBqYCKzQ4x`M*-lrT5Dlj^Wd4+zhb3azQ2{{_=oQpPNKy-3cE#R z_i60D6>GjAsbcK2YY;C3$3O=VKfsRdK`wNJyPez~G<1ZbIBh=r;H@qk|Mp-dnVpD4 zfZQSz4`b|G2=Kg+yPo16jGaS(bOJ?wyd8{M195cs9^%Ho+PbyVUw!PwNgZ)g69vN2xok*7a(2y$VykT)j!zy!+9cYV`@N`b7G|vd1@j zNU7iX=&J&z9qFkZE(S8M8h+BCG( zG^93N*P5;a9aTMo<`F=$ZU`=h9#^XkN3@0`DpQ+2Idk@TMeSTF6Mi)Oc=*e1rJ`T0 z=+`RxLE3jzXpRl@ktGK>plREi#!#+5u2rB(@~Z3&jlH2bZ#?(a%)ONvRyH3nUe(vB z`8v}lm;LqgjhTe%e_ivxo<5Ux_~sgx92>B?sL@nuoBhvz%2qQsI1j^Y)0j4eX?xDNRxGq@@7zh4q-!q$ z^TWmX;)P=T@wH-{#K0Mgtph-nr34rZ@EDw|DEtZ!3I@Ov=&w-G{)hr!X$grkORyks z!T$?@w-VWN?g6i1&@7pQKbG9|E4>(Bl*U1-huTG{68)gyp`^YI^8BS-><(Sx7#P75=qe*n^w!KUjvRfa1e{u!wNfDN#c3_Xr5LK=EDt zH2~dfcyil`hgKwprhJlNZ(?%=3Z7oPk_f(Ek-_S)ZFu9eMWiGaf z+D-%AF@pfbcxRGJR-j5W-IR4#&%8P}FgNhQ;D>{%d!y#ws4yFGyPHTpIU4^9W|AC~