Files
clean-architecture-backend-…/docs/adr/ADR-GRPC-002-schema-governance-without-protoc.md

63 lines
3.5 KiB
Markdown

# ADR-GRPC-002: Schema governance runs without protoc and without the Buf CLI
- Status: accepted
- Date: 2026-08-30
- Scope: `:grpc:grpc-proto-contract`, `:grpc:grpc-codegen`
## Context
Stable Tasks 8 through 11 require proto style rules, Buf format/lint/breaking governance, a single
Java codegen owner, and a descriptor artifact whose consumer-compile result gates a release.
Two of the tools those tasks name are absent from this toolchain. The Buf CLI is not installed. And
`protoc` is available through the Gradle protobuf plugin, but every leaf in this repository passes
spotless with google-java-format, checkstyle, SpotBugs at HIGH confidence, Error Prone and `-Werror`
— and generated protobuf sources pass none of them. Turning codegen on means excluding a source set
from five quality gates.
There is precedent for such an exclusion: the `jmh` source set has `spotbugsJmh` and `checkstyleJmh`
disabled and Error Prone off. So the carve-out is available. It is also a decision about the quality
baseline of a leaf, taken for one task, and outside what this work was asked to change.
`adapter:inbound:grpc` also carries a recorded decision in the opposite direction: its `CLAUDE.md`
forbids the protobuf plugin and `.proto` in that leaf, on the grounds that a consuming feature module
should own its schema.
## Decision
Commit the `.proto` sources and implement every rule the tasks require as executable Java, with no
protoc run and no Buf CLI invocation.
`GrpcProtoContractValidator` reads `.proto` text and enforces proto3 syntax, the
`{organization}.{domain}.v{major}` package rule, `java_multiple_files`, a generated Java package
disjoint from the hand-written one, `_UNSPECIFIED` enum zero values, `reserved` declarations checked
against a supplied removal history, a well-known-type allowlist and a map-field allowlist. It runs
against the committed schema in its own test, so the shipped `.proto` files are live rather than
decorative.
`GrpcBufPolicy` fixes the breaking gate at Buf's `FILE` category and names the four lifecycle stages
a compliant pipeline registers. `GrpcCodegenManifest` fixes one codegen owner and refuses a literal
generator version. `GrpcDescriptorArtifact`, `GrpcConsumerFixture` and `GrpcSchemaArtifactPublisher`
carry the schema hash, the descriptor digest and the per-consumer source-break report, and refuse a
publish that breaks a consumer or republishes a released version with different bytes.
The committed `buf.yaml` states the same rules, so running the CLI in an environment that has it
reaches the same verdict.
## Consequences
**The invariants are enforced; the process is not run.** Everything Tasks 8 to 11 are about — which
schema changes are refused, which consumer breaks block a release, who owns generation — is a
build-checkable rule here. What is missing is the protoc invocation and the Buf binary.
**Turning codegen on is a bounded change.** `GrpcCodegenManifest.caSkeleton()` already names the
owner, the managed version source, the build-directory output paths and the disjoint package policy
that a real plugin configuration has to satisfy. The work is a source-set carve-out and a plugin
block, not a redesign.
**The fixtures use a text codec.** `GrpcTextCodec` gives the testkit a UTF-8 marshaller so the
in-process and Netty lanes can exercise interceptors, status mapping, metadata limits and stream
sequencing without generated stubs. Those contracts are properties of the platform and the transport,
not of any message shape, so the substitution costs nothing — and the lanes run today rather than
after codegen lands.