The keycloak project ended with four open questions that design could not
settle. A two-VM lab was built to answer them by measurement, and this is
that material: 26 experiments, 125 raw command outputs, 22 browser captures.
Follows the import procedure in README.md.
source/ the originating repository verbatim — 78 documents, 28 SVGs,
8 manifests, plus .source-revision recording the commit
final/ the SSOT
document.md 729 lines written from the 29 experiment documents, not
concatenated: what was predicted, what was measured, and
where the measurement itself was wrong
evidence/raw 125 outputs, flattened to <experiment>__<file> because
the originals collided (01-baseline.txt appeared three
times) and the audit only globs the top level
evidence/meta one per raw file; command and exitCode are null and the
README says why rather than inventing them
evidence/browser 22 captures
assets/ three diagrams through techviz
.techviz/ their VizSpecs
A separate project rather than an addition to keycloak: the B-layer answers
that project's four questions, but the A, C and D layers are about cluster
failure, SSO and operations, and one document.md should hold one subject.
The four question records there can point here through 관계.
Recorded rather than papered over: only three of the 28 diagrams were
remade. The repository forbids hand-drawn SVG and forbids titles inside the
canvas; all 28 originals carry both, so converting them is redrawing, not
reformatting. They stay in source/ and the gap is written into the document.
verify-pipeline.py passes. audit-records.py reports no issues.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
311 lines
19 KiB
Markdown
311 lines
19 KiB
Markdown
# grpc-proto-contract 완전 해부
|
|
|
|
> 상태: COMPLETE
|
|
> 재오픈 게이트: cycle 2 — `src/main` production 3파일 605줄, 스키마/설정 리소스 5개, test 1파일 324줄 축자 통독 완료. `STRUCTURAL_ONLY` 잔여 없음.
|
|
> 기준 revision: `21234e38cdb9a926cbc92bb97a2aee2e4a7d2916`
|
|
> 분석 범위: `src/grpc/grpc-proto-contract`
|
|
> SSOT owner: `grpc-proto-contract`
|
|
> integration/family document: `analysis/20-grpc-platform.md` (secondary, INTEGRATION_ONLY)
|
|
|
|
---
|
|
|
|
## 0. SSOT identity / 커버리지와 숫자 지도
|
|
|
|
- `allowed_dependencies`: `["grpc-core-api"]`
|
|
- `runtime_memberships`: **`[]`**
|
|
|
|
| 파일 | LOC | 성격 |
|
|
|---|---:|---|
|
|
| `GrpcProtoContractValidator` | 447 | 라인 스캐너 + 9개 규칙 판정 |
|
|
| `GrpcProtoStyleManifest` | 120 | 규칙을 데이터로 둔 record |
|
|
| `GrpcProtoRuleViolation` | 38 | 위반 1건 record |
|
|
| **main java 합계** | **605** | |
|
|
| `error.proto` | 66 | 리치 오류 상세 5 메시지 + 열거형 1 |
|
|
| `stream.proto` | 57 | 스트림 공통 스키마 |
|
|
| `buf.yaml` · `buf.gen.yaml` · `buf.lock` | — | 생성 설정(이 리프는 protoc 을 돌리지 않는다) |
|
|
| `GrpcProtoContractValidatorTest` | 324 | 테스트 |
|
|
| `build.gradle` | 12 | 의존 project 1 |
|
|
|
|
### Coverage ledger
|
|
|
|
| scope | count | disposition | reason |
|
|
|---|---:|---|---|
|
|
| `main/java/**` | 3 | `FULL_READ` | 447+120+38 전 본문 축자 확인 |
|
|
| `main/resources/proto/**/*.proto` | 2 | `FULL_READ` | 66+57 전문 |
|
|
| `main/resources/proto/buf.*` | 3 | `FULL_READ` | 테스트가 단언하는 키 전수 확인 |
|
|
| `test/java/**` | 1 | `FULL_READ` | 324줄 · 테스트 13개 |
|
|
| `build.gradle` | 1 | `FULL_READ` | 12줄 |
|
|
| `gradle.lockfile` | 1 | `STRUCTURAL_ONLY` | 잠금 파일; 선언 의존은 project 1 뿐임을 build.gradle 에서 확인 |
|
|
|
|
`UNCLASSIFIED` 0.
|
|
|
|
---
|
|
|
|
## 1. 모듈의 정체와 경계
|
|
|
|
```groovy
|
|
// build.gradle:3-9
|
|
// Schema source of truth: the `.proto` files plus the rule engine that judges them.
|
|
// No protobuf plugin and no protoc invocation here — … running protoc is a separate, gated decision.
|
|
```
|
|
|
|
이 리프는 스키마 원본과 그것을 판정하는 규칙 엔진을 함께 담는다. protoc 은 돌지 않는다.
|
|
|
|
검증기 javadoc 이 그 한계를 스스로 규정한다.
|
|
|
|
> "A line scanner, not a Protobuf parser, and that is a deliberate limit rather than a shortcut.
|
|
> Everything this checks is a property of the source text a reviewer reads… Semantics that need a
|
|
> compiled descriptor belong to `grpc-codegen`'s descriptor artifact."
|
|
|
|
## 2. 규칙 9개
|
|
|
|
| 상수 | 판정 |
|
|
|---|---|
|
|
| `PROTO3_SYNTAX` | `syntax = "proto3"` 필수 |
|
|
| `PACKAGE_VERSIONED` | `<org>.<domain>.v<major>` — 접두 일치 + `.*\.v[1-9]\d*$` |
|
|
| `JAVA_MULTIPLE_FILES` | `option java_multiple_files = true` 필수 |
|
|
| `JAVA_PACKAGE_SEPARATE` | `java_package` 가 손으로 쓴 패키지 안이면 위반 |
|
|
| `ENUM_ZERO_UNSPECIFIED` | 0 값 이름이 `_UNSPECIFIED` 로 끝나야 함 |
|
|
| `RESERVED_HISTORY` | 삭제 이력의 번호·이름이 `reserved` 에 있어야 함 |
|
|
| `WELL_KNOWN_TYPE_ALLOWLIST` | `google/protobuf/` import 는 allowlist 에만 |
|
|
| `MAP_ALLOWLIST` | `map` 필드는 `message.field` 단위 허용 |
|
|
| `EXPLICIT_PRESENCE` | 매니페스트가 지정한 필드는 `optional` 선언 필수 |
|
|
|
|
## 3. 세 가지 설계 판단
|
|
|
|
### 3.1 금지가 아니라 allowlist
|
|
|
|
`GrpcProtoStyleManifest` javadoc:
|
|
|
|
> "Three of the five fields are allowlists, and that shape is the decision: `Any`, `Struct` and
|
|
> `map` are not banned, they are things you have to ask for by name. A ban gets worked around; an
|
|
> allowlist entry gets read by the next person to open the manifest and carries the field it was
|
|
> granted for."
|
|
|
|
`caSkeleton()` 의 기본값은 조직 `hyeonworks`, 손으로 쓴 패키지 `dev.caskeleton`, WKT allowlist 5개(timestamp·duration·field_mask·empty·wrappers), map allowlist 빈 집합, presence 요구 빈 맵이다.
|
|
|
|
`allowingWellKnownTypes` · `allowingMapFields` · `requiringPresence` 세 메서드가 매니페스트를 넓힌 사본을 만든다. 정규 생성자가 모든 컬렉션을 복사하므로 리뷰를 통과한 매니페스트가 나중에 넓어지지 않는다.
|
|
|
|
### 3.2 던지지 않고 목록으로 돌려준다
|
|
|
|
`GrpcProtoRuleViolation` javadoc:
|
|
|
|
> "Returned rather than thrown, and carrying a line number, because a schema review is a list. A
|
|
> validator that throws on the first violation turns 'this file breaks four rules' into four
|
|
> separate runs, and the author fixes them one at a time without ever seeing the shape of the
|
|
> problem."
|
|
|
|
### 3.3 삭제 이력은 추론하지 않고 입력으로 받는다
|
|
|
|
> "a field that is simply gone from the current source is indistinguishable from one that never
|
|
> existed. Recording removals and checking them against `reserved` is the only way the 'do not reuse
|
|
> a field number' rule survives the commit that deletes the field."
|
|
|
|
`SchemaHistory(removedFieldNumbers, removedFieldNames)` 가 그 입력이고, 키는 파일에 적힌 메시지 이름이며 중첩은 점으로 한정한다.
|
|
|
|
## 4. 스캔 절차
|
|
|
|
한 줄씩 읽으면서 `//` 이후를 지우고, 빈 줄을 건너뛰고, 순서대로 시도한다 — syntax → package → import → option(파일 수준만) → 스코프 열기 → 닫기 → 스코프 안 멤버.
|
|
|
|
스코프는 `message`·`enum`·`service`·`oneof` 넷을 열고 이름을 점으로 한정해 스택에 쌓는다. 열거형 안에서는 0 값 이름을, 메시지와 `oneof` 안에서는 `reserved`·`map`·필드를 본다.
|
|
|
|
## 10. 테스트 레인
|
|
|
|
`GrpcProtoContractValidatorTest` 324줄. 첫 두 테스트가 이 리프의 게이트다.
|
|
|
|
- `committedSchemaIsCompliant` — 커밋된 두 스키마를 실제로 검증기에 넣어 위반 0 을 단언한다.
|
|
- `theBufConfigurationAgreesWithTheValidator` — `buf.yaml` 의 `FILE`·`STANDARD` 범주, 생성 경로가 `build/generated/...` 이고 `out: src/` 가 아님, 버전 리터럴 부재, `deps: []` 를 단언한다.
|
|
|
|
나머지는 규칙별 거부 사례다 — proto2 거부, 패키지 형식, 자바 패키지 충돌, `java_multiple_files` 부재, 열거형 0 값, 삭제 이력, WKT allowlist, map allowlist, explicit presence, 중첩 한정, `describe()` 렌더링. 전부 13개.
|
|
|
|
거부 사례 셋은 **넓힌 매니페스트로 같은 소스를 다시 돌려** 통과까지 확인한다 — `allowingWellKnownTypes` · `allowingMapFields` · `requiringPresence`. allowlist 라는 설계가 실제로 넓혀지는지까지 붙드는 형태다.
|
|
|
|
`enumZeroValueNeedsTheUnspecifiedSuffix` 는 줄 번호 7까지 단언한다 — 위반이 줄을 정확히 가리키는지가 이 리프의 산출물 형태(`file:line rule — detail`)에 직결되기 때문이다.
|
|
|
|
## 12. negative-space probes
|
|
|
|
**12.1 도달성.** 이 리프의 production 소비자는 0 이다. `GrpcProtoContractValidator`·`GrpcProtoStyleManifest`·`GrpcProtoRuleViolation` 을 부르는 코드는 자기 테스트뿐이다.
|
|
|
|
리프 밖 참조는 두 종류다.
|
|
|
|
| 참조 | 형태 |
|
|
|---|---|
|
|
| `grpc-codegen/…/GrpcBufPolicy.java:10` | javadoc 언급 |
|
|
| `grpc-testkit` · `grpc-codegen` · `grpc-spring-boot-starter` · `grpc-advanced-edition` 의 build.gradle | project 의존 선언 |
|
|
|
|
네 모듈이 의존을 선언하지만 그중 어느 자바 파일도 이 리프의 타입을 import 하지 않는다. §17.4 가 그 결과를 다룬다.
|
|
|
|
**12.3 아홉 규칙 중 둘은 이 저장소의 매니페스트에서 사실상 비활성이다.**
|
|
|
|
```java
|
|
public static GrpcProtoStyleManifest caSkeleton() {
|
|
return new GrpcProtoStyleManifest("hyeonworks", Set.of("dev.caskeleton"),
|
|
ALWAYS_ALLOWED_WELL_KNOWN_TYPES, Set.of(), Map.of());
|
|
// ^^^^^^^^ ^^^^^^^
|
|
// mapFieldAllowlist presenceRequiredFields
|
|
}
|
|
```
|
|
|
|
- `MAP_ALLOWLIST` — 허용 목록이 비었으므로 실제 판정은 "map 전면 금지"다. §3.1 이 설명하는 "금지가 아니라 이름으로 요청" 이라는 형태는 매니페스트를 넓히는 호출자가 있어야 성립하는데, `allowingMapFields` 를 부르는 곳은 테스트뿐이다.
|
|
- `EXPLICIT_PRESENCE` — 요구 맵이 비었으므로 어떤 필드도 `optional` 을 강제받지 않는다. 커밋된 두 스키마가 `optional` 을 다섯 곳에 쓰지만(예: `FieldViolation.description`, `StreamEnvelope.resume_token`) 그것을 요구하는 규칙은 없다. 규율이 코드가 아니라 저자의 손에 있다.
|
|
|
|
나머지 일곱은 기본 매니페스트에서도 실제로 판정한다.
|
|
|
|
**12.2 대조군.** 저장소에 `.proto` 파일이 넷 있다.
|
|
|
|
| 파일 | 이 검증기가 판정하는가 |
|
|
|---|---|
|
|
| `grpc-proto-contract/.../common/v1/error.proto` | 예 (테스트 목록) |
|
|
| `grpc-proto-contract/.../common/v1/stream.proto` | 예 (테스트 목록) |
|
|
| `messaging-schema-protobuf/src/test/proto/order_created_v1.proto` | 아니오 |
|
|
| `grpc-advanced-edition/.../edition2024/compatibility.proto` | 아니오 |
|
|
|
|
**12.4 드리프트.** build.gradle 주석이 규칙으로 든 다섯(proto3 + explicit optional, 패키지 버전, reserved 이력, 열거형 0 접미, WKT allowlist)이 전부 상수로 존재한다. 드리프트 없음.
|
|
|
|
## 16. 확인하지 못한 것
|
|
|
|
- `reserved` 범위 문법의 오탐(§17.1)을 실행으로 재현하지 않았다. 정규식과 수집 코드로 판정했다.
|
|
- 블록 주석(`/* */`) 안의 선언이 스캔되는지 실행으로 확인하지 않았다. `LINE_COMMENT` 가 `//` 만 지우므로 그 형태가 남는다.
|
|
- 테스트를 실행하지 않았다. 13개 전부 본문으로만 확인했다.
|
|
- §17.4 의 "부르는 빌드가 없다"는 `*.gradle` · `*.kts` · `*.yml` 세 확장자와 자바 타입 이름 grep 으로 판정했다. 리플렉션이나 서비스 로더로 부르는 형태라면 잡히지 않는다.
|
|
- 열거형 `reserved` 오탐(§17.5)을 실행으로 재현하지 않았다. 스코프 분기 코드로 판정했다.
|
|
|
|
## 17. 손볼 것
|
|
|
|
### 17.1 P3 — `reserved 2 to 5;` 범위가 개별 숫자로만 수집되어 `RESERVED_HISTORY` 오탐이 된다
|
|
|
|
```java
|
|
RESERVED_NUMBERS = Pattern.compile("^\\s*reserved\\s+([^\";]*\\d[^\";]*);");
|
|
NUMBER = Pattern.compile("\\d+");
|
|
…
|
|
Matcher number = NUMBER.matcher(reservedNumbers.group(1));
|
|
while (number.find()) { scan.reservedNumbers…add(Integer.valueOf(number.group())); }
|
|
```
|
|
|
|
`reserved 2 to 5;` 는 그룹이 `"2 to 5"` 이고 수집되는 것은 `{2, 5}` 다. `3`·`4` 는 들어가지 않는다. `reserved 9 to max;` 는 `{9}` 만 남는다.
|
|
|
|
그러면 삭제 이력이 `3` 을 담고 스키마가 `reserved 2 to 5;` 로 정확히 예약했는데도 `RESERVED_HISTORY` 위반이 보고된다. 범위 예약은 표준 문법이고 여러 필드를 한 번에 지울 때 쓰는 형태이므로 도달 가능하다.
|
|
|
|
수정은 `to` 를 인식해 범위를 펼치는 것이다. `max` 는 상한 상수로 다루거나 그 메시지에 대해 검사를 통과시킨다.
|
|
|
|
### 17.2 P3 — 반환 목록이 자바독이 약속한 source order 가 아니다
|
|
|
|
```java
|
|
Scan scan = scan(fileName, source, violations); // import·enum·map·presence 위반이 여기서 append
|
|
checkFileHeader(fileName, scan, violations); // syntax·package·java_* 위반이 그 뒤에 append
|
|
checkRemovalHistory(fileName, scan, history, violations);
|
|
```
|
|
|
|
`validate` 의 javadoc 은 "@return every violation found, **in source order**" 라고 적는다. 실제로는 파일 앞머리의 `syntax`·`package` 위반이 40번째 줄의 `map` 위반보다 뒤에 온다.
|
|
|
|
`describe()` 가 `file:line rule — detail` 형태를 만들고 그 형태의 목적이 빌드 로그를 읽는 것이므로, 정렬이 어긋나면 리뷰 목록으로서의 값이 줄어든다. 수정은 반환 직전에 `line` 으로 안정 정렬하는 것이다.
|
|
|
|
### 17.3 P3 — 커밋 스키마 게이트가 파일 목록을 하드코딩한다
|
|
|
|
```java
|
|
List<String> files = List.of(
|
|
"proto/hyeonworks/grpc/common/v1/error.proto",
|
|
"proto/hyeonworks/grpc/common/v1/stream.proto");
|
|
```
|
|
|
|
리소스 디렉터리를 훑지 않는다. 이 리프에 세 번째 `.proto` 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고, 빌드는 초록으로 남는다.
|
|
|
|
같은 저장소가 다른 곳에서 이 형태를 이미 경계했다 — 빠뜨림이 통과가 되는 게이트다. 수정은 `proto/**` 아래 `.proto` 를 전부 열거해 돌리는 것이다.
|
|
|
|
### 기록 — `oneof` 도 스코프 이름을 밀어 넣는다 (현재 무해)
|
|
|
|
`SCOPE_OPEN` 이 `oneof` 를 스코프로 열고 이름을 점으로 한정한다. 그러면 `message Foo { oneof kind { … } }` 안의 필드는 `Foo.kind.<field>` 로 한정되고, `SchemaHistory` javadoc 이 말하는 키 규약(메시지 이름)과 어긋난다.
|
|
|
|
지금은 도달하지 않는다. protobuf 가 `oneof` 안에서 `map` 과 `optional` 을 모두 금지하므로 `MAP_ALLOWLIST`·`EXPLICIT_PRESENCE` 판정이 그 자리에서 발생하지 않고, `reserved` 도 `oneof` 안에 올 수 없다. 규칙을 넓힐 때 다시 볼 자리로 남긴다.
|
|
|
|
### 17.4 P2 — 두 파일이 이 검증기를 "빌드를 실패시키는 것" 이라고 단언하는데, 어떤 빌드도 그것을 부르지 않는다
|
|
|
|
같은 주장이 두 곳에 있다.
|
|
|
|
```java
|
|
// grpc-codegen/…/GrpcBufPolicy.java:8-10
|
|
* Buf's CLI is not part of this toolchain (adaptation D5), so the four lifecycle task names
|
|
* below are the contract a CI environment fulfils and {@code GrpcProtoContractValidator} is what
|
|
* actually fails a build here.
|
|
```
|
|
|
|
```yaml
|
|
# grpc-proto-contract/…/proto/buf.yaml:3-5
|
|
# The rules named here are also implemented in GrpcProtoContractValidator, which is what actually
|
|
# fails this repository's build: the Buf CLI is not part of this toolchain, and a gate that silently
|
|
# passes when a binary is missing is worse than one that computes the same judgement from the
|
|
# committed schema.
|
|
```
|
|
|
|
두 문장이 같은 논증을 편다 — CLI 가 없으므로 이 자바 검증기가 그 자리를 대신한다는 것. 그런데 그 검증기를 부르는 빌드 코드가 없다.
|
|
|
|
```
|
|
$ grep -rn "GrpcProtoContractValidator" --include=*.gradle --include=*.kts --include=*.yml .
|
|
(매치 없음)
|
|
$ grep -rn "GrpcProtoContractValidator" --include=*.java src/ | grep -v grpc-proto-contract/
|
|
grpc-codegen/…/GrpcBufPolicy.java:10: * … {@code GrpcProtoContractValidator} is what
|
|
```
|
|
|
|
Gradle 태스크도, 검증 훅도, 다른 모듈의 호출도 없다. 실제로 이 규칙 아홉 개를 실행하는 것은 `GrpcProtoContractValidatorTest` 하나이고, 그 테스트가 판정하는 대상은 §17.3 이 지적한 대로 **하드코딩된 두 파일**이다.
|
|
|
|
**그래서 지금 성립하는 것과 성립하지 않는 것.**
|
|
|
|
- 성립: 이 리프에 커밋된 `error.proto` · `stream.proto` 는 매 빌드마다 아홉 규칙에 걸린다(테스트가 그것을 돌린다).
|
|
- 성립하지 않음: "이 저장소의 빌드를 실패시킨다"는 범위. 리프 밖의 `.proto` 는 판정되지 않고(§12.2), 이 리프에 새로 추가되는 `.proto` 도 테스트 목록에 손으로 넣기 전까지 판정되지 않는다.
|
|
|
|
**왜 P2 인가.** 오작동이 아니라 **주장과 배선의 불일치**다. 그리고 그 주장이 CLI 부재를 정당화하는 논거로 쓰이고 있다 — "바이너리가 없을 때 조용히 통과하는 게이트보다 낫다"고 말하면서, 실제로 만든 것도 조용히 통과하는 게이트다. `grpc-codegen` §17.1 이 같은 형태를 반대편에서 기록했다(Buf 태스크 이름 넷이 어떤 빌드 파일에도 없다). 두 리프가 서로를 가리키며 상대가 게이트라고 말하는 모양이다.
|
|
|
|
**수정.** 두 가지 중 하나다.
|
|
|
|
1. 배선한다 — `check` 에 물리는 Gradle 태스크가 `proto/**` 를 훑어 `validate` 를 돌리고 위반이 있으면 실패한다. §17.3 의 하드코딩도 함께 해소된다.
|
|
2. 문장을 사실에 맞춘다 — "빌드를 실패시킨다"를 "이 리프의 테스트가 커밋된 스키마에 대해 실행한다"로 낮춘다. buf.yaml 과 GrpcBufPolicy 두 곳을 함께 고쳐야 한다.
|
|
|
|
낮추는 쪽을 고르더라도 §12.3 이 남는다 — 규칙 아홉 중 둘은 기본 매니페스트에서 판정할 것이 없다.
|
|
|
|
### 17.5 P3 — 열거형 안의 `reserved` 는 수집되지 않는다
|
|
|
|
`scan` 은 스코프 종류로 갈라진다.
|
|
|
|
```java
|
|
if ("enum".equals(scopeKind)) {
|
|
scanEnumValue(fileName, line, lineNumber, scopeName, violations);
|
|
} else if ("message".equals(scopeKind) || "oneof".equals(scopeKind)) {
|
|
scanMessageMember(fileName, line, lineNumber, scopeName, scan, violations);
|
|
}
|
|
```
|
|
|
|
`reserved` 수집은 `scanMessageMember` 안에만 있다. proto3 는 열거형에도 `reserved 2, 15;` 와 `reserved "OLD_VALUE";` 를 허용하고, 열거형 값을 지울 때 번호를 예약하는 것은 필드와 같은 이유로 필요하다 — 예약하지 않고 재사용하면 옛 클라이언트가 보낸 정수가 다른 뜻으로 해석된다.
|
|
|
|
지금 `SchemaHistory` 에 열거형 이름으로 삭제 이력을 넣으면, 스키마가 정확히 예약했더라도 `scan.reservedNumbers` 에 그 이름이 없으므로 `RESERVED_HISTORY` 오탐이 난다. §17.1 의 범위 문법 문제와 같은 방향(fail-closed)이고 같은 자리에서 고칠 수 있다.
|
|
|
|
**수정.** `reserved` 수집을 스코프 종류와 무관하게 먼저 시도한 뒤 나머지 판정을 갈래로 보낸다.
|
|
|
|
### 확인된 설계(문제 아님)
|
|
|
|
- **라인 스캐너라는 한계를 스스로 규정하고 그 경계 밖을 다른 리프로 넘긴 것.**
|
|
- **금지 대신 allowlist 를 고르고 그 이유를 적은 것** — 금지는 우회되고 allowlist 항목은 다음 사람이 읽는다.
|
|
- **위반을 던지지 않고 목록으로 돌려주는 것** — 스키마 리뷰는 목록이다.
|
|
- **삭제 이력을 입력으로 받는 것** — 사라진 필드는 없던 필드와 구분되지 않으므로 추론할 수 없다.
|
|
- **커밋된 스키마 자체를 테스트가 검증기에 넣는 것** — 규칙이 자기 스키마에 실제로 적용된다.
|
|
- **buf 설정과 검증기가 같은 것을 말하는지 테스트가 붙드는 것.**
|
|
- **넓힌 매니페스트로 같은 소스를 다시 돌려 통과까지 확인하는 테스트 형태** — allowlist 라는 설계가 거부만이 아니라 허용도 실제로 하는지 붙든다.
|
|
- **`buf.lock` 을 빈 채로 커밋한 것과 그 근거** — "adding a first dependency is a visible diff in a file that already exists, instead of a new file nobody reviews."
|
|
- **`buf.gen.yaml` 에 판본 리터럴을 두지 않은 것** — 관리 플랫폼이 플러그인 판본을 소유한다는 `GrpcCodegenManifest` 의 규칙과 같은 결정이고, 테스트가 `version: v1` 부재로 그것을 붙든다.
|
|
- **리치 오류 상세를 `google.rpc.*` 대신 자기 메시지로 소유한 것과 그 근거** — "the Stable contract is that a client branches on a code, a reason and a typed detail — never on a message string", 그리고 모양을 `google.rpc` 에 맞춰 두어 나중의 이전이 재설계가 아니라 이름 바꾸기가 되게 한 것.
|
|
|
|
---
|
|
|
|
## Source anchors
|
|
|
|
```
|
|
src/grpc/grpc-proto-contract/build.gradle:1-12
|
|
main/java/…/contract/GrpcProtoContractValidator.java:1-447
|
|
main/java/…/contract/GrpcProtoStyleManifest.java:1-120
|
|
main/java/…/contract/GrpcProtoRuleViolation.java:1-38
|
|
main/resources/proto/hyeonworks/grpc/common/v1/error.proto:1-66
|
|
main/resources/proto/hyeonworks/grpc/common/v1/stream.proto:1-57
|
|
main/resources/proto/buf.yaml:1-17 · buf.gen.yaml:1-24 · buf.lock:1-12
|
|
grpc/grpc-codegen/…/GrpcBufPolicy.java:8-10 (게이트라고 주장하는 두 자리 중 하나)
|
|
test/java/…/contract/GrpcProtoContractValidatorTest.java:1-324
|
|
```
|