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>
9542 lines
1.2 MiB
Plaintext
9542 lines
1.2 MiB
Plaintext
---
|
||
schemaVersion: 1
|
||
project: clean-architecture-backend-template
|
||
sourceDocument: final/document.md
|
||
sourceDocumentSha256: 880d3a4a7dba4f6303ff69be9475190b62f2a9d03b7794482b900457ee045ad6
|
||
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916
|
||
generatedAt: 2026-09-01T00:00:00+09:00
|
||
candidateLedger: candidate-ledger.json
|
||
candidateLedgerSha256: 32bc34bc953b541338476a1288661fb266db1ec618dfded97ed05eee6c65648b
|
||
sourceManifest: root-tree-source-manifest.json
|
||
sourceManifestSha256: 77d8f6c4d5ab6e4efc5e21ed1fce5dcff8b365ed7a39f4e3f3968de18bf4dd76
|
||
---
|
||
|
||
# Root Tree
|
||
|
||
PROJECT
|
||
clean-architecture-backend-template
|
||
|
||
> **분해 기준.** Topic은 공학 문제 공간이고 디렉터리가 아니다. 한 Topic 안의 Case들은 서로 다른
|
||
> 메커니즘을 갖되 같은 판정 기준과 어휘를 공유한다. Reference는 그 Topic의 Case를 지우고도 남는
|
||
> 규칙만 올린다. 빈 브랜치는 채우지 않는다.
|
||
>
|
||
> 노드 수가 많은 이유는 분모가 크기 때문이다 — 62 leaf · main Java 4,614 파일 · 320,318 LOC ·
|
||
> 모듈 문서 43,700줄(`analysis/**` 66편 — 최상위 23 · messaging 25 · grpc 18). `analysis/05`(persistence-jpa) 하나가 4,950줄이고 그 안에 P1급만 열 건이 넘는다.
|
||
>
|
||
> **CONCEPT 브랜치에 대하여.** Studio/project contract가 explanatory body record를 지원하므로
|
||
> Concept를 정식 kind로 사용한다. `root-tree-contract.md`, `record-kinds.md`, review checklist와
|
||
> writing template도 동일 기준으로 맞췄다.
|
||
>
|
||
> - **CASE** — 재현하고 검증한 하나의 사건. "이때 이런 일이 일어났고 이렇게 확인했다."
|
||
> - **CONCEPT** — 이 코드베이스에 실제 구현된 메커니즘·상태 모델·경계의 설명. 일반 튜토리얼이 아니다.
|
||
> - **REFERENCE** — 그 메커니즘에서 뽑은 재사용 기준. 규칙·적용조건·예외로 나뉜다.
|
||
>
|
||
> 셋의 관계는 보통 CONCEPT ← CASE → REFERENCE다. 같은 근거에서 여러 kind가 필요하면 한 기록에
|
||
> 섞지 않고 관계로 연결한다.
|
||
|
||
---
|
||
|
||
TOPIC
|
||
커밋 모호성 — "모른다"를 결과로 유지하기
|
||
commit-ambiguity-as-a-result
|
||
|
||
├── CONCEPT
|
||
│ ├── 커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지
|
||
│ └── 트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문
|
||
├── CASE
|
||
│ ├── `pg_terminate_backend`가 `57P01`로 도착하고 커밋 레코드는 이미 WAL에 있었다
|
||
│ ├── high-water mark가 "본 위치"를 뜻해서 재전달된 변경이 영구히 사라졌다
|
||
│ ├── 번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다
|
||
│ └── 커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다
|
||
├── REFERENCE
|
||
│ ├── 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다
|
||
│ ├── 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다
|
||
│ ├── 커밋 모호성 판정은 넓혀도 좁혀도 해롭다
|
||
│ └── 실패 번역 사슬의 순서는 계약이다
|
||
├── OPEN QUESTION
|
||
│ └── 커밋 모호성 계약 레인이 이 리비전에서 통과하는지 실행으로 확인되지 않았다
|
||
└── DECISION
|
||
├── completion-unknown은 자동으로도 수동으로도 재시도하지 않는다
|
||
├── 전송·업무·스트림 증거는 세 축이고 서로를 함의하지 않는다
|
||
└── 재시도 단위는 statement가 아니라 유스케이스 전체다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
조립 소유권 — 통제와 그 의존을 같은 곳이 소유하기
|
||
assembly-ownership
|
||
|
||
├── CONCEPT
|
||
│ ├── Spring 조립의 세 경로와 각각이 결정하는 것
|
||
│ └── 조건부 빈의 평가 시점 — 파싱 시점과 등록 시점
|
||
├── CASE
|
||
│ ├── 스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다
|
||
│ ├── outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다
|
||
│ ├── 관측을 필수 생성자 인자로 만든 수정을 조립이 6인자 생성자로 되돌렸다
|
||
│ ├── 이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다
|
||
│ ├── `@ConditionalOnBean(DataSource.class)`가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다
|
||
│ ├── 넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다
|
||
│ ├── 시작 검증기 13개 규칙이 유일한 조립 지점에서 호출되지 않는다
|
||
│ └── subsystem 전체가 미배선인데 그것을 켜는 flag는 startup 검사를 수행한다
|
||
├── REFERENCE
|
||
│ ├── `@Bean`이 있다는 것은 조립 증거가 아니다
|
||
│ ├── `@ConditionalOnBean`은 조건이 만족될 수 있는지까지 확인해야 한다
|
||
│ ├── 시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다
|
||
│ ├── 조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다
|
||
│ ├── "꺼짐"은 조건의 반복이 아니라 구조여야 한다
|
||
│ └── 프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다
|
||
├── OPEN QUESTION
|
||
│ ├── 부팅 한 번으로 확증 가능한 두 건이 아직 정적 추론으로만 남아 있다
|
||
│ └── `@ConditionalOnBean` 사슬의 실제 평가 순서를 확인하지 않았다
|
||
└── DECISION
|
||
├── 마스터 스위치는 루트 하나가 소유하고 자식 설정은 조건을 갖지 않는다
|
||
└── 풀이 필요한지는 "JPA가 켜졌나"가 아니라 "커넥션이 필요한 capability가 있나"로 묻는다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
게이트가 증명하지 않는 것
|
||
what-a-gate-does-not-prove
|
||
|
||
├── CONCEPT
|
||
│ ├── strict test lane — 발견하지 못하면 실패하는 레인
|
||
│ └── 증거 등급과 provenance — R1과 R2를 가르는 것
|
||
├── CASE
|
||
│ ├── "certified"라 불리던 레인이 threshold를 하나도 비교하지 않고 있었다
|
||
│ ├── 문서 계약 테스트의 단언 경계 밖에 발견된 드리프트 세 건이 전부 있었다
|
||
│ ├── 릴리스 게이트가 읽는 증거를 아무도 생산하지 않는다
|
||
│ ├── 패키지 카탈로그가 트리보다 아홉 개 적어서 사이클이 통과했다
|
||
│ ├── 릴리스 레인이 매트릭스 세 버전 중 첫 번째만 돌리고 세 개를 커버로 기록했다
|
||
│ └── 다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다
|
||
├── REFERENCE
|
||
│ ├── 빠뜨림이 통과가 되는 게이트는 게이트가 아니다
|
||
│ ├── 문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다
|
||
│ ├── 아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다
|
||
│ ├── 계약 테스트는 어댑터가 실제로 돌리는 statement를 실행해야 한다
|
||
│ └── 아무것도 발견하지 못한 레인은 성공이 아니라 실패여야 한다
|
||
├── OPEN QUESTION
|
||
│ └── 컨테이너가 필요한 레인의 실제 결과를 실행으로 확인하지 않았다
|
||
└── DECISION
|
||
├── 인증 레인만 Docker 가드를 달지 않는다
|
||
├── 성능 측정은 릴리스 게이트에 넣지 않는다
|
||
└── 후보 증거는 통과해도 R1에 머무르고 R2는 별도 게이트가 판정한다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
타입으로 카디널리티와 개인정보를 막기
|
||
bounding-by-type
|
||
|
||
├── CONCEPT
|
||
│ ├── 서명된 커서의 구조와 검증 순서
|
||
│ └── 카디널리티 경계를 타입으로 표현하기
|
||
├── CASE
|
||
│ ├── 시그니처가 payload를 받지 않는데 예외 메시지로 PII가 로그에 남았다
|
||
│ ├── 진단 리포트가 살아 있는 리소스를 담지 않도록 값 타입을 좁혔다
|
||
│ └── 커서 서명 검증이 길이·상수시간·순서를 전부 지켜야 했던 이유
|
||
├── REFERENCE
|
||
│ ├── 이름은 값이 아니라 registry key다
|
||
│ ├── 관측을 위해 수집한 데이터가 관측 대상보다 위험할 수 있다
|
||
│ ├── path·identifier는 등록하고 value는 바인딩한다
|
||
│ └── sanitize가 아니라 reject가 기본이다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
├── tenant id는 메트릭 태그가 되지 않는다
|
||
└── 커서에 서명하는 이유는 기밀성이 아니라 무결성이다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
중복 장치 — 조립된 쪽이 약한 쪽일 때
|
||
duplicate-mechanisms
|
||
|
||
├── CASE
|
||
│ ├── 클라이언트가 준 엔드포인트가 SSRF 가드가 아니라 약한 private 사본을 지났다
|
||
│ ├── 요청 식별자를 클라이언트가 고를 수 없다는 정책이 뒤에 도는 필터에 뒤집혔다
|
||
│ ├── JPA 플랫폼 capability 대부분에 production 소비자가 없다
|
||
│ ├── forwarded 헤더 신뢰 판정이 Nginx에만 있고 Java 정책 421 LOC은 배선되지 않았다
|
||
│ └── 재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다
|
||
├── REFERENCE
|
||
│ ├── 중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다
|
||
│ ├── `Atomic*` 타입의 존재는 원자성의 증거가 아니다
|
||
│ └── 같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
└── 감사 메커니즘은 엔티티당 정확히 하나여야 한다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
문서 드리프트의 방향
|
||
drift-direction
|
||
|
||
├── CASE
|
||
│ ├── 지원 매트릭스가 코드와 반대를 적었고, 그 오해가 소비자에게 자기 멱등성을 생략하게 한다
|
||
│ ├── README의 활성화 recipe를 그대로 따르면 애플리케이션이 시작되지 않는다
|
||
│ ├── 출하 default 조합이 첫 write에서 예외를 던진다
|
||
│ ├── 문서가 UUIDv7이라 말하고 생성되는 것은 v4다
|
||
│ ├── 선택할 수 없는 브로커가 지원 매트릭스에 기능 목록과 함께 실려 있다
|
||
│ └── 다섯 문서가 "exactly 19 leaf"라고 적고 레지스트리는 62다
|
||
├── REFERENCE
|
||
│ ├── 과대 진술 문서를 과소보다 먼저 고친다
|
||
│ └── 문서의 수치는 세지 말고 파생하거나 게이트로 붙든다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
├── 문서 계약 테스트의 단언 범위를 capability 표까지 넓힐 것인가
|
||
└── 지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
플랫폼 가족 사이의 학습 전이
|
||
learning-transfer-between-families
|
||
|
||
├── CASE
|
||
│ ├── 두 번째 플랫폼이 첫 번째의 bridge 부재는 막고 게이트 배선은 옮기지 않았다
|
||
│ └── 같은 자격 증명 회전 결함이 한 가족에서 닫히고 다른 가족에서 재현됐다
|
||
├── REFERENCE
|
||
│ └── 레지스트리로 표현된 규칙은 전이되고 CI로 표현된 규칙은 전이되지 않는다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
└── gRPC 플랫폼은 build-only로 두고 애플리케이션 도달 경로를 먼저 정한다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
owner-safe 상태 기계 — 소유권을 SQL에 적기
|
||
owner-safe-state-machines
|
||
|
||
├── CONCEPT
|
||
│ ├── fenced lease — 만료 시각만으로는 부족한 이유
|
||
│ ├── CAS 튜플과 update count가 답이 되는 구조
|
||
│ └── capability_schema_registry — 스키마 적용과 사용 승인의 분리
|
||
├── CASE
|
||
│ ├── lease가 만료 시각만 기록하고 소유자를 기록하지 않아 terminal state가 되돌려졌다
|
||
│ ├── transition digest가 "누가·언제"만 덮고 "무엇"을 덮지 않아 다른 전이를 같다고 보고했다
|
||
│ ├── 활성 트랜잭션 검사가 data source를 묻지 않아 다른 커넥션에서 커밋됐다
|
||
│ ├── 만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다
|
||
│ └── native claim이 `@Version`을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다
|
||
├── REFERENCE
|
||
│ ├── CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다
|
||
│ ├── 시간은 DB에서, 그리고 행을 잠근 다음에 읽는다
|
||
│ ├── 만료된 claim과 만료된 실행은 다르게 다뤄야 한다
|
||
│ └── digest는 길이 프레이밍하고 버전을 붙인다
|
||
├── OPEN QUESTION
|
||
│ └── V2 상태 기계 넷의 컨테이너 레인이 실행되지 않았다
|
||
└── DECISION
|
||
├── capability는 스키마 적용과 사용 승인을 분리한다
|
||
└── 상태 기계 구현은 Spring stereotype을 갖지 않는다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
트랜잭션 데드라인과 커넥션 예산
|
||
transaction-deadline-and-pool
|
||
|
||
├── CONCEPT
|
||
│ ├── 호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파
|
||
│ └── `REQUIRES_NEW`의 커넥션 비용과 풀 사이징 제약
|
||
├── CASE
|
||
│ ├── `connection-timeout: 5s`가 모든 prod 배포를 시작 실패시켰고 local만 통과했다
|
||
│ ├── validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다
|
||
│ └── `REQUIRES_NEW`가 바깥 커넥션을 핀한 채 새 커넥션을 딴다
|
||
├── REFERENCE
|
||
│ ├── 데드라인은 호출 예산에서 시작해 세 단계로 좁힌다
|
||
│ ├── 쓰기 트랜잭션에는 유한 타임아웃이 필수다
|
||
│ └── 세션 스코프 설정은 풀로 돌아간 커넥션에 남는다
|
||
├── OPEN QUESTION
|
||
│ └── 풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다
|
||
└── DECISION
|
||
├── 트랜잭션 템플릿은 모드별로 미리 만들어 둔다
|
||
└── `inRootWrite`는 suspend하지 않고 fail-fast한다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
스키마 소유권과 capability 스트림
|
||
schema-ownership-and-capability-streams
|
||
|
||
├── CONCEPT
|
||
│ └── 독립 Flyway 스트림과 baseline version 0
|
||
├── CASE
|
||
│ ├── 두 트리가 다 V1부터 번호를 매겨 공유 history가 하나를 건너뛸 수 있었다
|
||
│ ├── `char(64)`와 `varchar(64)` 불일치를 H2가 가리고 있었다
|
||
│ ├── 레지스트리 컬럼이 38자 경로에서 짧아 "더 짧은 경로를 적는" 우회를 유혹했다
|
||
│ ├── Flyway location customizer가 운영자가 바인딩한 값을 덮어썼다
|
||
│ └── messaging 마이그레이션 두 leaf가 같은 디렉터리에서 `V2`를 둘 만들었다
|
||
├── REFERENCE
|
||
│ ├── 마이그레이션 스트림은 자기 history 테이블을 갖는다
|
||
│ ├── 로컬이 다른 DB면 로컬 테스트는 다른 시스템에 대한 진술이다
|
||
│ └── 적용된 마이그레이션의 checksum은 그것을 돌린 모든 배포에 대한 약속이다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
├── Flyway가 스키마를 소유하고 런타임 롤은 DDL 권한을 갖지 않는다
|
||
└── Repair는 모드가 아니라 운영자가 호출하는 작업이다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
Redis 명령 admission과 카탈로그
|
||
redis-command-admission
|
||
|
||
├── CONCEPT
|
||
│ └── 명령 카탈로그와 admission 아홉 단계
|
||
├── CASE
|
||
│ ├── "build gate"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다
|
||
│ ├── 의미 어댑터 다섯이 gateway를 직접 불러 admission 아홉 단계를 건너뛴다
|
||
│ ├── NOSCRIPT 복구가 다섯 벌이고 넷은 스크립트 레지스트리를 지나지 않는다
|
||
│ └── startup probe가 production에서 한 번도 실행되지 않는다
|
||
├── REFERENCE
|
||
│ ├── 서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다
|
||
│ └── 단일 admission point는 우회 경로를 세어야 성립한다
|
||
├── OPEN QUESTION
|
||
│ └── Redis 토폴로지 레인이 실행되지 않아 key spec 드리프트가 확인되지 않았다
|
||
└── DECISION
|
||
└── 분류되지 않은 명령은 fail-closed로 거부한다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
HTTP 실패 분류와 재시도 안전성
|
||
http-failure-classification
|
||
|
||
├── CONCEPT
|
||
│ └── 전송 실패의 단계와 범주 — `AttemptStage`와 `FailureCategory`
|
||
├── CASE
|
||
│ ├── 붉은 테스트를 제품 결함으로 읽은 오진 — 듀얼스택 `localhost`가 TLS 실패를 가린다
|
||
│ └── 로컬 거부 경로에서 회로 브레이커 permission이 반환되지 않는다
|
||
├── REFERENCE
|
||
│ ├── 원인 사슬은 가장 구체적인 분류가 이기도록 순회한다
|
||
│ ├── 재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다
|
||
│ ├── 분류기는 엔진이 남긴 것만 볼 수 있다
|
||
│ └── 런타임의 모양에 대한 주장은 런타임에서 확인한다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
└── 재시도 안전성은 증거에 기반해 판정한다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
파일 상태 기계와 물리 정리의 seam
|
||
fileserver-state-and-fencing
|
||
|
||
├── CONCEPT
|
||
│ └── 파일 상태 기계와 READY가 뜻하는 것
|
||
├── CASE
|
||
│ ├── scriptable 콘텐츠 탐지가 BOM·NUL·주석으로 우회된다
|
||
│ ├── cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다
|
||
│ ├── claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다
|
||
│ └── README가 "노출된 setting도 bean도 없다"고 적은 능력에 production bean 여덟이 있다
|
||
├── REFERENCE
|
||
│ ├── 공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다
|
||
│ ├── 조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다
|
||
│ └── 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
└── 물리 경로와 원본 파일명을 저장하지 않는다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
객체 스토리지의 단계적 수명주기와 권한 분리
|
||
objectstorage-staged-lifecycle
|
||
|
||
├── CONCEPT
|
||
│ └── staged lifecycle과 서명된 grant
|
||
├── CASE
|
||
│ ├── APPLY를 켜는 설정은 있고 승인을 검증하는 bean은 없다
|
||
│ ├── 직접 multipart의 마지막 part는 grant를 받을 수 없다
|
||
│ └── 서명된 grant의 endpoint 검증이 upload 경로에만 있다
|
||
├── REFERENCE
|
||
│ ├── 권한이 센 절반이 설정 한 줄로 켜지면 안 된다
|
||
│ └── 이진 승인 코덱은 decode 후 재인코딩이 원본과 같아야 한다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
└── legacy 채택은 서로 다른 두 승인자의 서명을 요구한다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
자기 공시 — 등급을 스스로 낮춰 적기
|
||
self-disclosure-grading
|
||
|
||
├── CONCEPT
|
||
│ └── 네 단계 공시 등급 — modelled에서 production-verified까지
|
||
├── CASE
|
||
│ ├── 등급표 13행 중 일곱을 스스로 강등하고 한 행만 관측과 어긋났다
|
||
│ ├── 시작 검증기가 커서 서명 키를 요구하는데 그 키로 서명하는 코드가 없다
|
||
│ └── build-only 등급이 90개 파일의 미조립을 오늘의 사고에서 면제한다
|
||
├── REFERENCE
|
||
│ ├── 등급은 네 단계로 나누고 관측보다 높게 적지 않는다
|
||
│ └── `runtime_memberships`를 먼저 읽고 심각도를 정한다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
└── 능력 등급은 코드가 아니라 실행된 증거에서 파생한다
|
||
|
||
---
|
||
|
||
TOPIC
|
||
멀티테넌시 격리가 조용히 무력화되는 방법
|
||
multitenancy-isolation
|
||
|
||
├── CONCEPT
|
||
│ ├── RLS가 성립하기 위한 세 전제
|
||
│ └── 네 가지 멀티테넌시 전략과 각각의 격리 경계
|
||
├── CASE
|
||
│ ├── RLS가 아무것도 하지 않는 세 가지 방법
|
||
│ ├── `search_path`가 풀로 돌아간 커넥션에 남아 다음 tenant가 상속한다
|
||
│ └── tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다
|
||
├── REFERENCE
|
||
│ ├── Hibernate filter는 보안 경계가 아니다
|
||
│ ├── tenant 컬럼이 있는 테이블의 모든 unique 제약에 그 컬럼이 들어가야 한다
|
||
│ └── 격리 설정은 트랜잭션 로컬이어야 한다
|
||
├── OPEN QUESTION
|
||
│ └── (없음)
|
||
└── DECISION
|
||
└── 클래스패스에 있는 것은 실행 동의가 아니다
|
||
|
||
---
|
||
|
||
|
||
---
|
||
|
||
TOPIC
|
||
계약 도메인과 경계값 — 타입·문법·상한이 실제 허용 범위와 맞는가
|
||
contract-domain-and-bounds
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── encode가 발급한 2046~2048-byte cursor를 decode가 거부한다
|
||
│ ├── canonical transaction boundary documentation과 실제 dual stack 불일치
|
||
│ ├── `SpecificationPolicy`는 `Specification.unrestricted()`를 bounded로 오인한다
|
||
│ ├── `normalize`는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다
|
||
│ ├── 다중 키 fan-in 중 HyperLogLog `merge`만 budget이 없다
|
||
│ ├── `requireIdentifier`의 다섯 검사 중 둘은 도달할 수 없다
|
||
│ ├── `BoundedDataBufferFlux`의 두 연산자가 이름만 있고 아무것도 하지 않는다
|
||
│ ├── "상한을 두고 읽는다"는 본문 핸들러가 전부 읽은 뒤에 자른다
|
||
│ ├── 의미 지문이 길이 프레이밍 없이 구분자로 만들어진다
|
||
│ ├── `maxArrayElements`가 선언만 되고 강제되지 않으며, 바이트 예산 백스톱도 없다
|
||
│ └── 기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다
|
||
├── REFERENCE
|
||
│ └── (추가 없음)
|
||
├── OPEN QUESTION
|
||
│ ├── response/LRO invariant enforcement boundary
|
||
│ ├── DomainContextKey same-name different-type collision
|
||
│ ├── bounded operational record identifiers
|
||
│ └── `CapabilitySupport.constraints`의 bounded/report-safe 계약이 타입에서 강제되지 않음
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
---
|
||
|
||
TOPIC
|
||
런타임 도달성과 조립 — 구현된 능력이 실제 경로에 설치되는가
|
||
runtime-reachability-and-composition
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── notification consumer는 diagnostic failure를 authoritative failure로 바꿀 수 있다
|
||
│ ├── notification fail-open consumer가 logger failure를 격리하지 않는다
|
||
│ ├── `TransactionProfileRegistry`는 declarative retry 제거 후 legacy residue 후보
|
||
│ ├── Stable runtime-role verification이 startup에서 실제 policy를 적용하지 않는다
|
||
│ ├── V8 atomic admin claim은 production service에 연결되지 않았고 completion 모델도 미완성이다
|
||
│ ├── vendor selector의 fail-fast 계약이 shipped composition에 설치돼 있지 않다
|
||
│ ├── `change-streams=true`는 거부되지 않고 조용히 버려지며, 그 결과 startup validator의 한 분기가 production에서 도달 불가다
|
||
│ ├── 서버 측 deadline이 경로마다 다르게 적용되고, 문서가 지목한 메커니즘은 production 호출자가 0이다
|
||
│ ├── `changeStreams` flag는 `false`로 고정돼 있는데, 소비자 bean은 그것과 무관하게 조립된다
|
||
│ ├── 프로파일의 TLS·타임아웃·풀·Stable API가 driver에 도달하지 않는다
|
||
│ ├── 모듈의 존재 논거인 `UuidCodec`에 production 소비자가 없다
|
||
│ ├── production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다
|
||
│ ├── SDK가 선언한 두 진입점에 구현이 없다
|
||
│ ├── 멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다
|
||
│ ├── 배선된 캐시 필터의 `no-store`가 배선된 조건부 읽기 경로를 무력화하고, 둘을 조정하려고 만든 패키지는 참조 0이다
|
||
│ ├── `VirtualThreadProfile.propertyName()`이 아무것도 게이트하지 않는 이름을 반환한다
|
||
│ ├── 이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다
|
||
│ ├── `SpringMvcRouteInventoryCollector` 138줄에 참조가 하나도 없다
|
||
│ ├── `WebPlatformStartupValidator`가 시작 시 실행되지 않는다
|
||
│ ├── 설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다
|
||
│ ├── 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다
|
||
│ ├── 5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다
|
||
│ ├── 연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 `GraphQlOperationNamePolicy`를 쓴다
|
||
│ ├── 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다
|
||
│ ├── 프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다
|
||
│ ├── "기본 비활성"은 존재하지 않는 스위치의 기본값을 서술한다
|
||
│ ├── 출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다
|
||
│ ├── 8개 profile validator 중 조립에서 실행되는 것은 3개
|
||
│ ├── `messaging-cloudevents`는 출하 leaf이고 starter의 의존이며 소비자가 없다
|
||
│ ├── 접근 검사가 두 갈래로 존재하고, 조립된 쪽이 진단이 약한 쪽이다 (§8.3)
|
||
│ ├── claim-check는 starter에 배선 코드가 한 줄도 없다
|
||
│ ├── admin 스위치가 가드를 켜고 서비스는 켜지 않는다
|
||
│ ├── 조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다
|
||
│ ├── 시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다
|
||
│ └── 같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다
|
||
├── REFERENCE
|
||
│ ├── (추가 없음)
|
||
│ └── 검증기는 발행이 아니라 주입이 강제다
|
||
├── OPEN QUESTION
|
||
│ └── (추가 없음)
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
---
|
||
|
||
TOPIC
|
||
검증 경로 커버리지 — 초록불이 실제 production 경로를 검증하는가
|
||
verification-path-coverage
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── Stable completion-evidence capability가 shipped composition에 설치되지 않는다
|
||
│ ├── `collection-fetch-pagination` blocking release gate가 실제 위험을 증명하지 않는다
|
||
│ ├── nightly workflow가 광고하는 세 가지 중 하나를 lane이 실제로 관측하지 않는다
|
||
│ ├── selected base card `jpa-flyway-migration`의 producer가 현재 revision에서 실패한다
|
||
│ ├── selected base card 3개의 evidence tag가 production code 없는 fixture로 충족된다
|
||
│ ├── D3 gateway가 문서화한 검사 순서에 존재하지 않는 단계가 있다
|
||
│ ├── admin gateway의 두 audit 경로 중 하나만 fail-closed다
|
||
│ ├── sharding admin gateway의 네 작업 중 셋은 어떤 입력으로도 완료될 수 없다
|
||
│ ├── promotion 증거 어휘가 둘이고, gate는 하나만 검사한다
|
||
│ ├── 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다
|
||
│ ├── release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다
|
||
│ ├── 소비자가 없는 fixture 셋
|
||
│ ├── R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다
|
||
│ ├── 문서가 지목한 기본값 위치와 test 목록이 실제와 다르다
|
||
│ ├── `TransferBufferPool.maxBorrowedBytes()`가 자기 회귀 test를 지목하는데 그 test가 읽지 않는다
|
||
│ ├── README readiness 표와 build.gradle 주석이 실제 소스와 어긋난다
|
||
│ ├── Pub/Sub 채널만 렌더 크기 검증을 받지 않는다
|
||
│ ├── 위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다
|
||
│ ├── 용량 보호 계층 전체(41 main files)가 자기 테스트 픽스처 안에서만 실행된다
|
||
│ ├── 크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다
|
||
│ ├── `@oneOf` 게이트와 런타임 검증기가 미배선이고, "플랫폼이 강제한다"는 서술이 그것을 넘어선다
|
||
│ ├── 파싱·검증 실패에 플랫폼 매퍼가 없다
|
||
│ ├── 실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다
|
||
│ ├── `messaging-reliability-api`는 main 13파일 · 817 LOC에 테스트가 0개다
|
||
│ ├── 스키마 호환성 검증기는 출하 leaf에 있고, main 코드에서 호출되지 않는다
|
||
│ ├── `messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다
|
||
│ └── `MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다
|
||
├── REFERENCE
|
||
│ └── (추가 없음)
|
||
├── OPEN QUESTION
|
||
│ ├── cache-redis/httpclient의 support project dependency 필요성 재검증
|
||
│ └── 실제 성능·용량 특성을 어떤 모듈에서도 측정하지 않았다
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
---
|
||
|
||
TOPIC
|
||
상태 소유권과 동시성 — claim·lease·replay·메모리 상태의 소유자를 지키기
|
||
state-ownership-and-concurrency
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── notification admin atomic claim contract가 service에서 사용되지 않음
|
||
│ ├── `inspect()`와 `claim()`이 만료된 COMPLETED row를 동시에 다른 상태로 해석한다
|
||
│ ├── `complete()`의 replay 판정이 `replayTtl` 변경을 무시한다
|
||
│ ├── persistent byte quota가 실제 admission에서 집행되지 않는다
|
||
│ ├── quota reclaim은 최대 64개 committed row만 처리하고 남은 byte를 조용히 버린다
|
||
│ ├── direct `FileQuotaService.commit()`은 만료 reservation을 commit한다
|
||
│ ├── recovery queue의 `enqueue()`는 concurrent upsert가 아니다
|
||
│ ├── cleanup crash-reclaim은 `MAXIMUM_ATTEMPTS`를 우회해 poison item을 무한 재시도할 수 있다
|
||
│ ├── provider 호출 뒤 recipient projection write가 lease fencing을 우회한다
|
||
│ ├── reconciliation `FOR UPDATE SKIP LOCKED`는 worker 처리 구간을 claim하지 않는다
|
||
│ ├── `recordApplied`는 문서화된 fence 계약을 구현하지 않고, 보호를 역전시킨다
|
||
│ ├── Flamingock lease로는 어떤 migration도 실행할 수 없고, javadoc은 다르게 적는다
|
||
│ ├── recovery package에 쓰이는 어휘와 쓰이지 않는 어휘가 나란히 있다
|
||
│ ├── nonce replay 경계가 결과를 읽고 버린다
|
||
│ ├── `Number`가 허용 목록에 있어 가변 숫자 타입이 REPLAYABLE로 인증된다
|
||
│ ├── `GrpcAdmissionController.tryAdmit()`의 동시성 경계가 동시성 아래에서 성립하지 않는다
|
||
│ ├── `GrpcOutcomeReplay`가 제거 경로 없는 인메모리 저장소다
|
||
│ ├── `GrpcCompletionReconciler`가 요청 경로에서 동기화 없는 `ArrayList`를 변경한다
|
||
│ ├── 배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다
|
||
│ └── 배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다
|
||
├── REFERENCE
|
||
│ └── (추가 없음)
|
||
├── OPEN QUESTION
|
||
│ └── notification derived idempotency key가 32-bit hash
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
---
|
||
|
||
TOPIC
|
||
스키마와 데이터 계약 — migration·index·collection 규칙의 실행 의미
|
||
schema-and-data-contracts
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── schema version 실패는 두 경로 중 어느 쪽도 온전하지 않다
|
||
│ ├── collection 이름 불변식이 aggregation executor의 서명에서 깨진다
|
||
│ ├── index diff가 실제로 비교하는 것은 두 필드뿐이다
|
||
│ ├── TTL이 두 곳에 선언되고, 규칙을 가진 쪽은 아무도 쓰지 않는다
|
||
│ ├── `check`에 붙은 `verifyJsonSchemaRuntimeGraph`가 실행되면 실패한다
|
||
│ └── 음수 `Retry-After` 헤더가 throttle 결과 대신 `IllegalArgumentException`을 만든다
|
||
├── REFERENCE
|
||
│ └── (추가 없음)
|
||
├── OPEN QUESTION
|
||
│ └── messaging schema qualification boundary
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
---
|
||
|
||
TOPIC
|
||
전송·프로바이더 의미론 — timeout·TLS·서명·실패 분류의 경계
|
||
transport-and-provider-semantics
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── timeout 초과 경로가 한 observation에 success와 failure를 모두 기록한다
|
||
│ ├── 동적 대상 DNS 핀 능력 검사가 블로킹 오버로드에만 있다
|
||
│ ├── SigV4가 서명한 `host`에 포트가 없어, 기본 포트가 아닌 엔드포인트에서 서명이 어긋난다
|
||
│ ├── SigV4 서명 키 파생이 비밀을 지울 수 없는 `String`으로 승격시킨다
|
||
│ ├── FCM만 "커밋 후 응답 손실 = ambiguous" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다
|
||
│ ├── 공유 provider 계약이 8종 중 3종에서만 상속되고, 강제 장치가 없다
|
||
│ └── 능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다
|
||
├── REFERENCE
|
||
│ └── (추가 없음)
|
||
├── OPEN QUESTION
|
||
│ └── (추가 없음)
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
---
|
||
|
||
TOPIC
|
||
보안 정책 강제 — 감사·PII·권한·SSRF 규칙이 실제 요청에 닿는가
|
||
security-policy-enforcement
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── property-access `IDENTITY` entity가 batch guard를 우회한다
|
||
│ ├── "모든 reveal은 감사된다"고 선언한 `AccessContext`를 읽는 코드가 저장소에 하나도 없다
|
||
│ ├── `publicPaths`가 `RestrictedPathRule`보다 먼저 등록되어, 넓은 공개 경로 하나가 관리 평면 규칙을 조용히 덮는다
|
||
│ ├── 연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다
|
||
│ └── 마스킹이 IPv4 만 알아서 검사가 나머지 주소 형태를 전부 통과시킨다
|
||
├── REFERENCE
|
||
│ └── (추가 없음)
|
||
├── OPEN QUESTION
|
||
│ └── permission component grammar
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
---
|
||
|
||
TOPIC
|
||
선언과 구현 드리프트 — 문서·매트릭스·설정 이름이 현재 코드와 같은가
|
||
declaration-and-document-drift
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── support README가 current architecture registry/history와 drift
|
||
│ ├── `RetryDecision.reason`의 “bounded” 설명과 constructor contract 불일치
|
||
│ ├── 폐기된 namespace guard의 탐색 domain이 operator가 읽는 두 문서를 덮지 않는다
|
||
│ ├── 예외 계층의 "cause를 붙이지 않는다" 규칙에 문서화되지 않은 예외가 하나 있다
|
||
│ ├── CLAUDE.md의 의존성 서술이 세 항목 모두 틀렸다
|
||
│ ├── README의 세 가지 사실 오류
|
||
│ ├── CLAUDE.md가 대는 두 가드 중 하나는 저장소에 없다
|
||
│ ├── 그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다
|
||
│ ├── README의 `jackson-databind` 부재 주장이 현재 상태와 어긋난다
|
||
│ ├── 능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다
|
||
│ ├── 지원 매트릭스가 "모든 messaging leaf는 build-only"라고 적고, 가족 권위 문서는 그 문장이 틀렸다고 이미 기록했다
|
||
│ ├── 한 아티팩트 안의 서로 모르는 Kafka 스택 두 개 (MSG-015, 가족 문서가 미해결로 표시)
|
||
│ ├── `CompatibilityMatrix`에 `EXTENSION` 등급이 있고 항목이 없으며, bridge leaf가 표 밖에 있다
|
||
│ └── 증거 등급 모델 전체가 자동 실행 경로 밖에 있고, CLAUDE.md는 현재 시제로 서술한다
|
||
├── REFERENCE
|
||
│ └── (추가 없음)
|
||
├── OPEN QUESTION
|
||
│ └── (추가 없음)
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
---
|
||
|
||
TOPIC
|
||
런타임 계약 정확성 — 개별 정책·가드·연산이 자기 계약을 지키는가
|
||
runtime-contract-correctness
|
||
|
||
├── CONCEPT
|
||
│ └── (추가 없음)
|
||
├── CASE
|
||
│ ├── `IdFactory.newId()`의 “never-before-used” 문구 정밀화
|
||
│ ├── application-supplied `JpaRetryPolicy`가 valid execution에서 무시된다
|
||
│ ├── query SQL naming/observability composition 부재
|
||
│ ├── export surface split SSOT
|
||
│ ├── `MongoRegexPolicy.forbidden()`은 금지하지 않는다
|
||
│ ├── 태그 allowlist는 규약이지 강제가 아니다
|
||
│ ├── 구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다
|
||
│ ├── 발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 "근사에 불과하다"고 적은 사전검사다
|
||
│ ├── 패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다
|
||
│ ├── permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다
|
||
│ ├── `close()`가 실패하면 drain 스케줄러 스레드가 남는다
|
||
│ ├── `POOL_ROUTE_EXCEEDS_TOTAL` 위반 코드는 발화할 수 없다
|
||
│ ├── `AUTHENTICATION_FAILED`를 지우지 않는다는 `resumeHealthy`의 보장이, 관리자 평면에 노출된 2단계 시퀀스로 우회된다
|
||
│ ├── Thymeleaf 예외 메시지 삭제 가드가 프로덕션이 타지 않는 오버로드에만 있다
|
||
│ ├── `WebProblemSanitizer.alreadySafe`가 죽은 메서드이고 그 안의 조건도 죽어 있다
|
||
│ ├── 플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다
|
||
│ ├── 프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다
|
||
│ ├── 리액티브 전송에는 속도 제한 경로가 하나도 없다
|
||
│ ├── 선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다
|
||
│ ├── 원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다
|
||
│ ├── capability 12개 중 main 코드가 읽는 것은 3개, 거부하는 것은 1개
|
||
│ ├── 호환성 게이트를 가진 두 포맷은 build-only이고, 출하되는 유일한 코덱에는 게이트가 없다
|
||
│ ├── 브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다
|
||
│ ├── 저장소 어디에도 참조가 없는 타입 3개
|
||
│ ├── `GrpcStreamAdmission`도 같은 형태이고, per-caller 맵이 줄지 않는다
|
||
│ └── `GrpcSerializedStreamWriter`의 `DROP_OLDEST`가 잘못된 메시지의 바이트를 뺀다
|
||
├── REFERENCE
|
||
│ └── legacy storage/notification compatibility surface의 제거 조건 추적
|
||
├── OPEN QUESTION
|
||
│ └── isolation vocabulary와 legacy routing capability의 시차
|
||
└── DECISION
|
||
└── (추가 없음)
|
||
|
||
# Node Specifications
|
||
|
||
각 노드는 계약(`root-tree-contract.md`)이 요구하는 필드를 갖는다. `evidence`가 "없음"인 노드는 그
|
||
이유를 함께 적는다 — 명령 출력이 아닌 근거(코드·javadoc·ADR)로 충분한 경우와, 실행이 필요한데
|
||
하지 못한 경우를 구별한다. 후자는 `missing-verification`에 남고 대응하는 Open Question을 갖는다.
|
||
|
||
---
|
||
|
||
## TOPIC 1 — commit-ambiguity-as-a-result
|
||
|
||
### CASE — `pg_terminate_backend`가 `57P01`로 도착하고 커밋 레코드는 이미 WAL에 있었다
|
||
- slug: `commit-ambiguity-is-not-only-sqlstate-08`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §3.5
|
||
- code: `.../persistence-jpa/.../transaction/CommitFailureClassifier.java` · `docs/jpa/repository-adaptation.md` §4
|
||
- evidence: 없음 — 저장소가 기록한 컨테이너 레인 관측을 인용한다. 이 분석에서 재실행하지 않았다
|
||
- classification: 설계가 "커밋 모호성은 SQLSTATE class 08뿐"이라고 적었고, in-flight 커밋 중인 백엔드를 `pg_terminate_backend`로 끊었을 때 `57P01`(admin_shutdown)이 도착하며 그 시점에 커밋 레코드가 이미 WAL에 있을 수 있음을 컨테이너 레인이 보였다. 규칙이 `57P01/57P02/57P03`까지 넓어졌고 `CommitAmbiguityContractTest`가 SQLSTATE를 직접 assert해 다시 좁아지지 못하게 고정했다.
|
||
- missing-verification: `jpaPlatformFailureTest`를 이 리비전에서 실행하지 않았다. 확인한 것은 규칙의 현재 코드 형태와 그 근거 문장이다.
|
||
- relations: `reference:unknown-is-a-third-result` · `reference:ambiguity-rule-hurts-both-ways` · `decision:completion-unknown-is-never-retried` · `open-question:commit-ambiguity-lane-not-executed`
|
||
|
||
### CASE — high-water mark가 "본 위치"를 뜻해서 재전달된 변경이 영구히 사라졌다
|
||
- slug: `high-water-mark-swallowed-a-redelivered-change`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-2` · `analysis/06` §67
|
||
- code: `.../persistence-mongo/.../MongoChangeStreamPipeline.java:49,58-63` · `.../ReactiveMongoChangeStreamConsumer.java:199`
|
||
- evidence: `evidence/raw/135a-mongo-changestream-execution-probes.txt` · `evidence/raw/135-mongo-changestream-manifest-and-probes.txt`
|
||
- classification: mark가 이벤트 수신 즉시 전진하므로 "완료된 위치"가 아니라 "본 위치"를 뜻하고, resume이 같은 pipeline 인스턴스를 재사용해 mark가 남는다. probe 세 개(A/B/C)가 worker 하나·평범한 failover에서 checkpoint 없이 지나간 이벤트를 다음 이벤트의 checkpoint가 추월하고 그 사이 재전달분을 pipeline이 삼키는 것을 관측했다. 구독은 `RUNNING`, runbook 비어 있음, caller의 `Flux`는 정상 완료. 세 테스트가 각각 절반씩만 보고 "본 적 있지만 완료되지 않은 위치"라는 제3의 상태가 없다.
|
||
- missing-verification: probe는 in-memory 구성이다. 실제 replica set failover에서 같은 순서가 재현되는지는 확인하지 않았다.
|
||
- relations: `reference:unknown-is-a-third-result` · `case:outbox-chain-behind-an-unsatisfiable-condition`
|
||
|
||
### CASE — 번역되지 않은 경합 예외가 재시도 코디네이터의 catch를 통째로 비껴갔다
|
||
- slug: `untranslated-contention-bypassed-the-retry-catch`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §3.6
|
||
- code: `.../persistence-jpa/.../transaction/PersistenceFailureTranslatorChain.java` · `.../OptimisticConflictTranslator.java`
|
||
- evidence: 없음 — 저장소의 javadoc이 사후 기록으로 남긴 회귀다
|
||
- classification: 재시도 코디네이터가 `JpaPersistenceException`만 catch하는데 executor는 아무것도 번역하지 않고 템플릿을 돌렸다. 그래서 경합이 실제로 만들어내는 실패 — Hibernate `OptimisticLockException`, Spring `OptimisticLockingFailureException`, raw 직렬화/데드락 `DataAccessException` — 이 번역되지 않은 채 executor를 떠나 catch를 완전히 비껴갔고 **production 경합은 재시도되지 않았다.** 단위 픽스처는 이미 번역된 예외를 던져서 초록불이었다.
|
||
- missing-verification: 현재 사슬이 네 단계 순서를 지키는지는 코드로 확인했으나, 경합을 실제로 일으켜 재시도를 관측하지 않았다.
|
||
- relations: `reference:translation-chain-order-is-a-contract` · `reference:unknown-is-a-third-result` · `case:a-certifying-lane-that-compared-nothing`
|
||
|
||
### CASE — 커밋 증거 프레임을 두 주인이 pop해서 바깥 트랜잭션의 실패가 익명이 됐다
|
||
- slug: `two-owners-popped-the-evidence-frame`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §3.4
|
||
- code: `.../persistence-jpa/.../transaction/TransactionEvidenceContext.java` · `.../TransactionEvidenceScope.java`
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: 증거 프레임을 pop하는 주인이 둘이었다 — 트랜잭션 매니저가 commit/rollback에서, executor의 `finally`가 operation·attempt 일치 시. 바깥과 같은 operation·같은 attempt를 가진 `REQUIRES_NEW` 안쪽 트랜잭션(기본 경로에서는 둘 다 attempt 1이라 **항상** 그렇다)에서 안쪽 매니저가 안쪽을 pop하고 executor가 바깥을 자기 것으로 오인해 pop했다. 이후 바깥의 커밋 실패는 operation 없이, attempt 1로, reconciliation key 없이 보고됐다. 해결은 깊이로 소유권을 식별하고 자기 프레임이 top일 때만 pop하는 것이다.
|
||
- missing-verification: 없음 — 수정된 형태와 그 테스트(`hasRawThreadLocalValue`)를 코드로 확인했다.
|
||
- relations: `reference:unknown-is-a-third-result` · `decision:retry-unit-is-the-use-case`
|
||
|
||
### REFERENCE — 모르는 것은 성공도 실패도 아닌 세 번째 결과여야 한다
|
||
- slug: `unknown-is-a-third-result`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 13 · `analysis/05` §16 · `analysis/19` §3.2
|
||
- classification: 완료 여부를 알 수 없는 상태를 성공이나 실패로 접으면 그 정보가 영원히 사라진다. 기준은 "결과 타입에 세 번째 변형이 있는가"이고, 그것이 있을 때만 caller가 재시도와 조정을 구별한다.
|
||
- scope: 커밋·발행·전달처럼 관측이 결과를 확정하지 못할 수 있는 모든 경계. 구현 예: `RetryDisposition.RECONCILE` · `TransactionResult.Indeterminate` · `WriteDisposition.UNDETERMINED` · `PublishCompletion.AMBIGUOUS` · `GrpcBusinessEvidence.COMMIT_UNKNOWN` · `ReplicaLagMonitor.replayedThrough(): Optional`
|
||
- exceptions: 아무것도 프로세스를 떠나지 않은 실패는 확정적이므로 세 번째 변형이 아니다. 남발하면 조정 큐가 커지고 운영자가 읽지 않고 비우게 된다.
|
||
- relations: `case:commit-ambiguity-is-not-only-sqlstate-08` · `case:high-water-mark-swallowed-a-redelivered-change` · `decision:completion-unknown-is-never-retried`
|
||
|
||
### REFERENCE — 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다
|
||
- slug: `make-the-unsafe-state-unrepresentable`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 14 · `analysis/05` §2.2 · `analysis/19` §3.2
|
||
- classification: "이 조합은 하면 안 된다"를 리뷰 규칙이나 정책 코드로 두면 정책 버그가 그것을 만들 수 있다. 생성자가 거부하면 그 값이 존재할 수 없다.
|
||
- scope: 두 필드의 조합이 의미상 불가능한 모든 값 타입. 예: `JpaFailureContext`가 `completionUnknown && retryable` 거부 + 예외가 안전 팩토리로 컨텍스트 재구성(이중 방어) · `PublishEvidence`의 두 거부 · `GrpcExecutionEvidence`가 "unary call with stream evidence" 거부
|
||
- exceptions: 생성자 검증은 값 타입에서만 싸다. 유효성이 다른 객체의 상태에 의존하면 조립 지점의 시작 검증기가 자리다.
|
||
- relations: `reference:unknown-is-a-third-result` · `decision:completion-unknown-is-never-retried` · `decision:three-axes-of-evidence`
|
||
|
||
### REFERENCE — 커밋 모호성 판정은 넓혀도 좁혀도 해롭다
|
||
- slug: `ambiguity-rule-hurts-both-ways`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §3.5 · `docs/adr/ADR-JPA-003-completion-unknown.md`
|
||
- classification: 모호성 규칙을 좁게 두면 실제로 모호한 실패가 확정 롤백으로 오인돼 재시도된다. 넓게 두면 평범한 풀 고갈과 서버 재시작이 조정 큐로 밀려들고 운영자가 그 큐를 읽지 않고 비우는 습관을 배운다. 기준은 "이 SQLSTATE가 커밋 **단계에서** 발생했고 드라이버가 어느 쪽인지 말해주지 못하는가" 두 조건의 교집합이다.
|
||
- scope: 커밋 실패 분류를 갖는 모든 데이터 계층. 판정 재료는 관측된 트랜잭션 phase와 SQLSTATE·예외 타입 두 가지이며 어느 하나만으로는 부족하다.
|
||
- exceptions: 커넥션 유실이 커밋을 불명으로 남겼는지는 phase에 달렸으므로 `08*`을 그 자체로 completion-unknown으로 두면 안 된다 — 벤더 분류기가 `CONNECTION_UNAVAILABLE`로만 두고 phase를 아는 커밋 분류기가 따로 판정하는 이유다.
|
||
- relations: `case:commit-ambiguity-is-not-only-sqlstate-08` · `decision:completion-unknown-is-never-retried`
|
||
|
||
### REFERENCE — 실패 번역 사슬의 순서는 계약이다
|
||
- slug: `translation-chain-order-is-a-contract`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §3.6
|
||
- classification: 번역 사슬은 (1) 이미 분류된 실패를 그대로 통과, (2) provider 고유 예외(SQLSTATE 없음), (3) 벤더 SQLSTATE, (4) 나머지는 손대지 않고 반환의 순서를 지켜야 한다. 재번역은 이미 들고 있는 attempt·key·completion 판정을 잃고, 무분류 포장은 프로그래밍 에러를 retryable로 보이게 한다.
|
||
- scope: 예외를 계층 간에 옮기는 모든 어댑터. 정책의 화이트리스트는 어떤 **범주**가 재시도될 수 있는지를 넓히지, **이 실패**에 대한 분류기의 판정을 뒤집지 않는다.
|
||
- exceptions: 도메인 예외·assertion 실패·`NullPointerException`은 퍼시스턴스 실패가 아니므로 번역 대상이 아니다.
|
||
- relations: `case:untranslated-contention-bypassed-the-retry-catch` · `reference:unknown-is-a-third-result`
|
||
|
||
### OPEN QUESTION — 커밋 모호성 계약 레인이 이 리비전에서 통과하는지 실행으로 확인되지 않았다
|
||
- slug: `commit-ambiguity-lane-not-executed`
|
||
- readiness: `OPEN`
|
||
- source: `final/document.md#6-2` · `analysis/05` §3.5, §15
|
||
- known: `CommitFailureClassifier`의 현재 규칙 형태(`40003` · `08*` · `57P0x` · transport break 4종)를 코드로 확인 · `CommitAmbiguityContractTest`가 SQLSTATE를 직접 assert함 · 해당 레인은 Docker 부재 시 skip이 아니라 실패한다
|
||
- unknown: 이 리비전에서 실제로 통과하는지 · `57P01` 관측이 PostgreSQL 16/17/18 전부에서 재현되는지
|
||
- next-verification: `./gradlew :adapter:outbound:persistence-jpa:jpaPlatformFailureTest --console=plain`, 매트릭스는 `-Pjpa.matrix.versions=16` 식으로 major당 한 번(다중 선택은 거부된다)
|
||
- decision-criterion: 세 major 통과 시 이 질문을 닫고 해당 Case의 `missing-verification`을 지운다. 실패하면 그 실패가 새 Case가 된다
|
||
- relations: `case:commit-ambiguity-is-not-only-sqlstate-08` · `open-question:container-lanes-not-executed`
|
||
|
||
### DECISION — completion-unknown은 자동으로도 수동으로도 재시도하지 않는다
|
||
- slug: `completion-unknown-is-never-retried`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-2` · `analysis/05` §2.2, §3.5
|
||
- decision-evidence: `docs/adr/ADR-JPA-003-completion-unknown.md` — Status: Accepted, Date: 2026-08-11, Design §17
|
||
- grounds: `case:commit-ambiguity-is-not-only-sqlstate-08` · `reference:unknown-is-a-third-result` · `reference:make-the-unsafe-state-unrepresentable`
|
||
- classification: ADR이 `TransactionCompletionUnknownException`을 자동으로도 수동으로도 재시도하지 않으며 `COMMITTING` 단계 관측 실패에 한해, `40003`·`08*`·transport break에 한해 생성한다고 명시한다. 근거는 "Retrying a possibly-committed write is the most damaging thing this platform could do... the failure is **epistemic rather than transient**"이고, 불변식을 정책이 아니라 타입 수준에서 강제한다.
|
||
- relations: `case:commit-ambiguity-is-not-only-sqlstate-08` · `decision:three-axes-of-evidence` · `reference:ambiguity-rule-hurts-both-ways`
|
||
|
||
### DECISION — 전송·업무·스트림 증거는 세 축이고 서로를 함의하지 않는다
|
||
- slug: `three-axes-of-evidence`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-5` · `analysis/20` §2.4
|
||
- decision-evidence: `docs/adr/ADR-GRPC-003-three-axis-execution-evidence.md` — Status: accepted, Date: 2026-08-30
|
||
- grounds: `reference:unknown-is-a-third-result` · `reference:make-the-unsafe-state-unrepresentable`
|
||
- classification: 상태 코드가 caller의 실제 질문에 대한 답이 아니라는 관찰에서 출발한다. `GrpcTransportEvidence`가 `NOT_SENT`와 `UNOBSERVED`를 구별하고, `GrpcBusinessEvidence`가 `COMMIT_UNKNOWN`을 실제 상태로 두며, `GrpcExecutionEvidence`가 관측 불가능한 조합을 거부한다. 응답 헤더를 확정 커밋으로 승격하는 것은 한 메서드를 고쳐야만 가능하다.
|
||
- relations: `decision:completion-unknown-is-never-retried` · `reference:unknown-is-a-third-result`
|
||
|
||
### DECISION — 재시도 단위는 statement가 아니라 유스케이스 전체다
|
||
- slug: `retry-unit-is-the-use-case`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `analysis/05` §3.6
|
||
- decision-evidence: `docs/adr/ADR-JPA-002-full-transaction-retry.md` · `.../FullTransactionRetryCoordinator.java`의 근거 javadoc · **주의(`EVD-336`)**: 같은 ADR 의 Enforcement 절이 지목하는 `RetryableJpaTransactionInterceptor` 는 이 저장소에 존재하지 않는다. 코디네이터는 조건부 빈으로 생성되지만 그것을 어떤 메서드에 적용할 어드바이스가 없어 프로덕션 호출자가 0 이다
|
||
- grounds: `case:untranslated-contention-bypassed-the-retry-catch` · `reference:unknown-is-a-third-result`
|
||
- classification: 낙관적 충돌·직렬화 실패에서 시도가 실패한 이유는 "계산의 기준이 된 상태가 더 이상 커밋된 상태가 아니라서"이므로, 같은 statement를 다시 쏘면 같은 틀린 답을 계산한다. executor에 다시 들어가면 새 트랜잭션과 새 Persistence Context를 받고 도메인 규칙이 reload된 데이터에 대해 다시 돈다. 예산은 attempt 수와 elapsed 두 상한을 모두 갖는다 — "타임아웃에 10초 걸리는 락에 3번 시도하면 30초짜리 호출이고 상류 호출자는 한참 전에 포기했다."
|
||
- relations: `case:a-retry-implementation-nobody-calls` · `reference:unknown-is-a-third-result`
|
||
|
||
### CONCEPT — 커밋 증거 단계 — NOT_STARTED에서 UNKNOWN까지
|
||
- slug: `commit-evidence-phases`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#3-2` · `analysis/05` §3.4, §2.3
|
||
- code: `.../persistence-jpa/.../api/transaction/TransactionCompletionEvidence.java` · `.../transaction/EvidenceAwareJpaTransactionManager.java` · `.../TransactionEvidenceContext.java`
|
||
- classification: 트랜잭션이 어디까지 갔는지를 여섯 단계로 기록하는 메커니즘의 설명이다. `NOT_STARTED → ACTIVE → COMMITTING → COMMITTED | ROLLED_BACK | UNKNOWN`이고, **`COMMITTING` 단계에서 관측된 실패만** completion-unknown이 될 수 있다는 것이 전체 모델의 핵심이다. `EvidenceAwareJpaTransactionManager`가 `mark(COMMITTING)`을 provider commit **직전**에 찍는 이유(그 안에서 프로세스가 죽으면 마지막 기록이 "물어봤고 모른다"여야 한다), 그리고 컨텍스트가 단일 슬롯이 아니라 `ArrayDeque` 스택인 이유(`REQUIRES_NEW`가 같은 스레드에서 바깥을 suspend한다)를 함께 설명한다. `ThreadLocal.withInitial`을 쓰지 않는 이유도 이 개념의 일부다 — 읽을 때마다 값을 설치하면 `clear()`가 방금 제거한 것을 다시 등록한다.
|
||
- missing-verification: 없음 — 구조와 근거를 코드로 확인했다
|
||
- relations: `case:two-owners-popped-the-evidence-frame` · `case:commit-ambiguity-is-not-only-sqlstate-08` · `reference:unknown-is-a-third-result` · `decision:completion-unknown-is-never-retried`
|
||
|
||
### CONCEPT — 트랜잭션 결과 대수 — 다섯 변형이 각각 답하는 질문
|
||
- slug: `transaction-result-algebra`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#3-2` · `analysis/03`(소유 SSOT — `TransactionResult`는 `application-core`에 있다) · `analysis/05` §3.2(`SpringPolicyTransactionPort` 쪽)
|
||
- code: `src/application-core/.../application/transaction/TransactionResult.java` · `.../persistence-jpa/.../transaction/SpringPolicyTransactionPort.java:92,215-260`(`PhaseSentinel`은 이 파일의 private 중첩 클래스다)
|
||
- classification: `TransactionResult<T>`가 sealed interface로 다섯 변형을 갖는 이유의 설명이다. `Committed`(물리 커밋 확인) · `Participating`(바깥 트랜잭션에 참여 — **커밋을 주장하지 않는다**) · `DeterminateRollback` · `Indeterminate`(**replay 권한을 주지 않는다**) · `CommittedWithPostCommitFailure`. 각 변형이 caller에게 허용하는 행동이 다르고, 특히 `Participating`과 `Indeterminate`는 boolean이나 예외로는 표현할 수 없는 상태다. `PhaseSentinel`이 `Ordered.HIGHEST_PRECEDENCE`로 등록되어 물리 소유자일 때만 phase를 기록하는 구조도 함께 다룬다.
|
||
- missing-verification: 없음
|
||
- relations: `concept:commit-evidence-phases` · `reference:unknown-is-a-third-result` · `reference:make-the-unsafe-state-unrepresentable`
|
||
|
||
---
|
||
|
||
## TOPIC 2 — assembly-ownership
|
||
|
||
### CONCEPT — Spring 조립의 세 경로와 각각이 결정하는 것
|
||
- slug: `three-assembly-paths`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#1-4`, `#7-1` · `analysis/18` · `analysis/14` §7.1
|
||
- code: `.../app-bootstrap/.../CaSkeletonApplication.java` · 8개 `AutoConfiguration.imports` · `.../app-bootstrap/src/main/resources/META-INF/spring.factories`
|
||
- evidence: `evidence/raw/tl-web-six-unowned-components.txt`
|
||
- classification: 이 저장소에서 빈이 컨텍스트에 들어오는 경로가 셋이고 각각 다른 질문에 답한다. **컴포넌트 스캔**(`@ComponentScan` + `AUTO_CONFIGURED_PACKAGES` 제외 정규식) · **`.imports`**(8개 파일 / 13개 클래스가 전부) · **`spring.factories`**(EnvironmentPostProcessor 6 · SpringBootExceptionReporter · AutoConfigurationImportFilter · ApplicationListener). 조립 결함을 판정할 때 세 경로를 다 읽어야 하는 이유와, main Java 4,614개 중 스테레오타입 보유가 206개(4.5%)뿐이라 조립 표면이 매우 좁다는 사실을 함께 설명한다.
|
||
- missing-verification: 없음 — 세 경로의 내용을 전수 판독했다
|
||
- relations: `case:scan-exclusion-without-an-owner` · `reference:read-the-assembling-side-first` · `reference:a-bean-is-not-composition-evidence`
|
||
|
||
### CONCEPT — 조건부 빈의 평가 시점 — 파싱 시점과 등록 시점
|
||
- slug: `when-conditions-are-evaluated`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §14.4
|
||
- code: `.../app-bootstrap/.../autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java`의 근거 javadoc · `.../PersistenceJpaRootAutoConfiguration.java`의 `jpaResolvedDataSourceCheck`
|
||
- classification: `@ConditionalOnBean`은 그 클래스가 **언제 평가되는가**에 따라 답이 달라진다. `@AutoConfiguration`으로 등록되면 다른 자동설정 이후에 평가되지만, plain `@Configuration`이 `@Import`로 들어오면 **클래스가 파싱되는 동안 — 대상 빈 정의가 존재하기 전에** 평가된다. 이 저장소가 그 함정을 실제로 밟았고(여덟 빈이 조용히 사라짐), 회피 방법 두 가지를 남겼다 — 검증기를 주입받지 않고 직접 생성하기, 그리고 조건을 루트로 올리기.
|
||
- missing-verification: 현재 리비전의 각 조건부 빈이 어느 시점에 평가되는지는 `ConditionEvaluationReport`로 확인하지 않았다
|
||
- relations: `case:conditionalonbean-evaluated-at-parse-time` · `reference:conditionalonbean-must-be-satisfiable` · `open-question:conditional-evaluation-order-unverified`
|
||
|
||
### CASE — 스캔에서 뺀 다섯 패키지의 컴포넌트 여섯을 두 자동설정 어느 쪽도 소유하지 않았다
|
||
- slug: `scan-exclusion-without-an-owner`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#3-1` · `analysis/14` §8.1, §51
|
||
- code: `.../CaSkeletonApplication.java` `AUTO_CONFIGURED_PACKAGES` · `.../mvc/autoconfigure/WebMvcPlatformAutoConfiguration.java` · `.../webflux/autoconfigure/WebFluxPlatformAutoConfiguration.java`
|
||
- evidence: `evidence/raw/tl-web-six-unowned-components.txt` · `evidence/meta/tl-web-six-unowned-components.json` · `evidence/terminal/tl-web-six-unowned-components.svg`
|
||
- classification: 합성 루트가 다섯 web 패키지를 스캔에서 제외하고 근거를 "Ownership by auto-configuration is what ties a control's presence to its dependency's"로 적었다. 두 자동설정은 실제로 존재하고 `.imports`에 있고 출하 컨텍스트에 도달하는데, 등록하는 `@Bean` 13개·10개가 전부 협력자이고 제외된 패키지의 컴포넌트 여섯은 어느 쪽도 소유하지 않는다(전부 main 참조 0). `ProblemCatalog`와 `WebProblemFactory`는 빈이고 그것을 쓰는 `@RestControllerAdvice`는 빈이 아니다.
|
||
- missing-verification: `/actuator/beans`로 여섯 타입의 부재를 직접 확인하지 않았다
|
||
- relations: `concept:three-assembly-paths` · `reference:a-bean-is-not-composition-evidence` · `reference:read-the-assembling-side-first` · `case:observation-downgraded-by-the-composition`
|
||
|
||
### CASE — outbox 가 둘이고, 출하되는 것은 messaging 플랫폼 쪽이 아니다
|
||
- slug: `outbox-chain-behind-an-unsatisfiable-condition`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-3` · `analysis/19` §7.1
|
||
- code: `.../messaging-spring-boot-starter/.../MessagingReliabilityAutoConfiguration.java` · `.../messaging-outbox-jdbc-postgresql/.../JdbcOutboxRepository.java` · `.../messaging-inbox-jdbc-postgresql/.../JdbcInboxRepository.java`
|
||
- evidence: `EVD-335` (`evidence/raw/335-two-outboxes-one-wired.txt`) · `evidence/raw/tl-outbox-unsatisfiable-condition.txt`(일부 구획에 셸 변수가 전개되지 않은 채 저장돼 있어 수치는 재측정했다)
|
||
- classification: 이 저장소에는 서로를 모르는 outbox 구현이 둘 있다. 스택 A는 `application-core/.../outbox/` 의 포트와 `persistence-jpa` 의 `@Repository OutboxStoreAdapter`, 그리고 `app-bootstrap` 의 `OutboxConfig` 가 `@Scheduled` 로 구동하는 `PublishPendingOutboxEventsUseCase` 다 — 배선되어 출하된다. 스택 B는 `messaging-reliability-api` 의 `OutboxRepository` 와 `JdbcOutboxRepository`(2,276 LOC)와 `OutboxRelay` 계열이며, `MessagingReliabilityAutoConfiguration:81` 의 `@ConditionalOnBean({OutboxRepository.class, OutboxEnvelopeFactory.class})` 뒤에 있다. `JdbcOutboxRepository` 에는 스프링 스테레오타입이 없고 그것을 만드는 `@Bean` 도 main 에 없으므로 조건은 프로덕션에서 참이 되지 않는다. 스타터의 javadoc 은 이 조건들을 결함이 아니라 계약으로 서술하고 저장소·팩토리 제공을 애플리케이션 책임으로 둔다 — 따라서 이것은 조건 결함이 아니라 **정본이 정해지지 않은 중복**이다. 두 스택은 저장 모델과 발행 경로가 다르므로 동시에 켜면 같은 이벤트가 두 번 적히거나 두 번 발행될 수 있다.
|
||
- missing-verification: `ConditionEvaluationReport`로 미충족 사유를 확인하지 않았다
|
||
- relations: `reference:conditionalonbean-must-be-satisfiable` · `concept:when-conditions-are-evaluated` · `open-question:one-boot-would-settle-two-findings`
|
||
|
||
### CASE — 관측을 필수 생성자 인자로 만든 수정을 조립이 6인자 생성자로 되돌렸다
|
||
- slug: `observation-downgraded-by-the-composition`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#5-2` · `analysis/19` §5.1
|
||
- code: `.../messaging-runtime-core/.../DefaultMessagePublisher.java` (`NO_OBSERVATION` 필드 javadoc) · `.../MessagingCoreAutoConfiguration.java:445`
|
||
- evidence: `evidence/raw/tl-messaging-observation-noop.txt` · `evidence/meta/…json` · `evidence/terminal/…svg`
|
||
- classification: 관측을 선택적 데코레이터가 아니라 필수 생성자 인자로 만든 수정이 runtime-core에 있고 그 javadoc이 "an unobserved publish path is how 'the dashboards were empty during the incident' happens"로 이유를 적는다. 자동설정은 6인자 생성자를 골라 `NO_OBSERVATION`(다섯 메서드 전부 빈 본문)을 주입하고, 방출자 넷은 main 참조 0이며 등록되는 것은 협력자 둘뿐이다. 같은 경로가 예외 메시지를 의도적으로 버리므로 둘이 합쳐지면 진단 흔적이 남지 않는다.
|
||
- missing-verification: 부팅 후 `/actuator/metrics`에 `messaging.*` 시리즈 부재를 확인하지 않았다
|
||
- relations: `reference:a-bean-is-not-composition-evidence` · `case:scan-exclusion-without-an-owner` · `open-question:one-boot-would-settle-two-findings`
|
||
|
||
### CASE — 이름만 AutoConfiguration이던 세 클래스가 capability 리포트에 Stable로 올라 있었다
|
||
- slug: `autoconfiguration-in-name-only`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §14.4
|
||
- code: `.../app-bootstrap/.../autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java`의 javadoc
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: 세 클래스가 `...AutoConfiguration`으로 이름 붙었고 plain factory였다 — `@AutoConfiguration`도, `@Bean`도, `.imports` 엔트리도 없었고 합성 루트는 그 패키지를 스캔에서 제외한다. 그래서 capability 리포트는 transaction retry·completion evidence·observability를 Stable로 나열했고 **돌고 있는 컨텍스트에는 그중 아무것도 없었다.** 개발자가 재시도되지 않는 재시도에 의존하는 코드를 배포할 수 있었다.
|
||
- missing-verification: 없음 — 수정 후 형태를 코드로 확인했다
|
||
- relations: `reference:a-bean-is-not-composition-evidence` · `case:a-retry-implementation-nobody-calls` · `decision:capability-grade-is-declared-not-inferred`
|
||
|
||
### CASE — `@ConditionalOnBean(DataSource.class)`가 클래스 파싱 시점에 평가되어 여덟 빈이 사라졌다
|
||
- slug: `conditionalonbean-evaluated-at-parse-time`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §14.4
|
||
- code: `.../autoconfigure/jpa/JpaPlatformRuntimeAutoConfiguration.java` · `.../PersistenceJpaRootAutoConfiguration.java`
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: 이 클래스는 루트가 **import**하지 auto-configure하지 않으므로, 그 조건이 클래스 파싱 중 — datasource 빈 정의가 존재하기 전에 — 평가됐고 따라서 **모든 실제 배포에서 false**였다. 아래 여덟 빈이 조용히 사라졌고 아무것도 그중 어느 것에도 의존하지 않아 아무것도 보고하지 않았다. datasource validator가 caller에 배선되고 Compose 레인이 "No qualifying bean"이라고 답했을 때에야 드러났다. 같은 함정을 피하려고 루트의 검사가 validator를 주입받지 않고 직접 생성한다.
|
||
- missing-verification: 현재 리비전에서 재발하지 않는지 `ConditionEvaluationReport`로 확인하지 않았다
|
||
- relations: `concept:when-conditions-are-evaluated` · `open-question:conditional-evaluation-order-unverified`
|
||
|
||
### CASE — 넓은 스캔을 좁히자 여덟 컴포넌트에 아무것도 도달하지 않았다
|
||
- slug: `narrowing-the-scan-orphaned-eight-components`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §14.2
|
||
- code: `.../persistence-jpa/.../config/JpaAdapterComponentsConfig.java`의 javadoc
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: 합성 루트의 스캔이 persistence 트리를 정규식으로 제외했고 **그 제외는 옳다** — 그것이 optional capability를 optional하게 만든다. 빠진 것은 나머지 절반이다. 이 leaf의 여덟 클래스가 scanned component로 쓰여 있는데(`SpringTransactionPort`·`PersistenceExceptionTranslator`·`StandardSqlStateErrorMapping`·`DomainContextAuditContextPort`·idempotency store와 reaper·outbox store와 reaper) 넓은 스캔이 멈추자 **아무것도 도달하지 않았다.** 특히 `TransactionPort`는 구현이 전혀 없어서 트랜잭션을 여는 모든 유스케이스가 열 포트를 갖지 못했고, 단위 테스트는 각 클래스를 직접 생성하므로 볼 수 있는 것이 없었다.
|
||
- missing-verification: 없음 — 수정된 `@ComponentScan` 대상 6개를 코드로 확인했다
|
||
- relations: `concept:three-assembly-paths` · `reference:read-the-assembling-side-first` · `reference:off-must-be-structural`
|
||
|
||
### CASE — 시작 검증기 13개 규칙이 유일한 조립 지점에서 호출되지 않는다
|
||
- slug: `thirteen-startup-rules-never-run`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#5-3` · `analysis/20` §3.1
|
||
- code: `.../grpc-spring-boot-starter/.../GrpcPlatformStartupValidator.java`(188줄) · `.../GrpcPlatformAutoConfiguration.java`(106줄, `@Bean` 9)
|
||
- evidence: `evidence/raw/267-grpc-family-reachability.txt`
|
||
- classification: validator가 5개 그룹 13개 규칙을 갖고(transport·security 4 / executor 2 / methods 4 / channels 2 / advanced isolation 1) javadoc이 그 13개를 고른 기준을 "None of them fails a smoke test"로 적는다. 유일한 조립 지점인 자동설정은 `@Bean` 9개를 만들면서 이 validator를 부르지 않고, static 메서드라 빈이 될 수도 없다. CLAUDE.md가 인용한 "streaming method가 Stable catalog에 등록되면 startup을 거부한다"와 §2.2의 runtime 강제가 둘 다 이 validator를 통해서만 성립하므로 둘 다 실행되지 않는다.
|
||
- missing-verification: build-only 가족이라 부팅 확인이 불가능하다 — 채택 시점에만 관측 가능
|
||
- relations: `reference:the-startup-validator-follows-the-autoconfiguration-root` · `reference:a-bean-is-not-composition-evidence` · `case:a-release-gate-with-no-evidence-producer`
|
||
|
||
### CASE — subsystem 전체가 미배선인데 그것을 켜는 flag는 startup 검사를 수행한다
|
||
- slug: `a-flag-that-validates-an-unwired-subsystem`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-2` · `analysis/06` §49
|
||
- code: `.../persistence-mongo/.../MongoRootAutoConfiguration.java` 및 그 조건
|
||
- evidence: 없음 — 도달성 전수 확인
|
||
- classification: mongo leaf(497 파일 / main 351)가 배선돼 있지 않은데, 그것을 켜는 flag는 startup 검사를 수행한다. 즉 운영자가 스위치를 켜면 검사가 돌고 통과하지만 그 뒤에 아무 능력도 조립되지 않는다 — "켰고 검사도 통과했다"가 "작동한다"로 읽히는 조합이다.
|
||
- missing-verification: 부팅으로 빈 목록을 확인하지 않았다
|
||
- relations: `reference:off-must-be-structural` · `reference:a-bean-is-not-composition-evidence` · `case:mongo-default-throws-on-first-write`
|
||
|
||
### REFERENCE — `@Bean`이 있다는 것은 조립 증거가 아니다
|
||
- slug: `a-bean-is-not-composition-evidence`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 2·3 · `analysis/99-cross-scope.md` §3.1
|
||
- classification: 타입이 로드된다는 것은 jar가 클래스패스에 있다는 뜻이고, `@Bean`이 있다는 것은 그 값이 컨텍스트에 있다는 뜻이다. 둘 다 그것이 **실행 경로에 놓였다**는 뜻이 아니다. 기준은 "그 빈을 주입받아 호출하는 코드가 있는가"다.
|
||
- scope: 프레임워크가 조립하는 모든 통제 — 검증기·인터셉터·필터·어드바이스·정책 객체. 판정은 선언 파일을 제외한 main 참조 수를 세는 것이다.
|
||
- exceptions: build-only leaf의 **공개 API 표면**은 예외다(채택자가 부를 타입). 구분 기준은 "가족 내부의 다른 코드가 불러야 하는 조립·기계 타입인가". `@ConfigurationProperties`처럼 프레임워크가 소비하는 타입도 참조 0이 정상이다.
|
||
- relations: `case:scan-exclusion-without-an-owner` · `case:observation-downgraded-by-the-composition` · `case:thirteen-startup-rules-never-run` · `reference:conditionalonbean-must-be-satisfiable`
|
||
|
||
### REFERENCE — `@ConditionalOnBean`은 조건이 만족될 수 있는지까지 확인해야 한다
|
||
- slug: `conditionalonbean-must-be-satisfiable`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 4 · `analysis/19` §7.1
|
||
- classification: 조건이 만족되지 않으면 조용히 아무것도 만들어지지 않고 프레임워크는 이것을 오류로 보고하지 않는다. 기준은 두 질문이다 — "X의 구현이 저장소 안에 있는가", "그것을 만드는 자동설정이 있는가". 사슬이면 뿌리까지 따라간다.
|
||
- scope: 조건부 조립을 쓰는 모든 지점. 평가 시점 문제와 뿌리 부재 문제가 같은 증상을 내므로 `ConditionEvaluationReport`가 둘을 가른다.
|
||
- exceptions: 애플리케이션이 직접 등록하기를 기대하는 확장점이면 정당하다. 다만 그때는 저장소가 그 port의 구현을 제공하지 않거나, 제공한다면 `@ConditionalOnMissingBean` 기본값으로 함께 등록하는 것이 일관된다.
|
||
- relations: `case:outbox-chain-behind-an-unsatisfiable-condition` · `concept:when-conditions-are-evaluated` · `open-question:conditional-evaluation-order-unverified`
|
||
|
||
### REFERENCE — 시작 검증기가 도는지는 그 능력에 자동설정 루트가 있는지와 일치한다
|
||
- slug: `the-startup-validator-follows-the-autoconfiguration-root`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#5-3` · `analysis/18` (SRC-158)
|
||
- classification: 루트가 있으면 검증기를 매달 자리가 있고, 없으면 검증기는 컴포넌트 스캔이 닿기를 기대하는데 그 스캔이 그 패키지를 제외하고 있을 수 있다. 기준은 "이 검증기를 부르는 조립 지점이 어디인가"를 능력 단위로 묻는 것이다.
|
||
- scope: 자동설정과 컴포넌트 스캔을 함께 쓰는 조합. 확인 결과: app-bootstrap 12종 배선(고아 0), messaging 6종 배선, web·websocket 미배선.
|
||
- exceptions: **루트가 있는데도 부르지 않는 경우**가 둘 있다(`WebPlatformStartupValidator`, `GrpcPlatformStartupValidator`). 필요조건이지 충분조건이 아니다.
|
||
- relations: `case:thirteen-startup-rules-never-run` · `case:scan-exclusion-without-an-owner` · `reference:a-bean-is-not-composition-evidence`
|
||
|
||
### REFERENCE — 조립 결함을 판정하려면 조립하는 쪽을 먼저 읽어야 한다
|
||
- slug: `read-the-assembling-side-first`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#13` 항목 5 · `analysis/99-cross-scope.md` §5
|
||
- classification: 통제가 작동하지 않는다는 판정은 그 통제의 코드만 읽어서는 내릴 수 없다. 조립하는 쪽을 먼저 읽어야 원인이 "루트 없음" / "루트는 있는데 소유하지 않음" / "조건 만족 불가"로 갈리고, 세 경우의 조치가 다르다.
|
||
- scope: 프레임워크 조립을 쓰는 모든 분석. 순서는 (1) 레지스트리의 runtime membership, (2) `.imports`·`spring.factories` 전량, (3) 합성 루트의 스캔 경계와 import filter, (4) 그 다음 개별 통제.
|
||
- exceptions: 없음. 이 분석의 정정 네 건이 전부 이 순서를 어겨서 생겼다.
|
||
- relations: `concept:three-assembly-paths` · `case:scan-exclusion-without-an-owner` · `reference:a-bean-is-not-composition-evidence`
|
||
|
||
### REFERENCE — "꺼짐"은 조건의 반복이 아니라 구조여야 한다
|
||
- slug: `off-must-be-structural`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#10-3` · `analysis/05` §14.1 · `analysis/19` §6.1
|
||
- classification: 능력이 꺼졌다는 것은 빈이 없고, 소켓·풀·스레드가 없고, 설정이 바인딩되지 않고, 스키마 기대치도 없다는 뜻이어야 한다. 조건을 빈마다 반복하면 다음 달에 추가된 빈이 게이트 밖에 남는다. 기준은 "조건이 몇 곳에 적혀 있는가"이고 답이 하나여야 한다.
|
||
- scope: optional capability를 갖는 모든 조립. 구현 형태는 루트 하나가 조건을 갖고 자식을 `@Import`하는 것이다.
|
||
- exceptions: 기본 켜짐인 능력(`matchIfMissing=true`)은 이 모델에 그대로 들어가지 않는다 — web의 네 스위치가 그 경우이고, 그것을 활성화 모델에 넣으려면 조립 소유권을 먼저 정해야 한다.
|
||
- relations: `case:narrowing-the-scan-orphaned-eight-components` · `case:a-flag-that-validates-an-unwired-subsystem` · `decision:one-root-owns-the-master-switch`
|
||
|
||
### REFERENCE — 프레임워크가 기여하는 자동설정까지 세지 않으면 스위치가 아니다
|
||
- slug: `count-the-frameworks-own-autoconfigurations`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §14.1
|
||
- classification: 프로젝트 자신의 설정만 조건화하는 것으로는 부족하다 — starter가 프레임워크의 import metadata를 통해 자기 것을 기여하므로, 평범한 `@EnableAutoConfiguration` 애플리케이션은 프로젝트 조건이 무엇이라 하든 풀을 열고 마이그레이션을 돌린다. 기준은 "이 능력이 꺼졌을 때 프레임워크가 여전히 무엇을 만드는가"다.
|
||
- scope: starter를 클래스패스에 두는 모든 optional capability. 이 저장소의 형태는 `AutoConfigurationImportFilter`가 10종을 막는 것이다.
|
||
- exceptions: 필터 목록의 오타는 **조용히 fail-open**한다(매치하지 않을 뿐). 그래서 테스트가 필터의 반환값이 아니라 **빈 부재**로 assert해야 한다.
|
||
- relations: `case:narrowing-the-scan-orphaned-eight-components` · `reference:off-must-be-structural` · `decision:pool-need-is-a-capability-question`
|
||
|
||
### OPEN QUESTION — 부팅 한 번으로 확증 가능한 두 건이 아직 정적 추론으로만 남아 있다
|
||
- slug: `one-boot-would-settle-two-findings`
|
||
- readiness: `OPEN`
|
||
- source: `final/document.md#11` 항목 3 · `analysis/19` §5.1, §7.1 · `analysis/99-cross-scope.md` §6
|
||
- known: `messagingPublisher`가 6인자 생성자를 호출하고 그것이 `NO_OBSERVATION`을 넘긴다 · 방출자 넷의 main 참조 0 · 두 repository 구현의 main 참조 0
|
||
- unknown: `/actuator/metrics`에 `messaging.*` 시리즈가 실제로 없는지 · `ConditionEvaluationReport`가 outbox 사슬 미충족을 어떤 사유로 보고하는지
|
||
- next-verification: `app.messaging.enabled=true`, `app.messaging.broker=kafka`, `debug=true`로 한 번 부팅해 (1) `/actuator/metrics` 시리즈 목록, (2) 시작 로그의 `Negative matches` 중 `MessagingReliabilityAutoConfiguration` 항목을 캡처. 브로커가 없어도 둘 다 가능하다
|
||
- decision-criterion: 두 관측이 정적 판정과 일치하면 두 Case의 `missing-verification`을 지우고 닫는다. 어긋나면 모듈 분석을 먼저 갱신한다
|
||
- relations: `case:observation-downgraded-by-the-composition` · `case:outbox-chain-behind-an-unsatisfiable-condition`
|
||
|
||
### OPEN QUESTION — `@ConditionalOnBean` 사슬의 실제 평가 순서를 확인하지 않았다
|
||
- slug: `conditional-evaluation-order-unverified`
|
||
- readiness: `OPEN`
|
||
- source: `final/document.md#11` 항목 4 · `analysis/05` §14.4 · `analysis/99-cross-scope.md` §6
|
||
- known: 이 저장소에 그 함정의 실제 사례가 기록돼 있다(파싱 시점 평가로 여덟 빈 소실) · 회피를 위해 루트가 validator를 직접 생성한다 · **사이클 2 보강(`EVD-335`)**: outbox 사슬의 경우 `JdbcOutboxRepository` 에 스프링 스테레오타입이 없고 `new` 하는 main 코드도 0이며 `OutboxEnvelopeFactory` 를 만드는 `@Bean` 은 test 하나뿐이므로, 정적으로는 뿌리 부재가 거의 확실하다. 다만 런타임 사유 문자열로 확인한 것은 아니다
|
||
- unknown: 현재 리비전의 조건부 빈들이 각각 어느 시점에 평가되는지 · outbox 사슬이 "뿌리 부재"인지 "평가 시점"인지 — 정적으로는 전자로 보이지만 둘은 같은 증상을 낸다
|
||
- next-verification: `debug=true` 부팅의 `Negative matches` 사유 문자열을 읽는다. `OnBeanCondition`이 남기는 사유가 뿌리 타입 부재를 지목하는지가 갈림점이다
|
||
- decision-criterion: 사유가 뿌리 부재를 지목하면 해당 Case의 원인 분류가 확정된다. 평가 시점 문제면 분류와 조치가 바뀐다
|
||
- relations: `case:outbox-chain-behind-an-unsatisfiable-condition` · `case:conditionalonbean-evaluated-at-parse-time` · `concept:when-conditions-are-evaluated` · `open-question:one-boot-would-settle-two-findings`
|
||
|
||
### DECISION — 마스터 스위치는 루트 하나가 소유하고 자식 설정은 조건을 갖지 않는다
|
||
- slug: `one-root-owns-the-master-switch`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-3` · `analysis/19` §6.1 · `analysis/05` §14.1
|
||
- decision-evidence: `.../MessagingPlatformRootAutoConfiguration.java`의 조건과 javadoc · `.../PersistenceJpaRootAutoConfiguration.java` + `JpaOffAutoConfigurationImportFilter` · `.../GrpcPlatformAutoConfiguration.java`(`matchIfMissing=false`)
|
||
- grounds: `reference:off-must-be-structural` · `reference:count-the-frameworks-own-autoconfigurations`
|
||
- classification: 다섯 어댑터가 같은 형태로 교정된 결정이고 근거가 코드에 있다 — "one root owning the condition, importing children that carry none, so a bean added to any child next month is gated without anyone remembering to repeat a condition."
|
||
- relations: `case:scan-exclusion-without-an-owner` · `reference:off-must-be-structural` · `decision:pool-need-is-a-capability-question`
|
||
|
||
### DECISION — 풀이 필요한지는 "JPA가 켜졌나"가 아니라 "커넥션이 필요한 capability가 있나"로 묻는다
|
||
- slug: `pool-need-is-a-capability-question`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-3` · `analysis/05` §14.1
|
||
- decision-evidence: `.../app-bootstrap/.../persistencejpa/DataSourceRequirement.java`의 `reasons(environment)` 6개 조건과 javadoc
|
||
- grounds: `reference:count-the-frameworks-own-autoconfigurations` · `reference:off-must-be-structural`
|
||
- classification: 풀은 JPA의 사유물이 아니다 — outbox·JDBC idempotency store·multi-instance lock·notification store·Fileserver 트랜잭션 경로가 전부 필요로 한다. "JPA 꺼졌나?"만 물으면 아무도 안 쓰는 풀을 열거나 정당하게 쓰던 capability를 조용히 망가뜨린다. 그래서 여섯 조건의 OR로 판정한다.
|
||
- relations: `decision:one-root-owns-the-master-switch` · `reference:count-the-frameworks-own-autoconfigurations`
|
||
|
||
### CASE — 검증기가 운영에 TLS를 요구하고, 실제로 조립되는 생산자에는 그 설정이 없다
|
||
- slug: `a-validator-that-demands-tls-and-an-assembly-that-omits-it`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md` §17.1 · `final/document.md#14-2`
|
||
- code: `.../messaging-kafka/.../KafkaProfileValidator.java` · `.../messaging-spring-boot-starter/.../KafkaProducerFactory`(생산자 조립부) · `.../messaging-security/.../KafkaSecurityConfigurer.java`
|
||
- evidence: 없음 — 조립 코드와 `KafkaSecurityConfigurer` 호출자 전수 대조로 판정했다
|
||
- classification: `KafkaProfileValidator` 가 운영 프로파일이 전송 보안 없이 뜨는 것을 거부하고, 테스트 `aProductionKafkaBrokerWithoutTransportSecurityFailsStartup` 이 그 거부를 지킨다. 그런데 실제로 만들어지는 `KafkaProducer` 의 설정 맵에는 `bootstrap.servers`·직렬화기 둘·`acks=all`·`enable.idempotence` 만 있고 `security.protocol` 이 없다 — Kafka 기본값은 `PLAINTEXT` 다. `security.protocol`·`ssl.enabled.protocols`·`ssl.endpoint.identification.algorithm`·`sasl.mechanism`·`sasl.jaas.config` 를 만드는 `KafkaSecurityConfigurer` 는 저장소 전역에서 production 호출자가 0 이다. 즉 검증기는 "TLS 있음" 선언을 요구하고 통과시키며, 연결은 평문으로 열린다. 이 리프는 `app-bootstrap` 소속이므로 미배선 블록 안쪽 이야기가 아니다.
|
||
- missing-verification: 브로커를 띄워 실제 핸드셰이크를 관측하지 않았다 — 조립되는 설정 맵의 키 집합으로 판정했다
|
||
- relations: `reference:a-validator-is-enforced-by-injection` · `reference:read-the-assembling-side-first` · `reference:off-must-be-structural`
|
||
|
||
---
|
||
|
||
## TOPIC 3 — what-a-gate-does-not-prove
|
||
|
||
### CONCEPT — strict test lane — 발견하지 못하면 실패하는 레인
|
||
- slug: `strict-test-lane`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#6-3` · `analysis/05` §15.1 · `analysis/19` §2.3 · `analysis/20` §2.5
|
||
- code: `src/build-logic/src/main/groovy/ca.strict-test-lane.gradle` · 각 leaf의 `strictTestLanes { lane(...) }`
|
||
- classification: 이 저장소가 "레인"을 정의하는 방식의 설명이다. `failOnNoDiscoveredTests = true`(발견 0은 성공이 아니라 실패), `outputs.upToDateWhen { false }`(이전 실행 결과를 서빙하지 않음), 태그 기반 선택, 그리고 태그 필터가 discovery **이후**에 적용되므로 `failOnNoDiscoveredTests`만으로는 부족해 executed-selector 검사가 따로 있다는 것까지. Docker 부재를 skip이 아니라 에러로 두는 결정과, 반대로 랩톱 빌드를 깨지 않으려 `test`에서 태그를 제외하는 결정이 어떻게 공존하는지 설명한다.
|
||
- missing-verification: 플러그인이 `check`에 레인을 연결하지 않는다는 것은 확인했으나, TestKit 픽스처가 검증하는 범위는 읽지 않았다
|
||
- relations: `reference:a-lane-that-discovers-nothing-must-fail` · `reference:a-gate-nobody-runs-reports-the-last-run` · `decision:only-the-certification-lane-carries-no-docker-guard`
|
||
|
||
### CONCEPT — 증거 등급과 provenance — R1과 R2를 가르는 것
|
||
- slug: `evidence-grades-and-provenance`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#6-5` · `analysis/05` §15.6
|
||
- code: `gradle/jpa-evidence.gradle`(917줄) · `config/jpa/readiness-cards.yaml`(718줄)
|
||
- classification: 증거가 "돌았다"에서 "믿을 수 있다"로 올라가는 조건의 설명이다. 후보 검증(R1)은 JUnit XML에서 exact selector와 executed/skipped/failure 수를 읽고 canonical JSON SHA-256으로 manifest를 만들지만, candidate profile·dirty source·아직 R2가 아닌 prerequisite를 `readinessBlockers`에 **보존**해서 통과해도 R1에 머문다. R2는 clean revision + CI provenance(`JPA_EVIDENCE_CI_JOB`, artifact location) + immutable PostgreSQL image digest + prerequisite DAG를 요구한다. 로컬 dirty worktree에서 `worktree-is-dirty`로 실패하는 것이 정식 동작이다.
|
||
- missing-verification: 두 프로파일 모두 실행하지 않았다 — 컨테이너 필요
|
||
- relations: `decision:candidate-evidence-stays-at-r1` · `reference:agreement-between-documents-proves-nothing` · `open-question:container-lanes-not-executed`
|
||
|
||
### CASE — "certified"라 불리던 레인이 threshold를 하나도 비교하지 않고 있었다
|
||
- slug: `a-certifying-lane-that-compared-nothing`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#6-6` · `analysis/05` §15.3
|
||
- code: `.../persistence-jpa/build.gradle` (`jpaPlatformPoolContractTest` 등록부 주석)
|
||
- evidence: 없음 — build.gradle 주석이 자기 이력을 기록한다
|
||
- classification: 레인 이름이 `jpaPlatformPerformanceTest`였고 "certify"한다고 기술됐으며 나타나는 모든 곳에서 off가 기본인 boolean 뒤에 있었다 — 이 파일에서도, 명시적으로 off로 설정한 nightly workflow에서도. 릴리스 게이트가 "유일한 threshold assertion이 'threshold를 assert하지 않고 있다'인 레인"에 의존했다. 수정은 이름을 약속하지 않는 것으로 바꾸고 프로퍼티를 제거하는 것이었고, 주석이 그 이름을 반복하지 않는 이유까지 적는다.
|
||
- missing-verification: 현재 레인을 실행하지 않았다(실 PostgreSQL 필요)
|
||
- relations: `concept:strict-test-lane` · `reference:a-gate-nobody-runs-reports-the-last-run` · `decision:performance-measurement-is-not-a-release-gate`
|
||
|
||
### CASE — 문서 계약 테스트의 단언 경계 밖에 발견된 드리프트 세 건이 전부 있었다
|
||
- slug: `doc-contract-test-boundary-predicted-the-drift`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-4` · `analysis/19` §9.3
|
||
- code: `.../messaging-testkit/src/test/.../MessagingDocumentationContractTest.java` · `docs/messaging/support-matrix.md`
|
||
- evidence: `evidence/raw/261-messaging-documentation-contract-test-coverage.txt` · `evidence/raw/255-messaging-capability-doc-vs-code-drift.txt`
|
||
- classification: doc rot를 막기 위해 존재하는 계약 테스트의 단언 여덟 개가 붙드는 것(등급 이름·Kafka 버전 문자열·존재하지 않는 두 enum 상수)은 전부 정확하고, 붙들지 않는 것(capability 표 60칸·runtime membership 문장·브로커 등급표의 "제한" 칸)에 드리프트 세 건이 전부 있다. 테스트 javadoc은 좁은 단언을 고른 이유까지 옳게 적는다. 결함은 좁게 고른 것이 아니라 그 경계가 어디에도 적혀 있지 않다는 점이다.
|
||
- missing-verification: 없음 — 단언 목록과 문서를 전수 대조했다
|
||
- relations: `reference:agreement-between-documents-proves-nothing` · `case:support-matrix-said-the-opposite-of-the-code` · `decision:widen-doc-contract-assertions`
|
||
|
||
### CASE — 릴리스 게이트가 읽는 증거를 아무도 생산하지 않는다
|
||
- slug: `a-release-gate-with-no-evidence-producer`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-6` · `analysis/20` §3.2
|
||
- code: `.../grpc-testkit/.../release/GrpcStableReleaseGate.java` · `.../GrpcReleaseEvidence.java` · `docs/compatibility/grpc-support-matrix.md`
|
||
- evidence: `evidence/raw/tl-grpc-release-gate-no-producer.txt` · `evidence/meta/…json` · `evidence/terminal/…svg`
|
||
- classification: 지원 매트릭스가 "게이트가 릴리스를 막는다"고 현재 시제로 적는다. 게이트 설계는 정교하고 문서 부재를 차단 사유로 두는 근거까지 적지만, `GrpcReleaseEvidence`의 다섯 성분이 전부 호출자가 넘기는 값이고 `runbookPresent`는 파일 시스템을 보지 않으며 유일한 생성 지점 넷이 자기 단위 테스트다. Gradle 태스크 0, CI 워크플로 0/28.
|
||
- missing-verification: 없음 — 생성 지점·태스크·워크플로를 전수 확인했다
|
||
- relations: `reference:a-gate-nobody-runs-reports-the-last-run` · `case:the-second-platform-carried-the-design-not-the-wiring` · `concept:evidence-grades-and-provenance`
|
||
|
||
### CASE — 두 파일이 같은 검증기를 "빌드를 실패시키는 것"이라 적고, 어떤 빌드도 그것을 부르지 않는다
|
||
- slug: `two-files-name-a-build-gate-that-no-build-runs`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-proto-contract.md` §17.4 · `analysis/grpc/grpc-codegen.md` §17.1
|
||
- code: `.../grpc-proto-contract/.../GrpcProtoContractValidator.java` · `.../grpc-proto-contract/src/main/resources/proto/buf.yaml:3-5` · `.../grpc-codegen/.../GrpcBufPolicy.java:8-10`
|
||
- evidence: 없음 — `*.gradle`·`*.kts`·`*.yml` 과 자바 타입 이름 전수 grep 으로 판정했다
|
||
- classification: 두 파일이 같은 논증을 편다 — Buf CLI 가 이 툴체인에 없으므로 자바로 구현한 규칙 엔진이 그 자리를 대신하고, "그것이 실제로 이 저장소의 빌드를 실패시킨다". `buf.yaml` 주석과 `GrpcBufPolicy` javadoc 이 각각 그 문장을 갖는다. 그런데 `GrpcProtoContractValidator` 를 부르는 Gradle 태스크도, 검증 훅도, 다른 모듈의 호출도 없다. 실제로 아홉 규칙을 실행하는 것은 그 리프의 단위 테스트 하나이고, 그 테스트가 판정하는 대상은 **하드코딩된 두 파일**이다. 거꾸로 `GrpcBufPolicy` 가 계약이라고 든 네 태스크 이름(`bufFormatCheck`·`bufLint`·`bufBuild`·`bufBreaking`)도 어떤 빌드 파일에도 없고, 그 javadoc 은 "a missing stage is a test failure rather than a stage nobody noticed was gone" 라고 적는데 테스트는 그 목록을 리터럴 및 자기 자신과 비교한다. 두 쪽이 서로를 게이트라고 가리키고 어느 쪽도 실행되지 않는다.
|
||
- missing-verification: 리플렉션이나 서비스 로더로 부르는 형태는 배제하지 못했다 — 이름 기반 grep 으로만 확인했다
|
||
- relations: `reference:a-gate-declared-in-prose-is-not-in-the-build` · `reference:a-gate-nobody-runs-reports-the-last-run` · `case:a-release-gate-with-no-evidence-producer` · `case:a-build-gate-that-is-not-in-the-build`
|
||
|
||
### CASE — 이름이 검사한다고 말하는 것을 본문이 검사하지 않는 테스트 다섯
|
||
- slug: `test-names-that-assert-what-their-bodies-do-not`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-pulsar-experimental.md` §17.3 · `analysis/messaging/messaging-nats-experimental.md` §17.4 · `analysis/grpc/grpc-advanced-bootstrap.md` §17.4
|
||
- code: `.../PulsarSubscriptionGuardTest.theValidatorAcceptsAKeyedProfileOnKeyShared` · `.../PulsarAdapterContractTest.aTransportWithoutAConsumerFactoryRefusesToRegister…` · `.../NatsAdapterContractTest.theReportedElapsedTimeIsMeasuredRatherThanZero` · `.../GrpcAdvancedPromotionGateTest.theStableDefaultThresholdIsHigher`
|
||
- evidence: 없음 — 테스트 본문과 이름·`as()` 메시지 대조로 판정했다
|
||
- classification: 네 형태가 같은 결과를 낳는다. (1) 이름이 `theValidatorAccepts…` 인데 본문에 검증기가 없다 — `PulsarProfile` 생성자만 부른다. 그 리프에서 검증기를 언급하는 유일한 테스트 이름이 이것이라, 이름만 읽으면 커버리지가 있다고 읽힌다. (2) 이름이 "소비자 팩토리 없이 만든 전송이 등록을 거절한다" 인데 본문은 `register(null)` 을 불러 첫 줄의 널 검사에 걸린다 — 겨냥한 `PULSAR_CONSUMER_NOT_CONFIGURED` 는 한 번도 실행되지 않는다. (3) `as()` 가 "모든 결과가 `Duration.ZERO` 였다" 는 회귀를 막는다고 적는데 단언이 `isGreaterThanOrEqualTo(Duration.ZERO)` 라 `Duration.ZERO` 도 통과한다 — 구현이 무엇을 하든 참이다. (4) 이름이 "Stable default 가 되는 데 더 긴 담금이 필요하다" 인데 실제로 평가하는 전이는 `ADVANCED_STABLE → DISABLED`(철회)다. 어느 것도 잘못된 동작을 통과시키지는 않는다 — 틀리는 것은 커버리지 지도이고, 그래서 그 아래의 진짜 공백이 오래 눈에 띄지 않았다.
|
||
- missing-verification: 테스트를 실행하지 않았다 — 단언 의미론과 호출 경로로 판정했다
|
||
- relations: `reference:omission-that-passes-is-not-a-gate` · `case:a-test-that-passed-on-the-wrong-guard` · `reference:a-gate-declared-in-prose-is-not-in-the-build`
|
||
|
||
### CASE — 패키지 카탈로그가 트리보다 아홉 개 적어서 사이클이 통과했다
|
||
- slug: `a-catalog-nine-entries-short`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §1
|
||
- code: `.../persistence-jpa/src/test/.../JpaModuleBoundaryTest.java`
|
||
- evidence: 없음 — 테스트 javadoc의 사후 기록
|
||
- classification: 경계 테스트가 production root의 직계 자식 패키지 닫힌 카탈로그를 들고 실제 트리와 비교하는데, 카탈로그에 13개가 적혀 있고 트리에는 22개가 있었다. 그래서 아홉 패키지가 **아무 규칙의 지배도 받지 않았고** `transaction → postgresql` / `postgresql → transaction` 사이클이 통과했다. 수정은 "카탈로그와 트리의 **정확한 동등성**" 검사를 추가하는 것이었다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:omission-that-passes-is-not-a-gate` · `concept:strict-test-lane`
|
||
|
||
### CASE — 릴리스 레인이 매트릭스 세 버전 중 첫 번째만 돌리고 세 개를 커버로 기록했다
|
||
- slug: `three-versions-declared-one-executed`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §15.4
|
||
- code: `config/jpa/release-registry.json` · `.../JpaPlatformContractSupport.start()` · `.github/workflows/jpa-release.yml`
|
||
- evidence: 없음 — 레지스트리 `_comment`와 javadoc의 사후 기록
|
||
- classification: 릴리스 레인이 `-Pjpa.matrix.versions=16,17,18`을 `selectedVersions().get(0)`을 쓰는 지원 클래스에 넘겼고 **통합 suite 전체가 PostgreSQL 16에 대해 돌았으며**, 지원 표는 3개 assertion짜리 smoke test의 힘으로 17과 18을 완전 커버로 기록했다. 수정은 `start()`가 다중 선택을 아예 거부하고, workflow가 major당 job으로 fan-out하며, promotion job이 세 major의 증거가 **같은 commit SHA**를 담기를 요구하는 것이다.
|
||
- missing-verification: 현재 릴리스 워크플로를 실행하지 않았다
|
||
- relations: `reference:agreement-between-documents-proves-nothing` · `concept:evidence-grades-and-provenance` · `decision:capability-grade-is-declared-not-inferred`
|
||
|
||
### CASE — 다중 타깃 검증을 확인한다는 테스트가 다른 가드에 걸려 통과했다
|
||
- slug: `a-test-that-passed-on-the-wrong-guard`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#3-5` · `analysis/13` §9
|
||
- code: `.../notification/.../NotificationBindingCompiler.java:141,159-184`
|
||
- evidence: 없음 — 코드 경로와 테스트 단언 비교
|
||
- classification: `SINGLE` 전용 가드가 먼저 던져 다중 타깃 검증 전체(순환 탐지 포함)가 도달 불가인데, 그것을 검증한다는 테스트가 `hasMessageContaining("strategy")`로 단언한다. 그 문자열은 **먼저 던지는 다른 가드**의 메시지에도 있어서 테스트는 통과하고, 검증하려던 코드는 한 번도 실행되지 않는다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:omission-that-passes-is-not-a-gate` · `reference:a-contract-test-must-run-the-adapters-statement`
|
||
|
||
### REFERENCE — 빠뜨림이 통과가 되는 게이트는 게이트가 아니다
|
||
- slug: `omission-that-passes-is-not-a-gate`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 12 · `analysis/05` §1, §16
|
||
- classification: 목록·카탈로그·selector로 대상을 고르는 게이트는 새 항목 추가를 잊었을 때 초록불이 된다. 기준은 "대상 집합을 어떻게 정하는가"이고, 답이 "손으로 유지하는 목록"이면 그 목록과 실제 트리의 정확한 동등성을 별도로 검사해야 한다.
|
||
- scope: 아키텍처 규칙·entity scan·문서 수치 진술·테스트 레인 selector. 구현 예: `JpaModuleBoundaryTest` 동등성 · `PersistenceEntityScanCoverageTest` · `failOnNoDiscoveredTests` · `verifyDocumentedLeafCount`
|
||
- exceptions: 대상 집합을 **파생**하는 게이트에는 필요 없다(`CertifiedEvidence.knownGaps`가 `all() − covered`). 파생이 가능하면 목록을 두지 않는 것이 항상 낫다. 그리고 walk 범위 자체가 목록이면 그 범위가 같은 문제를 갖는다.
|
||
- relations: `case:a-catalog-nine-entries-short` · `case:a-build-gate-that-is-not-in-the-build` · `reference:numbers-in-docs-should-be-derived`
|
||
|
||
### REFERENCE — 문서와 상수가 서로 일치하는 것으로는 아무것도 증명되지 않는다
|
||
- slug: `agreement-between-documents-proves-nothing`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 5 · `analysis/19` §6.6
|
||
- classification: 지원 문서와 코드 상수를 대조하는 검사는 둘이 같이 틀렸을 때 통과한다. 기준은 "이 값이 실행된 무언가에서 왔는가"다. 원문: "the matrix and the support document agreed with each other and with nothing that had executed."
|
||
- scope: 지원 매트릭스·capability 선언·버전 표·커버리지 표. 파생 형태의 예: `hasLiveBrokerCertification()`이 레인 증거에서 계산되고, 증거는 레인이 쓴 manifest이며, Gradle이 양방향 대조하고 CI가 돌린다.
|
||
- exceptions: 실행이 불가능하거나 지나치게 비싼 주장은 파생할 수 없다. 그때는 **부재를 명시적으로 이름 붙이는 것**이 대안이다(`knownGaps`).
|
||
- relations: `case:doc-contract-test-boundary-predicted-the-drift` · `case:three-versions-declared-one-executed` · `case:a-release-gate-with-no-evidence-producer`
|
||
|
||
### REFERENCE — 아무도 돌리지 않는 레인의 게이트는 마지막으로 돌린 사람이 본 것을 보고한다
|
||
- slug: `a-gate-nobody-runs-reports-the-last-run`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 6 · `analysis/18` §4.1c · `analysis/20` §3.3
|
||
- classification: 게이트의 신뢰도는 단언이 아니라 도는 경로가 정한다. 두 질문 — "`check`에 연결돼 있는가", "이것을 이름으로 부르는 CI 워크플로가 있는가". 둘 다 아니면 상태는 마지막으로 명령을 입력한 사람이 본 것이다.
|
||
- scope: 별도 레인·소스셋·태그로 분리된 모든 검증. 판정은 `check` 의존 그래프와 CI YAML의 명령 문자열을 직접 읽는 것이다.
|
||
- exceptions: 의도적으로 `test`에서 뺀 레인은 그 자체로 결함이 아니다. 구분 기준은 대체 경로의 존재다 — messaging 인증 레인은 전용 워크플로가 있고, gRPC의 네 레인은 없다.
|
||
- relations: `case:a-certifying-lane-that-compared-nothing` · `case:a-release-gate-with-no-evidence-producer` · `case:a-build-gate-that-is-not-in-the-build` · `decision:only-the-certification-lane-carries-no-docker-guard`
|
||
|
||
### REFERENCE — 산문이 선언한 게이트는 빌드에 있는 게이트가 아니다
|
||
- slug: `a-gate-declared-in-prose-is-not-in-the-build`
|
||
- readiness: `READY`
|
||
- source: `analysis/99-cross-scope.md` §3.5 · `final/document.md#14-2`
|
||
- classification: `a-gate-nobody-runs-reports-the-last-run` 이 "레인이 도는가" 를 묻는다면, 이 규칙은 그 앞 단계를 묻는다 — **게이트가 존재한다고 말하는 문장이 어디에 근거하는가.** 주석·javadoc·설정 파일 헤더가 "이것이 빌드를 실패시킨다" 고 적는 것은 관찰이 아니라 의도다. 판정은 한 줄이다 — 그 심볼 이름으로 빌드 스크립트와 CI 정의를 grep 했을 때 나오는가. 나오지 않으면 그 문장은 지금 사실이 아니고, 주석 밀도가 높은 저장소일수록 비싸다 — 읽는 사람이 근거 있게 믿기 때문이다.
|
||
- scope: "fails the build" · "refuses at startup" · "is checked too" 처럼 실행을 단언하는 모든 주석. 이 저장소에서 다섯 곳이 걸렸다 — `GrpcProtoContractValidator`, `GrpcBufPolicy` 의 네 Buf 태스크, `GrpcAdvancedModuleGuard.requireStableStarterIsClean`, `NatsJetStreamProfileValidator`, `PulsarProfileValidator`.
|
||
- exceptions: 의도적으로 미래를 서술하는 선언은 이 규칙의 대상이 아니다 — 다만 그때는 시제가 달라야 한다. `GrpcCodegenManifest` 가 좋은 예다: "It does not run in this build yet — adaptation D6 records why, and this manifest is what a future decision to turn it on has to satisfy rather than replace."
|
||
- relations: `case:two-files-name-a-build-gate-that-no-build-runs` · `reference:a-gate-nobody-runs-reports-the-last-run` · `case:startup-validator-is-the-only-reader-of-four-keys` · `reference:a-validator-is-enforced-by-injection`
|
||
|
||
### REFERENCE — 계약 테스트는 어댑터가 실제로 돌리는 statement를 실행해야 한다
|
||
- slug: `a-contract-test-must-run-the-adapters-statement`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 19 · `analysis/05` §11.2, §17 P2
|
||
- classification: SQL을 테스트가 재타이핑하면 증명되는 것은 "테스트 작성자와 어댑터 작성자가 쿼리에 대해 합의했다"이고, 그것은 아무도 필요로 하지 않는 속성이다. 기준은 "이 테스트가 실행하는 statement가 production 상수에서 왔는가"다.
|
||
- scope: native SQL·스크립트·와이어 포맷을 갖는 모든 어댑터. 구현 형태는 SQL을 production 상수로 뽑고 테스트가 그 상수를 실행하는 것이다(`RecipientClaimSql`).
|
||
- exceptions: statement가 아니라 **정책 판정**을 검증하는 테스트는 재타이핑 문제가 없다. 다만 그때도 판정 대상이 production 타입이어야 한다 — 이 저장소에는 production 타입을 하나도 실행하지 않는 계약 테스트가 여섯 개 있다.
|
||
- relations: `case:a-test-that-passed-on-the-wrong-guard` · `reference:omission-that-passes-is-not-a-gate` · `case:jpa-platform-capabilities-have-no-consumer`
|
||
|
||
### REFERENCE — 아무것도 발견하지 못한 레인은 성공이 아니라 실패여야 한다
|
||
- slug: `a-lane-that-discovers-nothing-must-fail`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#6-3` · `analysis/05` §15.1
|
||
- classification: 선택된 레인이 테스트를 하나도 발견하지 못하면 기본 동작은 성공 보고다. 조용히 돌기를 멈춘 계약 suite는 통과하는 것과 구별되지 않는다. 기준은 "이 레인이 0건을 발견했을 때 무엇을 보고하는가"다.
|
||
- scope: 태그·소스셋·selector로 좁힌 모든 테스트 태스크. 두 층이 필요하다 — 발견 단계의 `failOnNoDiscoveredTests`와, 태그 필터가 discovery 이후에 적용되므로 executed-selector 검사가 추가로 필요하다.
|
||
- exceptions: 환경 부재로 skip하는 것이 정당한 레인(컨테이너 없는 랩톱)은 예외이되, **인증처럼 부재가 곧 거짓 성공이 되는 레인은 예외의 예외**다 — 그런 레인은 가드를 달지 않고 실패한다.
|
||
- relations: `concept:strict-test-lane` · `decision:only-the-certification-lane-carries-no-docker-guard`
|
||
|
||
### OPEN QUESTION — 컨테이너가 필요한 레인의 실제 결과를 실행으로 확인하지 않았다
|
||
- slug: `container-lanes-not-executed`
|
||
- readiness: `OPEN`
|
||
- source: `final/document.md#6-2` · `#11` 항목 1 · `analysis/99-cross-scope.md` §6
|
||
- known: 실행한 레인과 결과가 기록돼 있다(messaging 851 · grpc 579 · 증거 레인 7·9·9 · graphql 1,603 · websocket 720 · grpc adapter 48 · app-bootstrap 1,016 중 1 환경 실패) · 미실행 레인의 목록과 각각의 요구사항 · messaging 인증만 CI가 강제한다 · **사이클 2 보강(`EVD-334`)**: 이 컨테이너에 Docker 가 있고(client 29.1.3 / server 29.6.1) Testcontainers 기반 `test` 레인을 실제로 돌렸다 — persistence-jpa 477 · persistence-mongo 72 · cache-redis 435 · fileserver 398(1 로케일) · objectstorage 140 · notification 327 · httpclient 283(3 듀얼스택) 전부 실행됨
|
||
- unknown: JPA 의 6개 특수 레인(`jpaPlatformFailureTest` 포함)과 readiness task 15종 · websocket 커스텀 4종 · `grpcPerformanceTest` 의 실제 결과 · 그중 어느 것이 지금 빨간지. 기본 `test` 레인은 사이클 2에서 전부 돌렸으므로 남은 것은 **특수 레인과 게이트 태스크**다
|
||
- next-verification: `./gradlew jpaPlatformReleaseGate`와 `./gradlew :messaging:messaging-kafka:verifyMessagingCertificationEvidence` 두 개를 먼저 돌린다
|
||
- decision-criterion: 두 명령의 exit code와 실패 목록이 나오면 닫는다. 실패가 나오면 각각이 새 Case 후보가 되고 `a-gate-nobody-runs-reports-the-last-run`의 예측과 대조한다
|
||
- relations: `open-question:commit-ambiguity-lane-not-executed` · `open-question:redis-topology-lane-not-executed` · `open-question:v2-state-machine-lanes-not-executed` · `reference:a-gate-nobody-runs-reports-the-last-run`
|
||
|
||
### DECISION — 인증 레인만 Docker 가드를 달지 않는다
|
||
- slug: `only-the-certification-lane-carries-no-docker-guard`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#6-4` · `analysis/19` §2.3
|
||
- decision-evidence: `.../messaging-kafka/build.gradle`의 등록부 주석과 `excludeTags` · `.github/workflows/messaging-certification.yml` 헤더 주석
|
||
- grounds: `reference:a-lane-that-discovers-nothing-must-fail` · `reference:a-gate-nobody-runs-reports-the-last-run`
|
||
- classification: 모든 컨테이너 suite가 Docker 부재 시 이유를 적고 skip하는데 인증 레인만 예외다 — "a lane that skipped would report success for a broker nobody started." 대신 `test`에서 태그로 제외해 랩톱 빌드를 깨지 않고, 전용 워크플로가 레인이 아니라 **게이트**를 돌리며 게이트가 레인에 의존한다.
|
||
- relations: `case:a-release-gate-with-no-evidence-producer` · `concept:strict-test-lane`
|
||
|
||
### DECISION — 성능 측정은 릴리스 게이트에 넣지 않는다
|
||
- slug: `performance-measurement-is-not-a-release-gate`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-5` · `analysis/20` §2.5 · `analysis/05` §15.3
|
||
- decision-evidence: `.../grpc-testkit/build.gradle`의 `excludeTags 'grpc-performance'`와 주석 · `.../persistence-jpa/build.gradle`의 개명 기록
|
||
- grounds: `case:a-certifying-lane-that-compared-nothing` · `reference:a-gate-nobody-runs-reports-the-last-run`
|
||
- classification: 두 가족이 같은 결론에 다른 경로로 도달했다. gRPC는 처음부터 빼고 이유를 적었고("flaky gates get turned off"), JPA는 사고로 배웠다. 두 경우 모두 진짜 성능 게이트는 전용 러너·warmup/sample 수·기록된 baseline이 필요하고 그때는 별도 레인이어야 한다는 조건을 남겼다.
|
||
- relations: `case:a-certifying-lane-that-compared-nothing` · `open-question:container-lanes-not-executed`
|
||
|
||
### DECISION — 후보 증거는 통과해도 R1에 머무르고 R2는 별도 게이트가 판정한다
|
||
- slug: `candidate-evidence-stays-at-r1`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#6-5` · `analysis/05` §15.6
|
||
- decision-evidence: `gradle/jpa-evidence.gradle`의 두 태스크와 `readinessBlockers` 구현 · `config/jpa/readiness-cards.yaml`의 `state` 필드
|
||
- grounds: `concept:evidence-grades-and-provenance` · `reference:agreement-between-documents-proves-nothing`
|
||
- classification: 후보 검증이 zero-skip·schema·content hash·prerequisite link를 전부 만족해도 `attainedReadiness=R1`을 유지하고, 각 manifest가 candidate profile·dirty source·비R2 prerequisite를 blocker로 보존해 **후보 통과를 R2로 오인할 수 없게** 한다. R2는 clean revision과 CI provenance와 immutable image digest를 요구하며, 로컬 dirty worktree에서 실패하는 것이 정식 동작이다.
|
||
- relations: `concept:evidence-grades-and-provenance` · `decision:capability-grade-is-declared-not-inferred`
|
||
|
||
---
|
||
|
||
## TOPIC 4 — bounding-by-type
|
||
|
||
### CONCEPT — 서명된 커서의 구조와 검증 순서
|
||
- slug: `signed-cursor-structure`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#10-4` · `analysis/05` §2.4 · `analysis/19` §4.x
|
||
- code: `.../persistence-jpa/.../api/query/SignedJsonCursorCodec.java` · `.../grpc-policy/.../streaming/GrpcResumeTokenCodec.java` · `.../GrpcResumeToken.java`
|
||
- classification: 이 저장소에 서명 커서 구현이 둘 있고 같은 다섯 단계를 지킨다. (1) 길이 검사가 substring/decode/MAC **이전 첫 줄**에 온다 — 페이징 엔드포인트는 public이고 그 아래 모든 코드가 caller가 보낸 크기에 비례해 할당한다. (2) base64 확장률로 decode 후 크기를 할당 전에 bound한다. (3) MAC 길이를 먼저 확인한다 — `MessageDigest.isEqual`은 같은 길이 입력에 대해서만 상수 시간이다. (4) 상수 시간 비교. (5) **서명 검증 후에야** payload를 파싱한다. gRPC 쪽은 여기에 두 가지를 더한다 — 알 수 없는 key id를 현재 키로 폴백하지 않고 거부(폴백은 rotation을 "탈취된 키가 여전히 검증되는 창"으로 만든다), 그리고 malformed·unknown key·verify 실패를 **구별 불가능하게** 반환(구별은 probing oracle이다).
|
||
- missing-verification: 없음 — 두 구현을 코드로 확인했다
|
||
- relations: `case:cursor-verification-order` · `decision:cursors-are-signed-for-integrity` · `reference:names-are-registry-keys-not-values`
|
||
|
||
### CONCEPT — 카디널리티 경계를 타입으로 표현하기
|
||
- slug: `cardinality-bounds-as-types`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1`, `#5-2` · `analysis/05` §2.1, §12.2 · `analysis/02`
|
||
- code: `.../api/PersistenceOperationName.java` 계열 · `.../observation/JpaMetricTags.java` · `.../shared-contract/.../ForbiddenMetricTags.java` · `.../CardinalityBounds.java`
|
||
- classification: metric tag·trace·retry policy의 키가 되는 문자열을 값 타입으로 만들고 정규식을 생성자에 두는 패턴의 설명이다. `PersistenceOperationName`·`QueryName`·`ConstraintCode`·`FetchPlanName`·`WorkQueueName`·`JsonPathName`·`TenantId`가 전부 같은 모양이고, 목적은 엔티티 id·tenant id·SQL 조각·요청 스코프 값이 그 자리에 올 수 없게 하는 것이다. 검증이 registry가 아니라 생성자에 있는 이유(대시보드가 안 뜰 때까지 살아남지 않게)와 sanitize가 아니라 reject인 이유(sanitize하면 caller가 계속 넘기고 눈치채지 못한다)를 함께 다룬다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:names-are-registry-keys-not-values` · `reference:reject-rather-than-sanitize` · `decision:tenant-id-is-never-a-metric-tag`
|
||
|
||
### CASE — 시그니처가 payload를 받지 않는데 예외 메시지로 PII가 로그에 남았다
|
||
- slug: `pii-through-an-exception-message`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#8-1` · `analysis/04` §4
|
||
- code: `.../support/.../FailOpenDependencyLogger.java` (`logFailure`) · `.../app-bootstrap/.../LogMaskingPatterns.java`
|
||
- evidence: `evidence/raw/021a-support-logger-pii-probe.java` · `evidence/raw/021-support-logger-pii-probe.txt`
|
||
- classification: source와 README가 "logger method가 body/recipient/payload를 받지 않기 때문에 PII가 log에 닿지 않는다"고 주장하고 테스트도 그것을 검사하는데, fixture의 예외가 `"connection refused"`라 marker가 어떤 argument에도 없다 — 테스트는 payload object가 직접 전달되지 않는다는 것만 확인한다. PII를 담은 예외를 넣는 probe를 실행하니 formatted WARN에 그대로 남았다. 소비자 SPI 넷은 arbitrary `Exception`을 던질 수 있고, 전역 masking은 password/token 계열만 덮으며 README 스스로 "보증이 아니라 defence-in-depth"라고 적는다.
|
||
- missing-verification: 실제 provider SDK가 recipient/body를 예외 메시지에 넣는지는 확인하지 않았다. probe는 그것이 가능할 때 logger가 막지 못한다는 것만 보인다
|
||
- relations: `concept:cardinality-bounds-as-types` · `reference:telemetry-can-be-more-dangerous-than-its-subject` · `reference:reject-rather-than-sanitize`
|
||
|
||
### CASE — 진단 리포트가 살아 있는 리소스를 담지 않도록 값 타입을 좁혔다
|
||
- slug: `a-report-that-cannot-carry-a-datasource`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §2.5, §12.1
|
||
- code: `.../api/capability/CapabilitySupport.java` · `.../security/DatabasePrivilegeReport.java`
|
||
- evidence: 없음 — 값 타입 정의와 그 javadoc
|
||
- classification: `CapabilitySupport`가 provider 객체(`DataSource`·`EntityManagerFactory`·`SessionFactory`)를 절대 담지 않고, `DatabasePrivilegeReport`가 JDBC URL·패스워드·호스트를 담지 않는다. 이유가 같다 — 이 값들은 리포트로 직렬화되고 actuator로 publish될 수 있어야 하는데, 살아 있는 리소스를 값 타입에 끌고 들어가면 리포트가 자격증명을 흘린다. "publish 전에 마스킹해야 할 것은 애초에 들어가지 않는다"가 설계 문장이다.
|
||
- missing-verification: 없음
|
||
- relations: `concept:cardinality-bounds-as-types` · `reference:telemetry-can-be-more-dangerous-than-its-subject`
|
||
|
||
### CASE — 커서 서명 검증이 길이·상수시간·순서를 전부 지켜야 했던 이유
|
||
- slug: `cursor-verification-order`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §2.4
|
||
- code: `.../api/query/SignedJsonCursorCodec.java`
|
||
- evidence: 없음 — 구현과 그 javadoc
|
||
- classification: 다섯 방어가 각각 다른 공격을 막고 순서가 계약이다. `MAX_ENCODED_LENGTH(4096)` 검사가 첫 줄에 없으면 decode가 caller가 보낸 크기만큼 할당하고, MAC 길이 확인이 없으면 `MessageDigest.isEqual`의 상수 시간 보장이 깨지며, 서명 검증 전에 파싱하면 서명 없는 토큰이 애플리케이션 JSON 파서에 도달한다. 그리고 MAC이 **버전과 payload를 함께** 덮어 prefix 재작성으로 옛 포맷으로 다운그레이드하는 것을 막는다.
|
||
- missing-verification: 없음
|
||
- relations: `concept:signed-cursor-structure` · `decision:cursors-are-signed-for-integrity`
|
||
|
||
### REFERENCE — 이름은 값이 아니라 registry key다
|
||
- slug: `names-are-registry-keys-not-values`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 15 · `analysis/05` §2.1, §5, §7
|
||
- classification: 쿼리·큐·업서트·JSON path·정렬 필드처럼 파라미터로 바인딩할 수 없는 것을 caller가 문자열로 주게 하면 caller가 statement의 일부를 쓰는 것이다. 기준은 "이 값이 statement를 **선택**하는가, statement의 **일부가 되는가**"다.
|
||
- scope: SQL/JSON path/정렬/스키마 이름 등 바인딩 불가능한 위치 전부, 그리고 metric tag·trace·retry policy 키가 되는 모든 문자열.
|
||
- exceptions: 값(비교 대상·payload)은 반대다 — 바인딩할 수 있으므로 registry에 넣으면 유연성만 잃는다.
|
||
- relations: `concept:cardinality-bounds-as-types` · `reference:register-paths-bind-values` · `decision:tenant-id-is-never-a-metric-tag`
|
||
|
||
### REFERENCE — 관측을 위해 수집한 데이터가 관측 대상보다 위험할 수 있다
|
||
- slug: `telemetry-can-be-more-dangerous-than-its-subject`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#5-2` · `analysis/05` §12.2 · `analysis/02`
|
||
- classification: 메트릭 태그·로그·트레이스·예외 메시지는 원본보다 오래 살고 더 널리 퍼지는 사본이다. 기준은 "이 진단 경로에 들어가는 값의 도메인을 누가 정하는가"이고, 답이 "외부 라이브러리" 또는 "요청 내용"이면 그 경로는 개인정보 경계다.
|
||
- scope: metric tag·structured log field·trace attribute·exception message·actuator 응답. 구현: `JpaMetricTags` 다섯 개 · `ForbiddenMetricTags` · `SqlDiagnosticRedactor`(문자열 리터럴·숫자·이메일 통째 치환, 512자 절단) · `sanitized(Throwable)`가 타입만 남김.
|
||
- exceptions: 이미 외부에서 들어온 값(드라이버 예외 메시지)은 거부할 수 없으므로 그때만 redaction이 자리이고, 그것을 "보증"이 아니라 defence-in-depth로 부르는 것이 정확하다.
|
||
- relations: `case:pii-through-an-exception-message` · `case:a-report-that-cannot-carry-a-datasource` · `decision:tenant-id-is-never-a-metric-tag`
|
||
|
||
### REFERENCE — path·identifier는 등록하고 value는 바인딩한다
|
||
- slug: `register-paths-bind-values`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 16 · `analysis/05` §7.5
|
||
- classification: JSON path·정렬 필드·스키마 이름·conflict 컬럼은 파라미터로 바인딩할 수 없으므로 registry로 고정하고, 비교 값은 바인딩할 수 있으므로 항상 bound parameter로 넘긴다. 원문: "path는 registry에서 오고 고정 statement의 일부다. value는 caller에서 오고 항상 bound parameter다."
|
||
- scope: native SQL·JSONB 질의·동적 정렬·업서트 대상 컬럼. 등록 항목도 정규식으로 검증한다(예: unquoted PostgreSQL identifier `[a-z_][a-z0-9_]{0,62}`) — 등록 실수조차 함수 호출이나 두 번째 절을 넣지 못하게.
|
||
- exceptions: 스키마 이름처럼 statement의 일부이면서 tenant에서 유도되는 값은 registry 사전 등록이 유일한 안전 형태다 — 유도하면 외부 영향 데이터로 SQL을 만드는 것이다.
|
||
- relations: `reference:names-are-registry-keys-not-values` · `case:jpa-platform-capabilities-have-no-consumer`
|
||
|
||
### REFERENCE — sanitize가 아니라 reject가 기본이다
|
||
- slug: `reject-rather-than-sanitize`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §12.2
|
||
- classification: 경계를 넘는 값을 조용히 자르거나 치환하면 caller가 계속 그 값을 넘기고 절대 눈치채지 못한다. 거부하면 도입된 자리에서 실패한다. 기준은 "이 값의 생산자를 우리가 고칠 수 있는가"이고, 답이 예면 reject다.
|
||
- scope: metric tag·이름 값 타입·설정 키·헤더 이름. `LowCardinality.REGISTERED`가 정규식을 통과하지 못하면 던지는 것이 그 형태다.
|
||
- exceptions: 생산자를 고칠 수 없는 경우(외부 드라이버의 예외 메시지, 서드파티 응답 본문)는 redaction이 맞고, 그때는 그것이 보증이 아님을 문서에 적어야 한다.
|
||
- relations: `case:pii-through-an-exception-message` · `reference:telemetry-can-be-more-dangerous-than-its-subject` · `concept:cardinality-bounds-as-types`
|
||
|
||
### DECISION — tenant id는 메트릭 태그가 되지 않는다
|
||
- slug: `tenant-id-is-never-a-metric-tag`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-4` · `analysis/05` §13.2 · `analysis/02`
|
||
- decision-evidence: `.../experimental/multitenancy/TenantId.java`의 정규식과 근거 javadoc · `.../shared-contract/.../ForbiddenMetricTags.java` · `.../observation/JpaMetricTags.java`
|
||
- grounds: `reference:telemetry-can-be-more-dangerous-than-its-subject` · `concept:cardinality-bounds-as-types`
|
||
- classification: tenant 카디널리티는 정의상 unbounded이고 텔레메트리 안의 tenant id는 그렇게 취급되지 않는 시스템 안의 고객 데이터다. 두 층으로 강제된다 — `TenantId`가 `[a-z0-9][a-z0-9_-]{1,62}`로 스키마 이름·`set_config` 값·라우팅 키에 들어갈 수 있는 형태만 허용하고, 메트릭 태그 집합이 다섯 개로 닫혀 있으며 검증이 생성자에 있다. `request_id`가 baggage에는 허용되고 metric label에는 금지되는 비대칭이 테스트에서 pin돼 있다.
|
||
- relations: `reference:telemetry-can-be-more-dangerous-than-its-subject` · `case:pii-through-an-exception-message`
|
||
|
||
### DECISION — 커서에 서명하는 이유는 기밀성이 아니라 무결성이다
|
||
- slug: `cursors-are-signed-for-integrity`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-4` · `analysis/05` §2.4
|
||
- decision-evidence: `.../api/query/SignedJsonCursorCodec.java`의 클래스 javadoc이 결정과 근거를 적는다
|
||
- grounds: `concept:signed-cursor-structure` · `case:cursor-verification-order`
|
||
- classification: payload는 읽을 수 있고 그것이 의도다. 막는 것은 **무결성 침해**다 — "서명 없는 커서는 클라이언트가 제어하는 정렬 상태이고, 그걸 고쳐 쓰면 임의의 키로 seek할 수 있다. 스캔이 어디서 시작했는지에 predicate가 의존하는 곳이라면 이건 **접근 제어 우회**다." gRPC의 resume token이 같은 결정을 다른 필드 집합(caller·filter fingerprint·snapshot version·expiry·key id)으로 반복한다.
|
||
- relations: `concept:signed-cursor-structure` · `case:cursor-verification-order`
|
||
|
||
---
|
||
|
||
## TOPIC 5 — duplicate-mechanisms
|
||
|
||
### CASE — 클라이언트가 준 엔드포인트가 SSRF 가드가 아니라 약한 private 사본을 지났다
|
||
- slug: `a-weaker-private-copy-on-the-wired-path`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-3` · `analysis/13` §25.1
|
||
- code: `.../notification/.../WebPushSubscriptionValue.java:27-28,50-57`
|
||
- evidence: 없음 — 두 검사의 코드 비교와 호출 경로 확인
|
||
- classification: Web Push 구독 엔드포인트는 클라이언트가 제공하는 URL이고, 저장소에 SSRF 가드가 존재하는데 이 경로는 그것을 지나지 않고 같은 클래스 안의 약한 검사 private 사본을 쓴다. 중복 장치 중 조립된 쪽이 약한 쪽인 패턴에서 결과가 보안 경계에 닿는 사례다.
|
||
- missing-verification: 실제 SSRF 시도를 재현하지 않았다
|
||
- relations: `reference:check-which-duplicate-is-wired` · `case:a-policy-reversed-by-a-later-filter`
|
||
|
||
### CASE — 요청 식별자를 클라이언트가 고를 수 없다는 정책이 뒤에 도는 필터에 뒤집혔다
|
||
- slug: `a-policy-reversed-by-a-later-filter`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-3` · `analysis/14` §32.1
|
||
- code: `.../adapter/inbound/web/.../WebMvcRequestIdFilter.java`
|
||
- evidence: 없음 — 등록 지점과 두 구현 비교
|
||
- classification: 정책이 한 필터에 구현돼 있고 뒤에 도는 다른 배선 필터가 클라이언트 제공 값을 채택한다. 둘 다 조립돼 있어서 정책 쪽이 먼저 실행되고 나중 것이 덮는다.
|
||
- missing-verification: 부팅해서 실제 필터 체인 순서를 확인하지 않았다
|
||
- relations: `reference:check-which-duplicate-is-wired` · `case:a-weaker-private-copy-on-the-wired-path`
|
||
|
||
### CASE — JPA 플랫폼 capability 대부분에 production 소비자가 없다
|
||
- slug: `jpa-platform-capabilities-have-no-consumer`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#8-2` 항목 8 · `analysis/05` §17 P8, §11.0
|
||
- code: `.../persistence-jpa/.../springdata/**` · `.../hibernate/**` · `.../postgresql/{write,lock,json,array,range,copy}/**` · `.../cache/**` · `.../envers/**` · `.../querydsl/**`
|
||
- evidence: 없음 — leaf 밖 참조 전수 계수
|
||
- classification: `JpaKeysetQuerySupport`·`SafeSortMapper`·`JpaStreamExecutor`·`FetchPlanApplier`·batch/bulk/stateless executor·업서트·work claim·JSON/range/array·COPY·`CacheRegionCatalog`·`EntityGraphCatalog`가 전부 leaf 밖 참조 0이다. 결정적 증거는 **같은 leaf 안의 두 스토어**다 — `fileserver`(25파일)와 `notification`(53파일)이 합쳐 350KB이고 매일 PostgreSQL에 쓰는데 JPA 플랫폼 타입을 하나도 import하지 않고, 같은 문제(SKIP LOCKED 큐 클레임·안전 정렬·충돌 판정)를 각자 다시 만들었다. 즉 "아직 소비자가 없어서"가 아니라 **소비자가 있는데도 안 쓴다.** `support-matrix.md`가 대부분을 Advanced로 표기하므로 거짓말은 아니지만, 이 조합은 §P2(계약 테스트가 production 타입을 실행하지 않음)와 겹쳐 "구현됐고, 안 쓰이고, 테스트도 없다"가 된다.
|
||
- missing-verification: 파생 프로젝트가 실제로 이 capability들을 가져다 쓰는지 확인할 방법이 이 저장소 안에 없다 — 그것이 세 선택지(채택 / 명시적 라이브러리 선언 / 제거) 중 무엇이 맞는지를 정한다
|
||
- relations: `reference:check-which-duplicate-is-wired` · `reference:a-contract-test-must-run-the-adapters-statement` · `reference:a-bean-is-not-composition-evidence` · `decision:capability-grade-is-declared-not-inferred`
|
||
|
||
### CASE — forwarded 헤더 신뢰 판정이 Nginx에만 있고 Java 정책 421 LOC은 배선되지 않았다
|
||
- slug: `trust-policy-lives-in-nginx-not-in-the-code`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#3-1` · `analysis/14` §32.2
|
||
- code: `.../adapter/inbound/web/...`의 forwarded 헤더 정책(421 LOC) · `infra/nginx` 설정
|
||
- evidence: 없음 — 도달성 확인과 설정 파일 대조
|
||
- classification: forwarded 헤더를 어디까지 믿을지 판정하는 Java 정책이 421 LOC 작성돼 있고 배선되지 않는다. 실제 판정은 Nginx 설정이 한다. 두 곳이 어긋나면 코드 리뷰가 잡을 수 없고, Java 쪽을 고쳐도 동작이 바뀌지 않는다.
|
||
- missing-verification: Nginx 설정이 실제로 어떤 hop을 신뢰하는지 런타임에서 확인하지 않았다
|
||
- relations: `reference:check-which-duplicate-is-wired` · `reference:a-bean-is-not-composition-evidence`
|
||
|
||
### CASE — 재시도 구현이 둘이고 정교한 쪽을 아무도 호출하지 않는다
|
||
- slug: `a-retry-implementation-nobody-calls`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#8-2` 항목 7 · `analysis/05` §17 P1
|
||
- code: `.../transaction/FullTransactionRetryCoordinator.java` · `.../SpringPolicyTransactionPort.java` · `.../DefaultJpaRetryPolicy.java`
|
||
- evidence: `EVD-336` (`evidence/raw/336-retry-coordinator-has-no-advice.txt`) — 참조 전수 계수 · AOP 어드바이저 0 · ADR 이 지목한 인터셉터 부재
|
||
- classification: `FullTransactionRetryCoordinator`는 빈으로 등록되지만 production 코드 어디에서도 주입되지 않는다(참조는 app-bootstrap의 자기 config 둘과 테스트뿐). 실제로 도는 재시도는 `SpringPolicyTransactionPort` + `TransactionRetryBackoff`이고 `COMMAND_SERIALIZABLE_REPLAY_SAFE` 정책에만 적용된다. 둘이 다른 설정을 읽고(전자는 하드코딩 3회/20~500ms, 후자는 `ca-skeleton.jpa.transaction.retry-*` 기본 2회), `DefaultJpaRetryPolicy`의 6단계 순서·`IrreversibleSideEffectContext` 확인·`RetryBudget`의 elapsed 상한이 전부 호출되지 않는 경로에 있다. `support-matrix.md`는 "Full-transaction retry | Stable"이라고 선언한다.
|
||
- missing-verification: 없음 — 참조 전수 확인
|
||
- relations: `reference:check-which-duplicate-is-wired` · `case:autoconfiguration-in-name-only` · `decision:retry-unit-is-the-use-case`
|
||
|
||
### REFERENCE — 중복 장치를 찾으면 어느 쪽이 조립됐는지 먼저 확인한다
|
||
- slug: `check-which-duplicate-is-wired`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-3` · `analysis/99-cross-scope.md` §3.4
|
||
- classification: 같은 문제를 푸는 장치가 둘이면 "하나를 지우자"가 아니라 "요청 경로에 놓인 쪽이 어느 쪽인가"를 먼저 묻는다. 이 저장소의 13건 중 11건에서 조립된 쪽이 더 약했다. 이유는 잘 만든 정책 객체가 프레임워크 표면 밖(95.5%)에 있고 실제로 경로에 놓이는 것은 좁은 표면에서 손으로 배선한 것이기 때문이다.
|
||
- scope: 검증·가드·정책·복구 로직의 중복. 판정은 두 구현의 호출 경로를 각각 따라가는 것이고 코드 품질 비교보다 먼저 한다.
|
||
- exceptions: 방향이 반대인 경우도 있다 — 자격 증명 회전은 조립된 쪽이 동시성 계약 테스트까지 갖춘 더 나은 구현이고 죽은 쪽이 참조 0이다. 그때 조치는 "약한 쪽을 조립"이 아니라 "죽은 쪽을 제거"다.
|
||
- relations: `case:a-weaker-private-copy-on-the-wired-path` · `case:a-retry-implementation-nobody-calls` · `reference:two-vocabularies-for-one-concept`
|
||
|
||
### REFERENCE — `Atomic*` 타입의 존재는 원자성의 증거가 아니다
|
||
- slug: `atomic-type-is-not-atomicity`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#5-5` · `analysis/20` §7
|
||
- classification: `get()`으로 비교하고 별도로 `incrementAndGet()`을 부르는 것은 원자적 타입을 쓴 check-then-act다. 기준은 "읽은 값과 쓰는 값 사이에 다른 스레드가 끼어들 수 있는가"이고, 판정은 `compareAndSet`/`updateAndGet`/`synchronized`의 존재를 세는 것이다.
|
||
- scope: 카운터·레지스트리·상태 홀더. 특히 **경계를 강제하는 카운터**에서 결정적이다 — 경계는 동시성이 높을 때 필요하고 check-then-act는 정확히 그때 샌다.
|
||
- exceptions: 단일 스레드 전용임이 문서화됐거나 구조적으로 보장되는 경우. 다만 그 보장이 javadoc에 없으면 다음 사람이 알 수 없다.
|
||
- relations: `case:the-same-rotation-defect-closed-once-and-reproduced` · `reference:check-which-duplicate-is-wired`
|
||
|
||
### REFERENCE — 같은 개념의 두 어휘가 공존하면 하나를 죽은 것으로 표시한다
|
||
- slug: `two-vocabularies-for-one-concept`
|
||
- readiness: `READY`
|
||
- source: `analysis/19` §5.4 · `analysis/06` §58, §69 · `analysis/05` §12.5
|
||
- classification: 같은 개념을 두 타입이 표현하고 하나만 조립돼 있으면, 남은 쪽은 다음 사람이 어느 것을 써야 할지 알 수 없게 만든다. 기준은 "이 둘이 같은 질문에 답하는가"이고, 그렇다면 조립된 쪽을 정본으로 표시하고 나머지를 제거 대상으로 명시한다.
|
||
- scope: 감사 메타데이터·자격 증명 회전·TTL 선언·recovery 어휘. 표시 방법은 support matrix의 상태 컬럼("Candidate, not composed")과 ArchUnit 규칙(엔티티가 둘 다 쓰는 것을 금지)이다.
|
||
- exceptions: 두 어휘가 다른 계층에 속하고 각각 소비자가 있으면 중복이 아니다 — `api.error`의 안정 예외 계층과 `failure`의 웹 표면 매핑이 그 경우다.
|
||
- relations: `reference:check-which-duplicate-is-wired` · `decision:one-audit-mechanism-per-entity`
|
||
|
||
### DECISION — 감사 메커니즘은 엔티티당 정확히 하나여야 한다
|
||
- slug: `one-audit-mechanism-per-entity`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `analysis/05` §12.5
|
||
- decision-evidence: `.../testkit/arch/JpaArchitectureRules.java`의 `entitiesUseExactlyOneAuditMechanism` · `docs/jpa/support-matrix.md`의 두 메커니즘 상태 표기
|
||
- grounds: `reference:two-vocabularies-for-one-concept` · `reference:check-which-duplicate-is-wired`
|
||
- classification: `audit/AuditableEntity`(canonical, `@MappedSuperclass`, `created_at/by`·`updated_at/by`, actor 256)와 `auditing/AuditMetadata`(candidate, `@Embeddable`, `modified_*`, actor 64)가 공존하고 ArchUnit이 한 엔티티가 둘 다 쓰는 것을 막는다. 근거: "둘 다 고른 엔티티는 하나의 의미에 두 writer, 하나의 사실에 두 컬럼 계열, 그리고 어느 엔티티가 뭘 골랐는지 알아야 하는 마이그레이션을 얻는다." 어느 메커니즘도 bulk/native update에 도달하지 않으므로 `bulkUpdatesOfAuditedEntitiesStampAudit`가 별도로 그것을 강제한다.
|
||
- relations: `reference:two-vocabularies-for-one-concept`
|
||
|
||
### CASE — 같은 저장소가 "결정을 그 결정이 판정한 대상에 묶는 것"을 한 번은 맞게, 한 번은 틀리게 썼다
|
||
- slug: `the-same-repository-bound-a-decision-once-and-not-the-other-time`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-codegen.md` §17.4 · `analysis/grpc/grpc-advanced-bootstrap.md`(확인된 설계)
|
||
- code: `.../grpc-codegen/.../GrpcSchemaArtifactPublisher.java`(`evaluate`/`publish`) · `.../grpc-advanced-bootstrap/.../release/GrpcAdvancedSupportMatrix.java`(`apply`)
|
||
- evidence: 없음 — 두 메서드 본문 대조로 판정했다
|
||
- classification: 두 리프가 같은 문제를 푼다 — 판정과 기록이 두 호출로 나뉠 때 그 사이를 무엇이 묶는가. `GrpcSchemaArtifactPublisher.publish(candidate, decision)` 는 `decision.allowed()` 만 보고 기록한다. `PublishDecision` 은 `(boolean, List<String>)` 뿐이라 자기가 무엇을 판정했는지 들고 있지 않으므로, A 를 평가한 결정으로 B 를 발행할 수 있고 그러면 소비자 컴파일 게이트와 릴리스 버전 불변성을 둘 다 우회한다. 이 클래스가 존재하는 이유인 두 규칙이 인자 짝 하나로 무력해진다. 반대편에서 `GrpcAdvancedSupportMatrix.apply(decision)` 는 결정의 `from` 이 현재 등급과 다르면 던지고, 그 이유를 "두 승격이 경합했거나 하나가 재생된 경우" 라고 적는다. 결정이 자기가 밟고 선 상태를 들고 있고 적용 시점에 대조하는 형태다. 두 테스트의 차이도 같다 — 전자의 테스트는 `publish(artifact, evaluate(artifact, …))` 로 한 줄에서 짝을 맞춰 규율을 지키지만 코드가 그것을 강제하지 않고, 후자는 어긋난 짝을 넣는 테스트가 따로 있다.
|
||
- missing-verification: 어긋난 짝을 실제로 실행해 보지 않았다 — `publish` 본문에 대조 코드가 없다는 것으로 판정했다
|
||
- relations: `reference:check-which-duplicate-is-wired` · `case:the-same-rotation-defect-closed-once-and-reproduced` · `reference:a-validator-is-enforced-by-injection`
|
||
|
||
---
|
||
|
||
## TOPIC 6 — drift-direction
|
||
|
||
### CASE — 지원 매트릭스가 코드와 반대를 적었고, 그 오해가 소비자에게 자기 멱등성을 생략하게 한다
|
||
- slug: `support-matrix-said-the-opposite-of-the-code`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#8-1` · `analysis/19` §6.3
|
||
- code: `.../messaging-kafka/.../KafkaMessagingTransport.java:62-64`(`CAPABILITIES`와 javadoc) · `docs/messaging/support-matrix.md`
|
||
- evidence: `evidence/raw/tl-kafka-dedup-drift.txt` · `evidence/meta/…json` · `evidence/terminal/…svg` · `evidence/raw/255-messaging-capability-doc-vs-code-drift.txt`
|
||
- classification: capability 표 60칸을 코드 배열과 전수 대조한 결과 불일치가 정확히 한 칸이다 — Kafka `deduplicatedPublish`가 문서 `O`, 코드 `false`. 그 플래그가 12개 중 유일하게 실제 거부를 발생시키는 것이고, 코드 javadoc이 `true`→`false` 변경 이력과 피해를 직접 이름 붙인다 — "the caller believes the broker is deduplicating and skips the idempotency it would otherwise build." 나머지 48칸은 일치하고, Pulsar의 `keyedOrdering`은 문서가 두 배열 차이까지 반영해 코드보다 정밀하다.
|
||
- missing-verification: 없음 — 60칸 전수 대조
|
||
- relations: `reference:fix-overstatement-before-understatement` · `case:doc-contract-test-boundary-predicted-the-drift` · `decision:widen-doc-contract-assertions`
|
||
|
||
### CASE — README의 활성화 recipe를 그대로 따르면 애플리케이션이 시작되지 않는다
|
||
- slug: `the-readme-recipe-does-not-start`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-4` · `analysis/06` §4, §23
|
||
- code: `.../persistence-mongo/README.md` · `.../MongoMappingConfiguration.java` · `.../PolicyAwareMongoTypeMapper.java`
|
||
- evidence: `evidence/raw/129-mongo-empty-type-registry-write-probe.txt` · `evidence/raw/129a-mongo-empty-type-registry-write-probe.java…java`
|
||
- classification: README가 제시하는 활성화 절차를 그대로 따르면 시작은 하고 첫 write에서 예외가 난다. 빈 registry가 기본 bean이고 policy-aware mapper가 모든 converter에 무조건 설치되며 미등록 타입 write가 `IllegalStateException`을 던진다. probe가 shipped default 조합을 실제로 구성해 확인했고, 같은 converter에 Spring 기본 mapper를 두면 같은 write가 성공한다 — 실패는 문서·엔티티 형태가 아니라 이 leaf가 설치한 mapper에서 온다.
|
||
- missing-verification: 실제 MongoDB에 붙이지 않았다(probe는 converter 수준)
|
||
- relations: `reference:fix-overstatement-before-understatement` · `case:mongo-default-throws-on-first-write`
|
||
|
||
### CASE — 출하 default 조합이 첫 write에서 예외를 던진다
|
||
- slug: `mongo-default-throws-on-first-write`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-2` · `analysis/06` §23
|
||
- code: `.../persistence-mongo/.../MongoMappingConfiguration.java` · `.../MongoTypeMetadataConfigurer.java` · `.../PolicyAwareMongoTypeMapper.java:75,134` · `.../MongoTypeMetadataRegistry.java`
|
||
- evidence: `evidence/raw/129-mongo-empty-type-registry-write-probe.txt` · `evidence/raw/129a-mongo-empty-type-registry-write-probe.java…java`
|
||
- classification: 세 사실이 겹친다 — (1) 기본 bean이 **비어 있는** registry이고 그 javadoc이 "An empty registry so a deployment with no long-lived collection still starts"로 의도를 적는다, (2) configurer가 policy-aware mapper를 **모든** `MappingMongoConverter`에 무조건 설치한다, (3) 그 mapper가 미등록 타입 write에 `IllegalStateException`을 던진다. 그래서 module을 켜고 type metadata를 등록하지 않은 배포는 **시작은 하고 첫 write에서 실패한다.** probe가 shipped default 조합을 실제로 구성해 확인했고 Spring 기본 mapper로 바꾸면 같은 write가 성공한다. 그리고 같은 컴포넌트가 같은 질문에 세 가지로 답한다 — 미등록 타입의 정책은 `CLASS_METADATA_ALLOWED`, type-restricted **query**는 class name을 predicate에 쓰고, **write**는 예외다. 읽기와 쓰기가 정반대로 답하고 어느 쪽도 registry가 문서화한 기본값과 일치하지 않는다.
|
||
- missing-verification: 실제 MongoDB에 붙이지 않았다 — probe는 converter 수준이다
|
||
- relations: `case:the-readme-recipe-does-not-start` · `case:a-flag-that-validates-an-unwired-subsystem` · `reference:fix-overstatement-before-understatement`
|
||
|
||
### CASE — 문서가 UUIDv7이라 말하고 생성되는 것은 v4다
|
||
- slug: `documented-uuidv7-generates-v4`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-4` · `analysis/07` §5, §6, §7, §8 · `analysis/01` §11
|
||
- code: `.../adapter/outbound/identifier/...` · 해당 leaf의 `CLAUDE.md`·`README.md`
|
||
- evidence: 없음 — 문서 문장과 구현 대조
|
||
- classification: 문서는 UUIDv7을 말하고 구현은 v4를 만든다. 같은 leaf에서 `CLAUDE.md`의 의존성 서술 **세 항목이 모두** 틀렸고, README에 사실 오류가 셋 있으며, `CLAUDE.md`가 근거로 대는 두 가드 중 하나는 저장소에 없다. 10 파일짜리 leaf에서 문서 오류가 이만큼 나오는 것은 이 leaf에 production 소비자가 없다는 사실(§7 §3)과 함께 읽어야 한다 — 아무도 쓰지 않으면 문서도 검증되지 않는다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:fix-overstatement-before-understatement` · `reference:numbers-in-docs-should-be-derived`
|
||
|
||
### CASE — 선택할 수 없는 브로커가 지원 매트릭스에 기능 목록과 함께 실려 있다
|
||
- slug: `an-unselectable-broker-listed-with-features`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#2-4`, `#3-3` · `analysis/19` §6.2
|
||
- code: `.../messaging-spring-boot-starter/.../MessagingProviderSelection.java`(`BROKERS_WITHOUT_A_TRANSPORT`) · `docs/messaging/support-matrix.md` RabbitMQ 행
|
||
- evidence: `evidence/raw/256-messaging-provider-selection-and-rabbit-transport.txt`
|
||
- classification: `app.messaging.broker=rabbit`은 startup 오류다 — `RabbitChannelPublisher` 구현이 저장소에 없고 선택기가 그것을 이름으로 거부한다. 코드는 fail-closed로 정직하고 근거까지 적는다. 그런데 지원 매트릭스의 RabbitMQ 행은 "제한" 칸에 "장애 시나리오 미실행"만 적고 기능 칸에 publisher confirm·quorum queue·DLQ를 나열한다. 읽는 사람은 "검증은 덜 됐지만 쓸 수는 있는 옵션"으로 이해하고, 20 main / 2,443 LOC가 출하 아티팩트에 들어 있다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:fix-overstatement-before-understatement` · `case:support-matrix-said-the-opposite-of-the-code`
|
||
|
||
### CASE — 다섯 문서가 "exactly 19 leaf"라고 적고 레지스트리는 62다
|
||
- slug: `five-documents-say-nineteen-leaves`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-4` · `analysis/05` §17 P3
|
||
- code: `docs/jpa/repository-adaptation.md` · `docs/mongodb/…` · `docs/httpclient/…` · `docs/notification/module-mapping.md` · `docs/adr/ADR-MONGO-001-platform-boundary.md` · `src/settings.gradle`(16줄, 수 검사 없음)
|
||
- evidence: 없음 — 문서 문장과 레지스트리 대조
|
||
- classification: 다섯 문서가 "exactly 19 leaf identities"와 "`src/settings.gradle` throws when the registry does not contain exactly 19 modules"를 적는데, 레지스트리는 62개이고 `settings.gradle`에는 `19`도 수 검사도 없다(검증은 플러그인에 위임됐다). `verifyDocumentedLeafCount`가 존재하지만 `CLAUDE.md`·`AGENTS.md`·leaf `build.gradle`만 walk하고 `docs/**`는 대상이 아니다 — 그 게이트 자신의 주석이 "이름이 적힌 목록은 다섯 개의 모듈 CLAUDE.md와 네 개의 leaf build.gradle을 놓쳤다"고 같은 형태의 사고를 기록한다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:numbers-in-docs-should-be-derived` · `reference:omission-that-passes-is-not-a-gate`
|
||
|
||
### REFERENCE — 과대 진술 문서를 과소보다 먼저 고친다
|
||
- slug: `fix-overstatement-before-understatement`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-4` · `analysis/19` §6.3
|
||
- classification: 문서 드리프트에는 방향이 있고 위험이 대칭이 아니다. 과대 진술은 독자가 자기 안전장치를 생략하게 만들고, 과소 진술은 능력을 못 찾거나 잘못 고르게 만들지만 있는 방어를 제거하게 만들지는 않는다. 기준은 "이 문장을 믿은 팀이 무엇을 **하지 않게** 되는가"다.
|
||
- scope: 지원 매트릭스·capability 표·README의 보장 진술·javadoc의 현재 시제 서술. 이 저장소의 문서 드리프트 14건 중 P1은 하나뿐이고 그것이 과대 방향이다.
|
||
- exceptions: 과소 진술이라도 **선택을 바꾸는** 경우는 같은 우선순위다 — 선택 불가 브로커가 기능 목록과 함께 실린 경우가 그것이다.
|
||
- relations: `case:support-matrix-said-the-opposite-of-the-code` · `case:an-unselectable-broker-listed-with-features` · `case:the-readme-recipe-does-not-start`
|
||
|
||
### REFERENCE — 문서의 수치는 세지 말고 파생하거나 게이트로 붙든다
|
||
- slug: `numbers-in-docs-should-be-derived`
|
||
- readiness: `READY`
|
||
- source: `analysis/19` §6.4 · `analysis/05` §17 P3
|
||
- classification: 산문에 적은 수치는 다음 변경에서 드리프트한다. 기준은 "이 수치의 정본이 어디인가"이고, 정본이 있으면 문서는 세지 말고 가리켜야 한다. messaging 가족 문서가 자기 오류를 고치며 남긴 문장이 그 규칙이다 — "정확한 목록은 registry가 소유하므로 여기서 세지 않는다 — 세는 순간 다시 drift한다."
|
||
- scope: leaf 수·버전 목록·커버리지 수치·capability 개수. 대안은 (1) 세지 않고 SSOT를 가리키기, (2) 세야 한다면 그 수치를 검사하는 게이트를 두고 그 게이트의 walk 범위를 문서까지 넓히기.
|
||
- exceptions: 스냅샷임을 명시한 문서는 갱신하지 않는 것이 오히려 정확하다 — `00-project-overview.md`가 초기 sizing을 그대로 두고 헤더에 그 사실을 적는 형태다.
|
||
- relations: `case:five-documents-say-nineteen-leaves` · `reference:omission-that-passes-is-not-a-gate`
|
||
|
||
### OPEN QUESTION — 문서 계약 테스트의 단언 범위를 capability 표까지 넓힐 것인가
|
||
- slug: `widen-doc-contract-assertions`
|
||
- readiness: `OPEN`
|
||
- source: `final/document.md#7-4` · `analysis/19` §9.3
|
||
- known: capability 표는 기계로 검증 가능하다 — 어댑터 이름과 능력 상수와 마크다운 표가 전부 소스에 있다. 그리고 현재 계약 테스트의 좁은 단언 범위 밖에 발견된 드리프트 세 건이 전부 있다.
|
||
- unknown: 프로젝트가 이 확장을 채택할 것인지. 테스트 javadoc 은 오히려 좁게 유지하는 근거를 적는다 — "Asserting on wording would make every edit a test failure and the check would be deleted."
|
||
- next-verification: capability 표 60칸을 코드 상수에서 파생시키는 검사를 시제품으로 만들어 문구 변경에 대한 취약성을 실측한다.
|
||
- decision-criterion: 파생 검사가 문구 변경에 취약하지 않다는 것이 실측되면 확장을 채택한다. 그렇지 않으면 좁은 단언을 유지하고 경계를 문서에 적는다.
|
||
- relations: `case:doc-contract-test-boundary-predicted-the-drift` · `case:support-matrix-said-the-opposite-of-the-code` · `reference:agreement-between-documents-proves-nothing`
|
||
- classification: capability 표는 기계로 검증 가능하다 — `CompatibilityMatrix.entries()`가 어댑터 이름을 갖고, 각 어댑터의 `MessagingCapabilities`는 코드 상수이며, 표는 마크다운 파이프 테이블이다. 그러나 프로젝트가 이 확장을 선택한 기록이 없고, 테스트 javadoc은 오히려 좁게 유지하는 근거를 적는다("Asserting on wording would make every edit a test failure and the check would be deleted"). 결정이 필요한 지점이며 이 노드는 생성되지 않는다.
|
||
- relations: `case:doc-contract-test-boundary-predicted-the-drift` · `reference:fix-overstatement-before-understatement`
|
||
|
||
### DECISION — 지원 등급은 추론이 아니라 선언이고 증거 없이는 올라가지 않는다
|
||
- slug: `capability-grade-is-declared-not-inferred`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-5` · `analysis/05` §2.5 · `analysis/19` §6.6 · `analysis/20` §2.6
|
||
- decision-evidence: `.../api/capability/CapabilitySupport.java`의 javadoc("evidence suite가 돌지 않은 capability는 컴파일이 된다는 이유로 STABLE이 되지 않는다") · `CompatibilityMatrix.Entry.hasLiveBrokerCertification()`의 파생 구현 · `docs/adr/ADR-GRPC-ADV-001-capability-promotion-is-per-capability.md`
|
||
- grounds: `reference:grades-may-understate-never-overstate` · `reference:agreement-between-documents-proves-nothing`
|
||
- classification: 세 가족이 같은 결정을 각자 구현했다. JPA는 `SupportLevel`을 선언으로 두고 evidence card로 승격을 게이트하며, messaging은 등급을 boolean 필드에서 레인 증거 파생으로 바꿨고(그 전에는 Rabbit이 `true`를 달고 있었는데 fault scenario가 한 번도 실행된 적 없었다), gRPC는 capability별 promotion gate를 ADR로 고정했다.
|
||
- relations: `reference:grades-may-understate-never-overstate` · `case:three-versions-declared-one-executed` · `decision:candidate-evidence-stays-at-r1`
|
||
|
||
---
|
||
|
||
## TOPIC 7 — learning-transfer-between-families
|
||
|
||
### CASE — 두 번째 플랫폼이 첫 번째의 bridge 부재는 막고 게이트 배선은 옮기지 않았다
|
||
- slug: `the-second-platform-carried-the-design-not-the-wiring`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-6` · `analysis/20` §2, §3 · `analysis/99-cross-scope.md` §2, §4
|
||
- code: `src/grpc/CLAUDE.md` · `src/config/architecture/modules.json` · `.../messaging-kafka/build.gradle` · `.github/workflows/messaging-certification.yml`
|
||
- evidence: `evidence/raw/tl-platform-suite-results.txt` · `evidence/raw/266-grpc-family-claims-verification.txt` · `evidence/raw/250-messaging-certification-gate-chain.txt`
|
||
- classification: gRPC 가족이 messaging을 명시적으로 참조하며 만들어졌고 목표를 문서에 적었다. 옮겨진 셋(bridge를 먼저 정함·framework-free 계약 leaf·속성 기반 전송 선택)은 전부 레지스트리와 `build.gradle`로 표현되는 규칙이고, 옮겨지지 않은 셋(증거 파생 등급·CI가 돌리는 게이트·조립에 연결된 시작 검증기)은 전부 Gradle 태스크와 CI YAML로 표현되는 규칙이다.
|
||
- missing-verification: 이 분류가 인과인지 상관인지는 확인할 수 없다 — 작성자가 왜 후자를 옮기지 않았는지에 대한 기록이 없다
|
||
- relations: `reference:registry-rules-transfer-ci-rules-do-not` · `case:a-release-gate-with-no-evidence-producer` · `case:the-same-rotation-defect-closed-once-and-reproduced` · `decision:grpc-stays-build-only-until-the-bridge-is-decided`
|
||
|
||
### CASE — 같은 자격 증명 회전 결함이 한 가족에서 닫히고 다른 가족에서 재현됐다
|
||
- slug: `the-same-rotation-defect-closed-once-and-reproduced`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#5-5` · `analysis/19` §5.4 · `analysis/20` §7.4
|
||
- code: `.../messaging-security/.../CredentialRuntimeRegistry.java` · `.../src/test/.../CredentialRotationContractTest.java` · `.../grpc-policy/.../security/GrpcCredentialRotationManager.java:89,103,119,120`
|
||
- evidence: `evidence/raw/tl-rotation-defect-reproduced.txt` · `evidence/meta/…json` · `evidence/terminal/…svg`
|
||
- classification: messaging이 get → fetch → put → clear를 동기화 없이 하던 결함을 닫고 이력을 계약 테스트 javadoc에 남겼다 — "one replacement was dropped from the map without ever being cleared — a secret left in memory that nothing owns." gRPC의 회전 매니저는 `AtomicReference`를 쓰면서 `compareAndSet`을 한 번도 쓰지 않고 `get()`→`set()`으로만 다룬다(`synchronized`도 0). javadoc이 그 경합의 존재를 이미 알고 있다 — "the usual reason for one is two rotators racing". `grpc-policy`의 테스트 16개 중 동시성을 다루는 것이 없다.
|
||
- missing-verification: 경합을 재현하는 동시성 테스트를 작성하지 않았다
|
||
- relations: `reference:atomic-type-is-not-atomicity` · `reference:registry-rules-transfer-ci-rules-do-not`
|
||
|
||
### REFERENCE — 레지스트리로 표현된 규칙은 전이되고 CI로 표현된 규칙은 전이되지 않는다
|
||
- slug: `registry-rules-transfer-ci-rules-do-not`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-6` · `analysis/99-cross-scope.md` §2, §4
|
||
- classification: 한 팀이 배운 규칙을 다음 팀이 이어받는지는 그 규칙이 어디에 적혀 있는가로 갈린다. 레지스트리 항목·`build.gradle` 선언·타입 시그니처로 표현된 규칙은 다음 사람이 같은 파일을 편집하면서 마주치고 따라 한다. Gradle 태스크와 CI YAML로 표현된 규칙은 마주치지 않는다.
|
||
- scope: 플랫폼 가족·모듈 템플릿·복제되는 구조. 관측: gRPC가 messaging에서 옮겨온 셋은 전부 레지스트리/`build.gradle` 표현이고 옮기지 않은 셋은 전부 Gradle 태스크/CI YAML 표현이다.
|
||
- exceptions: 표본이 두 가족뿐이라 인과인지 상관인지 확정할 수 없다. 안전한 형태는 "전이되지 않는다"가 아니라 **"CI로만 표현된 규칙은 복제 시 명시적으로 옮겨야 한다"**는 체크리스트 항목이다.
|
||
- relations: `case:the-second-platform-carried-the-design-not-the-wiring` · `case:a-release-gate-with-no-evidence-producer`
|
||
|
||
### DECISION — gRPC 플랫폼은 build-only로 두고 애플리케이션 도달 경로를 먼저 정한다
|
||
- slug: `grpc-stays-build-only-until-the-bridge-is-decided`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#2-3` · `analysis/20` §2.3, §2.6
|
||
- decision-evidence: `docs/adr/ADR-GRPC-001-platform-family-and-registry-shape.md` · `modules.json`(18 leaf 전부 `runtime_memberships: []`, `adapter-inbound-grpc`의 `allowed_dependencies`에 이 가족 없음) · `src/grpc/CLAUDE.md` · `docs/compatibility/grpc-support-matrix.md`("Not released … build-only")
|
||
- grounds: `case:the-second-platform-carried-the-design-not-the-wiring` · `reference:registry-rules-transfer-ci-rules-do-not`
|
||
- classification: messaging이 starter를 `app-bootstrap` 의존으로 넣으면서 18 leaf가 출하 아티팩트에 실렸고 MSG-015가 실재 문제가 됐다. gRPC 가족은 그 선을 넘지 않기로 하고 레지스트리로 강제하며, 배선하려면 `runtime_memberships`를 먼저 바꾸고 `verifyRuntimeModuleMembership`을 통과시켜야 한다. 운영 문서가 그 상태를 정확히 공시한다는 점에서 messaging의 지원 매트릭스와 대비된다.
|
||
- relations: `case:the-second-platform-carried-the-design-not-the-wiring` · `case:a-release-gate-with-no-evidence-producer` · `reference:runtime-membership-decides-severity`
|
||
|
||
---
|
||
|
||
## TOPIC 8 — owner-safe-state-machines
|
||
|
||
### CONCEPT — fenced lease — 만료 시각만으로는 부족한 이유
|
||
- slug: `fenced-lease`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1`, `#4-3` · `analysis/05` §10 · `analysis/19` §7.3
|
||
- code: `db/migration/jpa/…` 및 `db/migration/messaging/V2__messaging_outbox_lease_fencing.sql` 헤더 · `.../fileserver/…` V3 마이그레이션
|
||
- classification: lease가 "언제 끝나는가"만 기록하고 "누가 들고 있는가"를 기록하지 않으면 만료를 지난 worker가 여전히 쓸 수 있다. V2 마이그레이션 헤더가 그 시나리오를 3단계로 적는다 — relay A가 claim하고 브로커를 부름 / lease 만료, relay B가 재claim하고 발행하고 PUBLISHED 기록 / relay A가 타임아웃 후 그 위에 AMBIGUOUS를 씀. 그리고 결정적 문장 — "**Making the lease longer than the publish timeout lowers the odds; it does not turn a GC pause, a scheduler stall**..." 해법은 소유자와 fencing token을 행에 기록하고 terminal write가 그 튜플로 매칭하는 것이다. 같은 결함이 이 저장소에서 최소 세 곳(messaging outbox·fileserver cleanup·notification dispatcher)에 나타났다.
|
||
- missing-verification: 없음 — 마이그레이션과 claim SQL을 코드로 확인했다
|
||
- relations: `case:a-lease-without-an-owner` · `reference:cas-tuple-in-the-where-clause` · `reference:expired-claim-and-expired-execution-differ`
|
||
|
||
### CONCEPT — CAS 튜플과 update count가 답이 되는 구조
|
||
- slug: `cas-tuple-and-update-count`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §10.1, §10.3
|
||
- code: `.../postgresql/idempotency/IdempotencyTransitionGateway.java`의 다섯 statement · `.../outbox/PostgreSqlPollingDeliveryAdapter.java`의 completion CAS 셋
|
||
- classification: 상태 전이를 "읽고 → 판단하고 → PK로 update"하면 그 사이에 takeover한 worker의 상태를 덮어쓴다. 이 저장소의 형태는 소유권 튜플 전체(scope · owner token · attempt · claim operation id · state revision)를 where 절에 반복하고 **update count 자체를 답으로 쓰는** 것이다 — 1이면 이 owner가 그 revision에서 여전히 owner였고, 0이면 다른 무언가가 record를 움직였으니 caller는 자기 view를 현재로 취급하면 안 된다. polling delivery는 여기에 authority `EXISTS` 서브쿼리를 더해 cutover를 가로지르지 못하게 한다.
|
||
- missing-verification: 컨테이너 레인 미실행 — 동시 claim에서 실제로 0행이 나오는지 관측하지 않았다
|
||
- relations: `reference:cas-tuple-in-the-where-clause` · `case:native-claim-did-not-bump-the-version` · `open-question:v2-state-machine-lanes-not-executed`
|
||
|
||
### CONCEPT — capability_schema_registry — 스키마 적용과 사용 승인의 분리
|
||
- slug: `capability-schema-registry`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §8.4, §11.3
|
||
- code: `db/migration/postgresql/V6__capability_schema_registry_adoption.sql` · 각 capability 스트림의 V1 · `.../notification/NotificationSchemaActivation.java`
|
||
- classification: 독립 Flyway 스트림 설계의 접착제다. 각 capability 스트림의 V1이 (1) 선행조건 검사(`DO $$ ... RAISE EXCEPTION`으로 core epoch가 ACTIVE인지), (2) 테이블 생성, (3) **자기를 `INSTALLED_INACTIVE`로 등록**의 세 단계를 밟고, 어댑터가 런타임에 `capability_id` + `core_epoch` + `feature_revision` + `lifecycle_state='ACTIVE'`를 조회해 확인한다. 그래서 "스키마가 적용됐다"와 "capability를 써도 된다"가 분리된다. 확인은 startup에서만 하고 호출마다 하지 않는 이유("승격되지 않은 스트림은 배포 상태이고, 매 호출마다 묻는 것은 프로세스가 도는 동안 바뀔 수 없는 질문에 round trip을 넣는 것")도 이 개념의 일부다.
|
||
- missing-verification: 없음
|
||
- relations: `decision:capability-separates-installation-from-activation` · `concept:independent-flyway-streams` · `case:registry-column-too-short-for-its-own-path`
|
||
|
||
### CASE — lease가 만료 시각만 기록하고 소유자를 기록하지 않아 terminal state가 되돌려졌다
|
||
- slug: `a-lease-without-an-owner`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-3` · `analysis/19` §7.3 · `analysis/08` §V3
|
||
- code: `db/migration/messaging/V2__messaging_outbox_lease_fencing.sql` · fileserver V3 마이그레이션
|
||
- evidence: 없음 — 마이그레이션 헤더가 사후 기록이다
|
||
- classification: V1이 `lease_expires_at`만 기록해 claim이 "언제 끝나는가"만 말하고 "누가"를 말하지 않았고, relay의 terminal write가 `message_id`만으로 매칭했다. 그래서 lease를 지나 멈춰 있던 worker가 다른 worker가 이미 PUBLISHED로 옮긴 행에 AMBIGUOUS를 덮어썼고 행이 다시 claim 가능해져 메시지가 두 번 발행됐다. 같은 결함이 fileserver cleanup(claim이 owner·token·expiry를 기록하지 않아 죽은 worker의 항목이 영영 IN_PROGRESS로 남음)과 notification dispatcher에도 있었다.
|
||
- missing-verification: 컨테이너 레인 미실행
|
||
- relations: `concept:fenced-lease` · `reference:cas-tuple-in-the-where-clause` · `open-question:v2-state-machine-lanes-not-executed`
|
||
|
||
### CASE — transition digest가 "누가·언제"만 덮고 "무엇"을 덮지 않아 다른 전이를 같다고 보고했다
|
||
- slug: `a-digest-that-covered-who-but-not-what`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §10.1
|
||
- code: `.../postgresql/idempotency/IdempotencyDigestPolicy.java`
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: digest가 `transition|operationId|ownerToken|attempt|stateRevision`이었고 **무엇을 했는지**를 전혀 덮지 않았다. retryable로 기록된 `FAIL`과 abandoned로 기록된 `FAIL`이 같은 digest를 냈고, 서로 다른 응답이나 서로 다른 retention을 가진 두 completion도 그랬다. digest를 비교하는 replay는 "차이 전체가 중요한 부분인 두 전이"를 같다고 결론지었다. 수정은 `semanticArguments`(disposition·retention·response digest·codec identity)를 포함하고 **길이 프레이밍**으로 구성하는 것 — 모든 구성요소가 가변 폭 텍스트이고 최소 하나(owner token)는 플랫폼이 제약할 것이 아니므로 delimiter join은 서로 다른 목록을 같은 문자열로 렌더링할 수 있다. `VERSION`을 붙여 구성이 바뀌면 저장된 digest가 가로질러 비교되지 않게 한다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:digest-must-be-length-framed-and-versioned` · `concept:cas-tuple-and-update-count`
|
||
|
||
### CASE — 활성 트랜잭션 검사가 data source를 묻지 않아 다른 커넥션에서 커밋됐다
|
||
- slug: `an-active-transaction-check-that-asked-the-wrong-question`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §10.1
|
||
- code: `.../postgresql/idempotency/IdempotencyCapabilityGuard.java`
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: store가 스레드에 활성 read-write 트랜잭션이 있는지 확인했는데 그건 **어떤 data source에서든 어떤 트랜잭션이든** 열려 있으면 참이다. 형제 outbox/inbox 어댑터는 `hasResource(dataSource)`를 확인하는데 그것이 실제로 중요한 질문이다. 차이는 data source가 둘인 애플리케이션에서 드러난다 — **다른** 쪽의 트랜잭션 안에서 발행된 mutation이 옛 검사를 통과하고, 이 store의 커넥션에서 트랜잭션 밖으로 돌고, **원자적이어야 했던 작업과 독립적으로 커밋됐다.**
|
||
- missing-verification: 두 data source 구성으로 재현하지 않았다
|
||
- relations: `reference:cas-tuple-in-the-where-clause` · `concept:cas-tuple-and-update-count`
|
||
|
||
### CASE — 만료된 CLAIMED는 takeover하고 만료된 EXECUTING은 조정을 요구하도록 갈랐다
|
||
- slug: `expired-claim-versus-expired-execution`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §10.1, §10.4
|
||
- code: `.../postgresql/idempotency/PostgreSqlOwnerSafeIdempotencyStore.java`의 claim 결정 트리 · `.../inbox/PostgreSqlSameStoreInboxAdapter.java`
|
||
- evidence: 없음 — 결정 트리와 그 javadoc
|
||
- classification: 만료된 lease를 일률적으로 takeover하면 이미 실행이 시작된 작업을 blind retry하게 된다. 이 저장소는 상태로 나눈다 — 만료된 `CLAIMED`는 `resetClaim`으로 takeover하고, 만료된 `EXECUTING`은 `abandonExpiredExecution`으로 `ABANDONED`에 넣고 `RecoveryRequired`를 반환한다. inbox도 같은 축을 쓰되 `RECEIVED`(takeover 가능)와 `PROCESSING`(→ DEAD, recovery-required)로 나눈다. 즉 "claim만 했다"와 "실행에 들어갔다"가 만료 시 다른 결론을 낳는다.
|
||
- missing-verification: 컨테이너 레인 미실행
|
||
- relations: `reference:expired-claim-and-expired-execution-differ` · `concept:fenced-lease` · `open-question:v2-state-machine-lanes-not-executed`
|
||
|
||
### CASE — native claim이 `@Version`을 올리지 않아 충돌을 보고하지 않는 낙관적 잠금이 됐다
|
||
- slug: `native-claim-did-not-bump-the-version`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §11.2
|
||
- code: `.../notification/RecipientClaimSql.java`의 claim statement
|
||
- evidence: 없음 — statement와 그 주석
|
||
- classification: claim이 native `UPDATE`인데 JPA `@Version` 컬럼을 올리지 않으면, claim 전에 로드된 managed 엔티티가 여전히 옛 version을 들고 있고 그 flush가 **성공하면서 lease와 state를 pre-claim 값으로 덮어쓴다.** native statement가 충돌이 있었다고 말해주지 않았기 때문에 **충돌을 보고하지 않는 낙관적 잠금**이 된다. 수정은 statement에 `version = d.version + 1`을 넣는 것이고, 그 한 줄에 주석이 붙어 있다.
|
||
- missing-verification: 컨테이너 레인 미실행
|
||
- relations: `concept:cas-tuple-and-update-count` · `reference:cas-tuple-in-the-where-clause` · `reference:a-contract-test-must-run-the-adapters-statement`
|
||
|
||
### REFERENCE — CAS 튜플을 where 절에 전부 반복하고 update count를 답으로 쓴다
|
||
- slug: `cas-tuple-in-the-where-clause`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 17 · `analysis/05` §10, §16
|
||
- classification: 읽고 나서 PK만으로 update하면 그 사이 takeover한 worker의 상태를 덮어쓴다. 소유권을 이루는 모든 값을 where 절에 반복하고 update count를 판정에 쓰면, 1은 "여전히 내 것"이고 0은 "누군가 움직였다"를 뜻한다.
|
||
- scope: lease·claim·상태 전이를 갖는 모든 관계형 어댑터. 튜플 구성은 보통 scope·owner token·attempt·claim operation id·state revision이고, 권한 경계가 있으면 authority `EXISTS`를 더한다.
|
||
- exceptions: 단일 writer가 보장되는 경로(마이그레이션·관리 작업)는 필요 없다. 다만 그 보장이 배포 형태(단일 인스턴스)에서 오면 다중 인스턴스로 가는 날 깨지므로, 보장의 출처를 적어야 한다.
|
||
- relations: `concept:cas-tuple-and-update-count` · `case:a-lease-without-an-owner` · `case:native-claim-did-not-bump-the-version`
|
||
|
||
### REFERENCE — 시간은 DB에서, 그리고 행을 잠근 다음에 읽는다
|
||
- slug: `read-the-clock-after-the-lock`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 17 · `analysis/05` §10, §16
|
||
- classification: 애플리케이션 시계로 lease 만료를 판단하면 lease를 쓴 머신이 아닌 곳에서 판단하는 것이고, 락 전에 DB 시계를 읽으면 행이 바뀔 수 있는 시점 이전의 순간으로 판단하는 것이다. 순서는 `for update` → `clock_timestamp()`다.
|
||
- scope: lease·claim·만료 판정을 갖는 모든 SQL. `now()`가 아니라 `clock_timestamp()`인 이유도 같은 축이다 — 전자는 트랜잭션 시작 시각으로 고정된다.
|
||
- exceptions: 읽기 전용 조회에서 만료 여부를 **보고만** 하는 경우는 잠글 필요가 없다. 다만 그 결과로 행동하면 안 된다.
|
||
- relations: `reference:cas-tuple-in-the-where-clause` · `concept:cas-tuple-and-update-count`
|
||
|
||
### REFERENCE — 만료된 claim과 만료된 실행은 다르게 다뤄야 한다
|
||
- slug: `expired-claim-and-expired-execution-differ`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §10.1, §10.4
|
||
- classification: lease 만료는 "누가 들고 있었는가"만 말하고 "무엇까지 했는가"를 말하지 않는다. claim만 한 상태의 만료는 안전하게 takeover할 수 있지만, 실행에 들어간 상태의 만료는 외부 부수효과가 이미 발생했을 수 있으므로 blind retry가 아니라 조정으로 보내야 한다.
|
||
- scope: 실행 전 예약과 실행 자체를 구별하는 모든 작업 큐·멱등성 저장소. 구현은 상태를 둘로 나누는 것이다(`CLAIMED`/`EXECUTING`, `RECEIVED`/`PROCESSING`).
|
||
- exceptions: 외부 부수효과가 없는 순수 계산 작업은 구별이 필요 없다. 다만 "부수효과 없음"이 유지되는지는 시간이 지나며 바뀌므로 그 전제를 적어야 한다.
|
||
- relations: `case:expired-claim-versus-expired-execution` · `concept:fenced-lease` · `reference:unknown-is-a-third-result`
|
||
|
||
### REFERENCE — digest는 길이 프레이밍하고 버전을 붙인다
|
||
- slug: `digest-must-be-length-framed-and-versioned`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §10.1
|
||
- classification: 가변 폭 문자열을 delimiter로 이어 digest를 만들면 서로 다른 구성요소 목록이 같은 문자열로 렌더링될 수 있다. 길이 프레이밍(`len:value`)이 그것을 막고, 버전 번호가 구성 변경 전후의 digest를 가로질러 비교하지 못하게 한다. 그리고 digest는 "누가·언제"뿐 아니라 **"무엇을"**까지 덮어야 한다.
|
||
- scope: replay 판정·중복 탐지·전이 동일성 비교에 쓰는 모든 digest. 구성요소 중 하나라도 플랫폼이 제약하지 않는 값(외부 토큰)이면 프레이밍이 필수다.
|
||
- exceptions: 모든 구성요소가 고정 길이이거나 플랫폼이 문법을 강제하는 값이면 delimiter로 충분하다. 다만 그 강제가 어디 있는지 적어야 한다.
|
||
- relations: `case:a-digest-that-covered-who-but-not-what` · `reference:names-are-registry-keys-not-values`
|
||
|
||
### OPEN QUESTION — V2 상태 기계 넷의 컨테이너 레인이 실행되지 않았다
|
||
- slug: `v2-state-machine-lanes-not-executed`
|
||
- readiness: `OPEN`
|
||
- source: `final/document.md#6-2` · `analysis/05` §15.5
|
||
- known: 네 상태 기계(idempotency·outbox-storage·outbox-polling·inbox)가 `readiness-cards.yaml`에서 `implemented-candidate` 상태다 · 각 card의 `evidence.scenarios`가 exact selector를 고정한다 · 후보 검증은 R1에 머문다
|
||
- unknown: 네 레인이 이 리비전에서 통과하는지 · 동시 claim에서 CAS가 실제로 0행을 내는지 · trigger 기반 fencing(`trg_fence_legacy_outbox_writer`)이 예상대로 발화하는지
|
||
- next-verification: `./gradlew :adapter:outbound:persistence-jpa:postgresqlIdempotencyIntegrationTest postgresqlOutboxStorageIntegrationTest postgresqlOutboxPollingIntegrationTest postgresqlInboxIntegrationTest` — 각각 readiness card의 producer다
|
||
- decision-criterion: 넷 다 zero-skip으로 통과하면 네 Case의 `missing-verification`을 지운다. 하나라도 실패하면 그 실패가 새 Case가 되고 해당 card의 state를 재검토한다
|
||
- relations: `case:a-lease-without-an-owner` · `case:expired-claim-versus-expired-execution` · `case:native-claim-did-not-bump-the-version` · `open-question:container-lanes-not-executed`
|
||
|
||
### DECISION — capability는 스키마 적용과 사용 승인을 분리한다
|
||
- slug: `capability-separates-installation-from-activation`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#4-1` · `analysis/05` §8.4, §11.3
|
||
- decision-evidence: `db/migration/postgresql/V6__capability_schema_registry_adoption.sql` · 각 capability 스트림 V1의 3단계 구조 · `.../NotificationSchemaActivation.java`의 startup 확인과 javadoc
|
||
- grounds: `concept:capability-schema-registry` · `concept:independent-flyway-streams`
|
||
- classification: 스키마가 적용된 것과 capability를 써도 되는 것을 분리하고, 후자를 `lifecycle_state='ACTIVE'`로 표시한다. activation 검증기의 javadoc이 이 결정이 없을 때의 두 실패 모양을 적는다 — `ddl-auto=validate`에서는 기능을 **끈** 배포가 테이블 부재로 boot 실패하고, `ddl-auto=none`이면 boot는 성공하고 **첫 발송이 raw `relation does not exist`를** 처음 보낸 사람에게 반환한다.
|
||
- relations: `concept:capability-schema-registry` · `decision:flyway-owns-the-schema`
|
||
|
||
### DECISION — 상태 기계 구현은 Spring stereotype을 갖지 않는다
|
||
- slug: `state-machines-carry-no-stereotype`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `analysis/05` §10
|
||
- decision-evidence: 네 V2 구현의 클래스 선언과 그 javadoc("두 composition root가 `dev.caskeleton.adapter`를 component-scan하므로 `@Repository`를 붙이면 선택하지 않은 배포에서도 빈이 된다")
|
||
- grounds: `reference:off-must-be-structural` · `reference:a-bean-is-not-composition-evidence`
|
||
- classification: 스테레오타입을 붙이면 두 합성 루트의 스캔이 그것을 잡아, capability를 선택하지 않은 배포에서도 빈이 된다. 그래서 네 구현이 전부 plain class이고 협력자를 주입이 아니라 생성자에서 조립한다 — "이건 이 store의 부품이지 애플리케이션이 조립하거나 교체하는 서비스가 아니고, 주입하면 public bean 표면이 1개에서 6개로 넓어진다."
|
||
- relations: `reference:off-must-be-structural` · `decision:one-root-owns-the-master-switch`
|
||
|
||
---
|
||
|
||
## TOPIC 9 — transaction-deadline-and-pool
|
||
|
||
### CONCEPT — 호출 예산에서 DB 로컬 타임아웃까지의 데드라인 전파
|
||
- slug: `deadline-propagation`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#3-2` · `analysis/05` §3.3
|
||
- code: `.../transaction/TransactionDeadlineCalculator.java` · `.../postgresql/PostgreSqlLocalTimeoutConfigurer.java` · `.../h2/H2LocalTimeoutConfigurer.java`
|
||
- classification: 호출자의 남은 예산이 세 단계로 좁혀져 DB 세션 설정에 도달하는 구조의 설명이다. 획득 전에는 `connectionTimeout + beginBudget + minimumActionWindow + completionMargin`을 요구하고 Spring의 초 단위 타임아웃이 1초 미만이면 시작하지 않는다. begin 이후에는 statement/lock/idle 셋을 각각 유도하고 하나라도 1ms 미만이면 거부한다. DB에 미는 방법이 `SET`이 아니라 `set_config(..., true)`인 이유가 둘이다 — `SET`은 파라미터 바인딩 전에 파싱되어 syntax error가 나고, 함수 호출은 값이 statement text에서 빠진다. 세 번째 인자 `true`가 transaction-local을 뜻한다. H2 구현이 두 가지에서 다른데(세션 스코프, idle 가드 없음) 그것이 H2의 성질이지 선택이 아니라는 점도 함께 적혀 있다.
|
||
- missing-verification: 실제 세션에서 `SHOW statement_timeout`으로 적용을 확인하지 않았다
|
||
- relations: `reference:deadline-narrows-in-three-stages` · `reference:session-scoped-settings-outlive-the-transaction` · `case:a-five-second-string-that-broke-every-prod-deploy`
|
||
|
||
### CONCEPT — `REQUIRES_NEW`의 커넥션 비용과 풀 사이징 제약
|
||
- slug: `requires-new-connection-cost`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#3-2` · `analysis/05` §3.1, §13.2
|
||
- code: `.../transaction/SpringTransactionPort.java`(`inNew`) · `.../experimental/multitenancy/TenantPoolBudget.java`
|
||
- classification: `REQUIRES_NEW`는 바깥 트랜잭션의 커넥션을 **핀한 채로** 새 물리 JDBC 커넥션을 딴다. 그래서 풀 사이징 제약이 곱셈이 된다 — `maximumPoolSize >= (concurrent_threads × (1 + max_inNew_depth)) + 1`. 레코드마다 `inNew`를 도는 루프가 금지인 이유(풀 고갈 + 데드락)와, 같은 곱셈 함정이 database-per-tenant에서 어떻게 반복되는지(각 tenant 풀은 개별적으로 합리적이고 그 합이 아니다 — 50 tenant × 10 = 서버 `max_connections` 100에 500 커넥션, 실패는 idle이던 것 포함 모든 tenant에 동시에 도착)를 함께 다룬다.
|
||
- missing-verification: 풀 계약 레인 미실행
|
||
- relations: `case:requires-new-pins-the-outer-connection` · `case:tenant-pools-summed-past-the-server-ceiling` · `open-question:pool-contract-lane-not-executed`
|
||
|
||
### CASE — `connection-timeout: 5s`가 모든 prod 배포를 시작 실패시켰고 local만 통과했다
|
||
- slug: `a-five-second-string-that-broke-every-prod-deploy`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §14.6
|
||
- code: `.../app-bootstrap/src/main/resources/application.yml`의 hikari 블록 주석 · `.../HikariPoolConstraintValidator.java`
|
||
- evidence: 없음 — application.yml 주석의 사후 기록
|
||
- classification: `connection-timeout`이 `HikariConfig#setConnectionTimeout(long)`에 바인딩되므로 binder가 `"5s"`를 즉시 거부한다. `"5s"`로 출시됐고 **모든 prod/dev 배포가 시작에 실패했는데** local은 `application-local.yml`이 값을 override해서 통과했다 — **prod 레인만이 그것을 찾을 수 있었다.** 그리고 `HikariPoolConstraintValidator`는 이 키를 `DurationStyle`로 읽어서 `"5s"`를 기꺼이 받아들인다 — **그 관대함이 잘못된 기본값을 지원되는 것처럼 보이게 만들었다.**
|
||
- missing-verification: 없음
|
||
- relations: `concept:deadline-propagation` · `reference:local-with-a-different-db-is-a-different-system` · `case:a-validator-checking-the-wrong-datasource`
|
||
|
||
### CASE — validator가 요청을 서비스하지 않는 datasource를 검증하고 있었다
|
||
- slug: `a-validator-checking-the-wrong-datasource`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §14.5
|
||
- code: `.../app-bootstrap/.../persistencejpa/JpaDataSourceProfileValidator.java`의 javadoc
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: validator가 `app.jpa-platform.datasource.*`에 바인딩된 settings를 읽었는데 **요청을 서비스하는 풀은 `spring.datasource.hikari.*`에서** 만들어진다 — 하나의 풀에 두 개의 기술, 그리고 validator는 사용되지 않는 기술에 대해 통과할 수 있다. 더 나쁜 건 그 평행 네임스페이스가 어떤 shipped YAML에도 env-key 레지스트리의 어떤 행에도 없어서 두 필드가 항상 null이었고 `requirePoolBounds`가 모든 배포에서 던졌을 것이라는 점이다 — 아무것도 그것을 호출하지 않아서 아무것도 실패하지 않았다. **서로를 상쇄한 두 결함이고, 애플리케이션이 시작한 이유는 두 번째가 첫 번째를 숨겼기 때문이다.**
|
||
- missing-verification: 없음
|
||
- relations: `reference:a-bean-is-not-composition-evidence` · `case:a-five-second-string-that-broke-every-prod-deploy`
|
||
|
||
### CASE — `REQUIRES_NEW`가 바깥 커넥션을 핀한 채 새 커넥션을 딴다
|
||
- slug: `requires-new-pins-the-outer-connection`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#3-2` · `analysis/05` §3.1
|
||
- code: `.../transaction/SpringTransactionPort.java`의 `inNew`와 그 javadoc(application-port D12)
|
||
- evidence: 없음 — 제약이 javadoc에 수식으로 적혀 있다
|
||
- classification: `inNew`가 편해 보이지만 커넥션 비용이 곱셈이라는 것을 문서화한 제약이다. 풀 사이징 부등식이 명시돼 있고, 레코드마다 `inNew`를 도는 루프가 금지이며(풀 고갈 + 데드락), 배치로 묶거나 루프를 트랜잭션 밖으로 빼야 한다. 같은 이유로 `inRootWrite`가 `REQUIRES_NEW`로 suspend해서 "root인 척"하지 않고 fail-fast한다.
|
||
- missing-verification: 풀 계약 레인 미실행 — 포화 시 pending count 보고와 대기 동작을 관측하지 않았다
|
||
- relations: `concept:requires-new-connection-cost` · `decision:in-root-write-fails-fast` · `open-question:pool-contract-lane-not-executed`
|
||
|
||
### REFERENCE — 데드라인은 호출 예산에서 시작해 세 단계로 좁힌다
|
||
- slug: `deadline-narrows-in-three-stages`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §3.3
|
||
- classification: 상류 호출자의 남은 시간이 트랜잭션·statement·lock 순으로 좁혀져야 하고, 각 단계에서 남은 시간이 다음 단계의 최소 요구를 못 채우면 **시작하지 않는 것**이 옳다. 시작해서 중간에 잘리면 completion-unknown을 만들지만 시작하지 않으면 확정 거부다.
|
||
- scope: 호출 예산을 전파하는 모든 계층. 각 단계에 여유(margin)를 두어 마지막에 결과를 기록할 시간을 남긴다.
|
||
- exceptions: 예산을 모르는 진입점(스케줄러·부팅 작업)은 자기 상한을 갖되 그것이 무한이 아니어야 한다.
|
||
- relations: `concept:deadline-propagation` · `reference:write-transactions-need-a-finite-timeout`
|
||
|
||
### REFERENCE — 쓰기 트랜잭션에는 유한 타임아웃이 필수다
|
||
- slug: `write-transactions-need-a-finite-timeout`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §2.3
|
||
- classification: 무제한 write 트랜잭션은 statement 하나가 막히면 커넥션·락·row version을 무한정 잡는다. 기준은 "이 프로파일이 read-only가 아닌데 타임아웃이 없는가"이고, 있으면 생성자가 거부해야 한다.
|
||
- scope: 트랜잭션 프로파일·정책 값 타입. `TransactionProfile`의 생성자가 `!readOnly && timeout이 null/0/음수`를 거부하는 형태다.
|
||
- exceptions: 읽기 전용 트랜잭션은 상한이 있으면 좋지만 필수는 아니다 — 락을 잡지 않기 때문이다. 다만 락을 잡는 read(`FOR UPDATE`)는 쓰기와 같이 취급한다.
|
||
- relations: `reference:deadline-narrows-in-three-stages` · `reference:make-the-unsafe-state-unrepresentable`
|
||
|
||
### REFERENCE — 세션 스코프 설정은 풀로 돌아간 커넥션에 남는다
|
||
- slug: `session-scoped-settings-outlive-the-transaction`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §3.3, §13.2
|
||
- classification: `SET`으로 건 설정은 커넥션이 풀로 돌아가도 살아남아 다음 borrower가 상속한다. 다음 borrower가 다른 tenant이거나 tenant 없는 백그라운드 job이면 그 설정이 격리 경계를 무너뜨린다. 기준은 "이 설정이 트랜잭션과 함께 되돌아가는가"이고, PostgreSQL에서는 `set_config(..., true)`가 그것을 보장한다.
|
||
- scope: `statement_timeout`·`lock_timeout`·`search_path`·`app.tenant_id` 등 세션 상태 전부. RLS의 tenant 바인딩이 transaction-local이어야 하는 이유가 같다.
|
||
- exceptions: 대응물이 없는 벤더(H2의 idle 가드)에서는 세션 스코프를 인정하되 **그 사실을 적고** caller-side deadline에 맡긴다 — "적용했다"고 거짓 보고하지 않는다.
|
||
- relations: `concept:deadline-propagation` · `concept:rls-three-preconditions` · `case:search-path-survived-the-return-to-the-pool`
|
||
|
||
### OPEN QUESTION — 풀 계약 레인이 실행되지 않아 포화 동작이 확인되지 않았다
|
||
- slug: `pool-contract-lane-not-executed`
|
||
- readiness: `OPEN`
|
||
- source: `final/document.md#6-2` · `analysis/05` §15.3
|
||
- known: `jpaPlatformPoolContractTest`가 threshold를 약속하지 않고 **행동 계약**만 검증하도록 개명됐다 · 검증 항목은 `REQUIRES_NEW` depth 1이 동시 스레드당 커넥션 2개를 요구하는 것, 포화된 풀이 pending count를 보고하는 것, caller가 커넥션 없이 진행하지 않고 기다리는 것
|
||
- unknown: 이 리비전에서 그 세 계약이 실제로 성립하는지 · 풀 사이징 부등식이 현재 기본값(`maximumPoolSize` 10, `minimum-idle` 2)에서 어떤 동시성까지 견디는지
|
||
- next-verification: `./gradlew :adapter:outbound:persistence-jpa:jpaPlatformPoolContractTest --console=plain`
|
||
- decision-criterion: 세 계약이 통과하면 `case:requires-new-pins-the-outer-connection`의 `missing-verification`을 지운다. 실패하면 그 실패와 현재 기본값의 관계를 새 Case로 기록한다
|
||
- relations: `case:requires-new-pins-the-outer-connection` · `concept:requires-new-connection-cost` · `open-question:container-lanes-not-executed`
|
||
|
||
### DECISION — 트랜잭션 템플릿은 모드별로 미리 만들어 둔다
|
||
- slug: `templates-are-built-once-per-mode`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#3-2` · `analysis/05` §3.1
|
||
- decision-evidence: `.../transaction/SpringTransactionPort.java`의 생성자와 javadoc
|
||
- grounds: `concept:requires-new-connection-cost` · `reference:write-transactions-need-a-finite-timeout`
|
||
- classification: `TransactionTemplate`은 문서상 thread-safe지만 **mutable**이다. 호출마다 propagation/readOnly를 바꿔 쓰면 같은 빈을 공유하는 동시 요청 사이에 race window가 생긴다. 그래서 네 모드에 대해 템플릿 셋을 생성 시점에 만들어 두고(`inWrite`와 `inRootWrite`는 실행 속성이 같아 공유하되 precondition만 다르다) 이후 변경하지 않는다.
|
||
- relations: `case:requires-new-pins-the-outer-connection` · `decision:in-root-write-fails-fast`
|
||
|
||
### DECISION — `inRootWrite`는 suspend하지 않고 fail-fast한다
|
||
- slug: `in-root-write-fails-fast`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#3-2` · `analysis/05` §3.1
|
||
- decision-evidence: `.../transaction/SpringTransactionPort.java`의 `inRootWrite` 구현과 `NestedRootTransactionRejectedException`
|
||
- grounds: `decision:templates-are-built-once-per-mode` · `concept:requires-new-connection-cost`
|
||
- classification: "root 트랜잭션이어야 한다"를 `REQUIRES_NEW`로 구현하면 호출자 트랜잭션과 독립 커밋되는 silent 의미 변경이 생긴다. 대신 활성 트랜잭션이 있으면 즉시 거부한다. 그리고 `TransactionTemplate.execute`가 커밋 후에 값을 반환하므로 `inRootWrite`의 결과는 **post-commit에만** 호출자에게 보이고 커밋 실패는 값 대신 예외로 전파된다.
|
||
- relations: `decision:templates-are-built-once-per-mode` · `reference:unknown-is-a-third-result`
|
||
|
||
---
|
||
|
||
## TOPIC 10 — schema-ownership-and-capability-streams
|
||
|
||
### CONCEPT — 독립 Flyway 스트림과 baseline version 0
|
||
- slug: `independent-flyway-streams`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §8.3
|
||
- code: `.../persistence-jpa/.../notification/NotificationSchemaStream.java` · `.../postgresql/PostgreSqlPersistenceConfig.java:115` · `db/migration/**`
|
||
- classification: 이 저장소에 Flyway 스트림이 여덟 개이고 각각 자기 history 테이블을 갖는 이유의 설명이다. 뻔한 해법(디렉터리를 primary location 목록에 추가)이 틀린 이유는 두 트리가 다 V1부터 번호를 매기고 공유 history가 두 V1을 같은 버전으로 만들어 하나를 거부하거나 resolution 순서에 따라 건너뛰기 때문이다. `baselineVersion("0")` + `baselineOnMigrate(true)`가 필요한 이유(core가 이미 채운 스키마 위에 자기 history를 만들어야 하므로 Flyway가 "history 없는 비어있지 않은 스키마"라며 거부한다)와, version 0이어야 하는 이유(더 높은 baseline은 그 스트림의 마이그레이션을 건너뛴다 — 이 설정이 보통 두려워하는 실패 모드)를 함께 다룬다.
|
||
- missing-verification: 마이그레이션 레인 미실행
|
||
- relations: `case:two-trees-both-numbered-from-v1` · `concept:capability-schema-registry` · `decision:flyway-owns-the-schema`
|
||
|
||
### CASE — 두 트리가 다 V1부터 번호를 매겨 공유 history가 하나를 건너뛸 수 있었다
|
||
- slug: `two-trees-both-numbered-from-v1`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §8.3
|
||
- code: `.../notification/NotificationSchemaStream.java`의 javadoc
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: notification 마이그레이션이 `db/migration/jpa/notification-platform`에 있는데 primary location은 `db/migration/postgresql`이라 **아무것도 적용하지 않았다.** 디렉터리를 목록에 추가하는 해법이 틀린 이유는 `V1__notification_platform_core`와 `V1__initial_schema`가 같은 버전이 되기 때문이다. 해법은 독립 스트림 + 자기 history 테이블이고, 그것이 capability를 진짜로 optional하게 만든다.
|
||
- missing-verification: 없음
|
||
- relations: `concept:independent-flyway-streams` · `case:messaging-migrations-collide-at-v2`
|
||
|
||
### CASE — `char(64)`와 `varchar(64)` 불일치를 H2가 가리고 있었다
|
||
- slug: `h2-hid-a-column-type-mismatch`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §8.5
|
||
- code: `db/migration/postgresql/V10__idempotency_request_hash_varchar.sql` 헤더 · `db/migration/jpa/idempotency/V2` · `.../IdempotencyRecordEntity.java`
|
||
- evidence: 없음 — 마이그레이션 헤더가 실패 메시지 전문을 담는다
|
||
- classification: V1이 `request_hash`를 `char(64)`로 선언했고 엔티티는 `length = 64`로 매핑해 Hibernate가 `varchar(64)`로 읽는다. 실 PostgreSQL에 `ddl-auto=validate`로 시작하면 실패했다 — `found [bpchar (Types#CHAR)], but expecting [varchar(64)]`. **로컬이 H2로 도는 한 보이지 않았다** — H2의 create-drop은 엔티티에서 스키마를 만들므로 절대 엔티티와 불일치할 수 없다. 수정이 엔티티가 아니라 마이그레이션 쪽인 이유와, V1을 in-place로 고치지 않은 이유("적용된 마이그레이션의 checksum은 이미 그것을 돌린 모든 배포에 대한 약속")가 함께 적혀 있다. 이 사건이 `application-local.yml`을 PostgreSQL 기본으로 바꿨다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:local-with-a-different-db-is-a-different-system` · `reference:an-applied-checksum-is-a-promise`
|
||
|
||
### CASE — 레지스트리 컬럼이 38자 경로에서 짧아 "더 짧은 경로를 적는" 우회를 유혹했다
|
||
- slug: `registry-column-too-short-for-its-own-path`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §8.4
|
||
- code: `db/migration/postgresql/V9__widen_capability_schema_stream.sql` · `db/migration/jpa/idempotency/V2`
|
||
- evidence: 없음 — 마이그레이션 헤더의 사후 기록
|
||
- classification: `schema_stream`이 `varchar(32)`였고 작성 당시 모든 스트림에 맞았으며 `'db/migration/jpa/notification-platform'`(38자)에서 안 맞기 시작했다. 실패 모드가 나쁜 종류다 — 모든 면에서 올바른 등록이 `value too long`으로 마이그레이션 타임에 실패하고, **뻔한 우회책은 스트림의 실제 경로가 아닌 더 짧은 경로를 기록하는 것**이며, 스키마가 어디서 왔는지에 대해 거짓말하는 레지스트리는 없는 것보다 나쁘다. 128로 넓힌 이유도 적혀 있다 — `capability_id`가 이미 `varchar(128)`이고 **하나의 bound가 두 개보다 추론하기 쉽다.**
|
||
- missing-verification: 없음
|
||
- relations: `concept:capability-schema-registry` · `reference:an-applied-checksum-is-a-promise`
|
||
|
||
### CASE — Flyway location customizer가 운영자가 바인딩한 값을 덮어썼다
|
||
- slug: `a-customizer-that-discarded-the-bound-property`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §7.7
|
||
- code: `.../postgresql/PostgreSqlPersistenceConfig.java:94-115`의 javadoc
|
||
- evidence: 없음 — javadoc의 사후 기록
|
||
- classification: customizer가 무조건 `locations(...)`를 호출했는데 그것은 Spring이 `spring.flyway.locations`에서 바인딩한 것을 **대체**한다. 그래서 운영자가 `SPRING_FLYWAY_LOCATIONS`로 capability 스트림을 추가하고 Flyway가 성공적 마이그레이션을 보고하는 것을 보고도 **벤더 스트림만** 얻을 수 있었다 — 프로퍼티는 읽히고 바인딩되고 그 뒤에 도는 customizer가 버렸다. `local-notification-ingest` 레인은 7개 location을 세팅하고 1개를 적용했다. 지금은 "아무도 고르지 않았을 때만 기여하고, 누군가 골랐으면 비켜선다".
|
||
- missing-verification: 없음
|
||
- relations: `concept:independent-flyway-streams` · `reference:a-bean-is-not-composition-evidence`
|
||
|
||
### CASE — messaging 마이그레이션 두 leaf가 같은 디렉터리에서 `V2`를 둘 만들었다
|
||
- slug: `messaging-migrations-collide-at-v2`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-3` · `analysis/19` §7.2
|
||
- code: `src/messaging/messaging-inbox-jdbc-postgresql/.../db/migration/messaging/V2__messaging_inbox.sql` · `src/messaging/messaging-outbox-jdbc-postgresql/.../db/migration/messaging/V2__messaging_outbox_lease_fencing.sql`
|
||
- evidence: `evidence/raw/259-messaging-flyway-migration-namespace.txt`
|
||
- classification: 두 leaf가 같은 classpath 리소스 디렉터리 `db/migration/messaging/`에 각자 번호를 매겨 `V2`가 둘이다. 그 위치를 Flyway에 주는 순간 duplicate version으로 부팅이 실패한다. 지금 실패하지 않는 유일한 이유는 **그 위치를 아무도 Flyway에 주지 않기 때문**이고(main 코드에서 `db/migration/messaging`을 부르는 곳 0건), 각 leaf의 IT는 자기 jar 리소스만 보므로 재현하지 못한다. 원 구현 계획서는 분리된 위치를 지정했었다.
|
||
- missing-verification: 실제로 그 위치를 Flyway에 추가해 duplicate version 실패를 관측하지 않았다
|
||
- relations: `concept:independent-flyway-streams` · `case:two-trees-both-numbered-from-v1` · `case:outbox-chain-behind-an-unsatisfiable-condition`
|
||
|
||
### REFERENCE — 마이그레이션 스트림은 자기 history 테이블을 갖는다
|
||
- slug: `each-stream-owns-its-history-table`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §8.3
|
||
- classification: 두 마이그레이션 트리가 하나의 history를 공유하면 버전 공간을 공유하게 되고, 각 트리가 독립적으로 번호를 매기는 한 충돌은 시간 문제다. 기준은 "이 트리의 버전 번호를 누가 정하는가"이고, 답이 둘 이상이면 스트림을 나누고 history를 분리한다.
|
||
- scope: capability별·모듈별로 나뉜 모든 마이그레이션. 분리 시 `baselineVersion("0")` + `baselineOnMigrate(true)`가 필요하고, 0이어야 그 스트림의 마이그레이션이 전부 돈다.
|
||
- exceptions: 하나의 팀이 하나의 트리를 소유하고 그 안에서 번호를 조정할 수 있으면 분리가 불필요하다. sample composition이 두 location을 하나의 목록으로 합쳐 버전 공간을 공유하는 경우가 그 예이고, 그때는 번호 충돌을 사람이 피해야 한다.
|
||
- relations: `concept:independent-flyway-streams` · `case:two-trees-both-numbered-from-v1` · `case:messaging-migrations-collide-at-v2`
|
||
|
||
### REFERENCE — 로컬이 다른 DB면 로컬 테스트는 다른 시스템에 대한 진술이다
|
||
- slug: `local-with-a-different-db-is-a-different-system`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 18 · `analysis/05` §8.5
|
||
- classification: 로컬이 in-memory DB로 돌고 스키마를 엔티티에서 생성하면 **엔티티와 마이그레이션의 불일치가 구조적으로 발견 불가능**하다. 기준은 "로컬과 prod가 같은 벤더·같은 마이그레이션 모드·같은 스키마 소유자를 쓰는가"이고, 아니면 "로컬에서는 됩니다"가 다른 시스템에 대한 진술이다.
|
||
- scope: 개발 환경 구성 전반. 이 저장소는 두 사건(`char(64)`, `fs_cleanup_item`) 이후 local을 PostgreSQL + Flyway + `ddl-auto=validate`로 바꾸고 H2를 별도 개발자 task로 밀었다.
|
||
- exceptions: 빠른 단위 테스트용 in-memory는 유지해도 된다. 다만 그 레인이 **스키마 계약을 증명한다고 주장하면 안 된다** — H2가 `FOR UPDATE SKIP LOCKED`를 실제로 지키는지 같은 벤더 차이는 측정하고 근거를 적어야 한다.
|
||
- relations: `case:h2-hid-a-column-type-mismatch` · `reference:an-applied-checksum-is-a-promise`
|
||
|
||
### REFERENCE — 적용된 마이그레이션의 checksum은 그것을 돌린 모든 배포에 대한 약속이다
|
||
- slug: `an-applied-checksum-is-a-promise`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §8.1, §8.5
|
||
- classification: 이미 적용된 마이그레이션을 in-place로 고치면 그것을 돌린 배포들의 history와 어긋난다. 기준은 "이 파일이 어딘가에 적용된 적이 있는가"이고, 있으면 새 버전을 추가하고 가드된 변환(`DO $$ ... IF EXISTS ... THEN ALTER`)을 쓴다. 같은 이유로 Flyway `repair`는 모드가 아니다 — history를 지금 디스크에 맞게 다시 써서 **증거를 지워 증상을 해결**한다.
|
||
- scope: 모든 forward-only 마이그레이션. 스트림이 둘 이상이고 상대 순서가 고정되지 않았으면 같은 가드된 변환을 양쪽에 둔다.
|
||
- exceptions: 어디에도 적용된 적 없는 마이그레이션(방금 작성한 것)은 고쳐도 된다. 판정 근거는 개발자의 기억이 아니라 history 테이블이다.
|
||
- relations: `case:h2-hid-a-column-type-mismatch` · `case:registry-column-too-short-for-its-own-path` · `decision:repair-is-not-a-mode`
|
||
|
||
### DECISION — Flyway가 스키마를 소유하고 런타임 롤은 DDL 권한을 갖지 않는다
|
||
- slug: `flyway-owns-the-schema`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#4-1`, `#10-3` · `analysis/05` §8.1, §12.1
|
||
- decision-evidence: `docs/adr/ADR-JPA-004-flyway-schema-source-of-truth.md` · `.../security/PostgreSqlRuntimeRoleVerifier.java`의 startup 질의 · `.../migration/FlywaySchemaPolicy.java`의 프로필별 모드
|
||
- grounds: `reference:an-applied-checksum-is-a-promise` · `reference:local-with-a-different-db-is-a-different-system`
|
||
- classification: 스키마를 Flyway가 소유하고 ORM은 확인만 한다(`ddl-auto`는 `none`/`validate`만 허용). 그리고 그것을 **강제 가능**하게 만드는 것이 런타임 롤에서 DDL 권한을 뺀 것이다 — 서버에 `has_schema_privilege`로 직접 물어 fail-closed로 거부한다. "애플리케이션 자신의 자격증명이 DDL을 실행할 수 없으면, 어떤 코드 경로도 어떤 라이브러리도 어떤 주입된 statement도 런타임에 스키마를 바꿀 수 없다." staging/prod는 배포 파이프라인이 마이그레이션을 소유하고 애플리케이션은 `VALIDATE_ONLY`다.
|
||
- relations: `decision:repair-is-not-a-mode` · `decision:capability-separates-installation-from-activation`
|
||
|
||
### DECISION — Repair는 모드가 아니라 운영자가 호출하는 작업이다
|
||
- slug: `repair-is-not-a-mode`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `analysis/05` §8.1
|
||
- decision-evidence: `.../migration/FlywaySchemaPolicy.java`(모드 enum에 repair 없음) · `.../FlywayValidationGate.java`의 javadoc
|
||
- grounds: `reference:an-applied-checksum-is-a-promise` · `decision:flyway-owns-the-schema`
|
||
- classification: Flyway의 `repair`는 schema history를 지금 디스크에 있는 스크립트에 맞게 다시 쓴다 — **증거를 지워서 증상을 해결**한다. checksum mismatch는 배포된 스크립트가 적용된 것과 다르다는 뜻이고 흥미로운 질문은 "어떤 변경이 이 DB에 빠졌는가"인데, repair는 그 질문을 물을 수 없게 만들어 답한다. 그래서 startup 동작이 아니라 운영자가 의도적으로 호출하는 operation descriptor로만 존재한다.
|
||
- relations: `decision:flyway-owns-the-schema` · `reference:an-applied-checksum-is-a-promise`
|
||
|
||
---
|
||
|
||
## TOPIC 11 — redis-command-admission
|
||
|
||
### CONCEPT — 명령 카탈로그와 admission 아홉 단계
|
||
- slug: `redis-admission-stages`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/10` §64
|
||
- code: `.../cache-redis/.../CommandPolicyGuard.java` · `.../RedisCommandCatalog.java` · `.../RedisCommandGateway.java`
|
||
- classification: 이 SDK가 명령 하나를 내보내기 전에 지나는 단계의 설명이다 — 카탈로그 분류(BLOCKED·R3·R4 거부) · capability/최소 버전 확인 · permit provenance 검증 · 네임스페이스 검사 · Cluster 동일 슬롯 검사 · 요청 예산 · 정책 기반 레인·타임아웃 유도 · 실패 번역 · 관측. 그리고 그 위에 얹힌 계약 — gateway는 "everything routed through it has already passed `CommandPolicyGuard`"를 전제하므로 정책·permit·예산·타임아웃·관측을 자기 관심사로 두지 않는다. 카탈로그가 미분류 명령을 fail-closed로 거부하는 것이 이 구조의 바닥이다.
|
||
- missing-verification: 토폴로지 레인 미실행 — 실제 서버에서 각 단계가 도는지 관측하지 않았다
|
||
- relations: `case:five-adapters-bypass-the-single-admission-point` · `reference:a-single-admission-point-must-count-its-bypasses` · `decision:unclassified-commands-are-refused`
|
||
|
||
### CASE — "build gate"라 불리는 catalog drift 검사가 어디에서도 실행되지 않는다
|
||
- slug: `a-build-gate-that-is-not-in-the-build`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/10` §47
|
||
- code: `.../cache-redis/.../RedisCommandMetadataDiff.java` · 명령 정책 파일 머리 주석
|
||
- evidence: `evidence/raw/163-cache-redis-guard-connection-codec-probes.txt` §8.1
|
||
- classification: 정책 파일이 "The catalog drift gate compares the two and fails the build when the server grows a command this file has not judged"라고 적고 비교 로직과 다섯 버킷 테스트가 완성돼 있다. 그런데 실제 서버 메타데이터를 넣는 곳이 없다 — main 참조 0, 토폴로지 레인 사용 0, 실행 태스크 없음. 빌드를 깨는 gate는 존재하지 않고 gate가 쓸 비교 함수와 그 단위 테스트만 있다. 위험은 fail-closed 카탈로그가 잡지 못하는 셋이다 — key spec 이동(네임스페이스·슬롯 검사가 잘못된 인자를 키로 봄) · ACL 카테고리 확장(계정 분리 가정이 약해짐) · deprecation(타입 있는 API가 사라질 명령을 계속 노출).
|
||
- missing-verification: 토폴로지 레인 미실행
|
||
- relations: `reference:a-gate-nobody-runs-reports-the-last-run` · `reference:server-metadata-defines-the-command` · `open-question:redis-topology-lane-not-executed`
|
||
|
||
### CASE — 의미 어댑터 다섯이 gateway를 직접 불러 admission 아홉 단계를 건너뛴다
|
||
- slug: `five-adapters-bypass-the-single-admission-point`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/10` §64
|
||
- code: `.../CommandPolicyGuard.java` · cache·idempotency·lease·ratelimit·realtime 어댑터의 `lease.gateway()` 호출부
|
||
- evidence: `evidence/raw/165-cache-redis-semantic-adapters-probes.txt` §8.1
|
||
- classification: 두 javadoc이 "The single admission point every command passes through"와 "everything routed through it has already passed `CommandPolicyGuard`"를 주장하는데, 다섯 어댑터가 `RedisRuntimeOwner` → `RedisLease` → `lease.gateway()`를 직접 부른다(30곳). `SyncRedisCommandExecutor`·`ReactiveRedisCommandExecutor`·`CommandPolicyGuard`·`CommandRequest`를 참조하는 파일이 0이고 타입 있는 API 참조도 0이다. 아홉 단계 중 네임스페이스만 `CapabilityKeyspace`가 같은 `RedisNamespace`에서 키를 조립해 구성으로 유지된다.
|
||
- missing-verification: 실제 Redis에 붙여 우회 경로의 명령이 정책 없이 나가는지 관측하지 않았다
|
||
- relations: `concept:redis-admission-stages` · `reference:a-single-admission-point-must-count-its-bypasses` · `reference:check-which-duplicate-is-wired`
|
||
|
||
### CASE — NOSCRIPT 복구가 다섯 벌이고 넷은 스크립트 레지스트리를 지나지 않는다
|
||
- slug: `five-copies-of-noscript-recovery`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/10` §56
|
||
- code: `.../cache-redis/...`의 NOSCRIPT 복구 다섯 지점
|
||
- evidence: 없음 — 복구 경로 전수 확인
|
||
- classification: Redis가 `NOSCRIPT`를 반환했을 때의 복구(스크립트 재등록 후 재시도)가 다섯 벌로 구현돼 있고 그중 넷이 스크립트 레지스트리를 지나지 않는다. 레지스트리를 지나는 한 벌만이 스크립트 SHA와 소스의 대응을 유지하므로, 나머지 넷은 재등록하는 스크립트가 레지스트리가 아는 것과 같다는 보장이 없다.
|
||
- missing-verification: 실제 `SCRIPT FLUSH` 이후 다섯 경로의 동작 차이를 관측하지 않았다
|
||
- relations: `reference:check-which-duplicate-is-wired` · `concept:redis-admission-stages`
|
||
|
||
### CASE — startup probe가 production에서 한 번도 실행되지 않는다
|
||
- slug: `a-startup-probe-that-never-runs`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/10` §6
|
||
- code: `.../cache-redis/...`의 startup probe와 그 조건
|
||
- evidence: 없음 — 도달성 확인
|
||
- classification: Redis 연결·버전·capability를 확인하는 startup probe가 존재하는데 production 경로에서 실행되지 않는다. 그래서 "이 배포의 Redis가 이 SDK가 요구하는 최소 버전을 만족하는가"가 부팅 시점에 답해지지 않고, 첫 명령에서 capability 예외로 나타난다.
|
||
- missing-verification: 부팅해서 probe 미실행을 직접 확인하지 않았다
|
||
- relations: `reference:the-startup-validator-follows-the-autoconfiguration-root` · `reference:a-bean-is-not-composition-evidence`
|
||
|
||
### REFERENCE — 서버 메타데이터가 명령의 정의이고 정책 파일은 허용 범위다
|
||
- slug: `server-metadata-defines-the-command`
|
||
- readiness: `READY`
|
||
- source: `analysis/10` §47
|
||
- classification: 명령이 **무엇인가**는 서버의 `COMMAND DOCS`/`COMMAND INFO`/`COMMAND GETKEYSANDFLAGS`가 정하고, 이 SDK가 그것으로 **무엇을 할 용의가 있는가**는 정책 파일이 정한다. 둘을 대조하는 게이트가 없으면 서버가 명령을 늘리거나 key spec을 옮겨도 알 수 없다.
|
||
- scope: 벤더 프로토콜을 감싸는 모든 SDK. 대조해야 할 다섯 버킷 — 미분류 신규 명령 · 사라진 명령 · key spec 이동 · ACL 카테고리 변경 · deprecation.
|
||
- exceptions: 미분류 명령은 fail-closed 카탈로그가 이미 막으므로 그 버킷만은 게이트 없이도 안전하다. 나머지 넷은 카탈로그가 잡지 못한다.
|
||
- relations: `case:a-build-gate-that-is-not-in-the-build` · `decision:unclassified-commands-are-refused`
|
||
|
||
### REFERENCE — 단일 admission point는 우회 경로를 세어야 성립한다
|
||
- slug: `a-single-admission-point-must-count-its-bypasses`
|
||
- readiness: `READY`
|
||
- source: `analysis/10` §64
|
||
- classification: "모든 것이 여기를 지난다"는 주장은 그 지점의 코드가 아니라 **그것을 지나지 않는 경로의 수**로 검증된다. 기준은 하류 실행기를 직접 부르는 호출자를 세는 것이고, 0이 아니면 그 주장은 성립하지 않는다.
|
||
- scope: guard·interceptor·gateway처럼 "유일한 통로"를 주장하는 모든 컴포넌트. 판정 방법은 하류 타입(gateway·executor)의 참조를 전수로 세고 guard를 지나는 것과 아닌 것을 나누는 것이다.
|
||
- exceptions: 우회 경로가 있어도 그 경로가 **같은 보장을 다른 방식으로** 유지하면 부분적으로 정당하다 — 이 저장소에서 네임스페이스가 그런 경우다. 다만 그 사실을 주장 옆에 적어야 하고, 나머지 여덟 단계는 그렇지 않다.
|
||
- relations: `case:five-adapters-bypass-the-single-admission-point` · `concept:redis-admission-stages` · `reference:check-which-duplicate-is-wired`
|
||
|
||
### OPEN QUESTION — Redis 토폴로지 레인이 실행되지 않아 key spec 드리프트가 확인되지 않았다
|
||
- slug: `redis-topology-lane-not-executed`
|
||
- readiness: `OPEN`
|
||
- source: `final/document.md#6-2` · `analysis/10` §0, §47
|
||
- known: `redisTopologyTest` 레인이 실제 서버에 붙는다 · `RedisCommandMetadataDiff`의 비교 함수가 완성돼 있고 다섯 버킷 테스트가 있다 · 그 둘을 잇는 실행 경로가 없다
|
||
- unknown: 현재 대상 Redis 버전의 실제 `COMMAND DOCS` 출력이 정책 파일과 어떤 차이를 보이는지 · 특히 key spec 이동과 ACL 카테고리 변경이 이미 있는지
|
||
- next-verification: 토폴로지 레인에서 `COMMAND DOCS`/`COMMAND INFO`를 읽어 `RedisCommandMetadataDiff.compare(...)`를 돌리고 결과를 출력한다 — 레인은 이미 서버에 붙어 있고 비교 함수도 있으므로 없는 것은 연결 한 줄이다
|
||
- decision-criterion: diff가 비어 있으면 현재 정책이 서버와 일치함이 확인되고 그 연결을 게이트로 승격할 근거가 된다. 비어 있지 않으면 각 버킷이 새 Case 후보다
|
||
- relations: `case:a-build-gate-that-is-not-in-the-build` · `open-question:container-lanes-not-executed`
|
||
|
||
### DECISION — 분류되지 않은 명령은 fail-closed로 거부한다
|
||
- slug: `unclassified-commands-are-refused`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `analysis/10` §47
|
||
- decision-evidence: `.../RedisCommandCatalog.java`의 `require` 구현과 테스트 `theCatalogFailsClosedForAnUnclassifiedCommand`
|
||
- grounds: `reference:server-metadata-defines-the-command` · `concept:redis-admission-stages`
|
||
- classification: 카탈로그에 분류가 없는 명령은 허용이 아니라 거부다. 그래서 서버가 새 명령을 추가해도 이 SDK를 통해 조용히 나가지 못한다. 이것이 catalog drift 게이트가 없는 상태에서도 한 버킷(미분류 신규 명령)만은 안전한 이유이고, 동시에 나머지 네 버킷은 이 fail-closed가 잡지 못한다는 사실의 근거이기도 하다.
|
||
- relations: `reference:server-metadata-defines-the-command` · `case:a-build-gate-that-is-not-in-the-build`
|
||
|
||
---
|
||
|
||
## TOPIC 12 — http-failure-classification
|
||
|
||
### CONCEPT — 전송 실패의 단계와 범주 — `AttemptStage`와 `FailureCategory`
|
||
- slug: `transport-failure-stage-and-category`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#8-1` · `analysis/11` §51
|
||
- code: `.../httpclient/.../ApacheFailureClassifier.java` · `.../TransportFailure.java` · `.../DefaultRetryEligibilityEngine.java`
|
||
- classification: HTTP 전송 실패를 "어디까지 갔는가"(`AttemptStage`: pool·DNS·CONNECT·TLS·전송·응답)와 "무엇이 실패했는가"(`FailureCategory`)의 두 축으로 모델링하는 구조의 설명이다. 두 축이 함께 `notSent`/`maybeSent` 판정을 만들고, 그것이 재시도 결정표의 입력이 된다 — `TLS_PERMANENT`는 `permanent()`로 절대 차단되고 `CONNECT`는 멱등성과 무관하게 재시도된다. 그래서 **분류가 곧 재시도 안전성**이다. 분류의 입력은 엔진이 남긴 예외 사슬이므로, 사슬 순회 순서와 **사슬에 무엇이 남았는가**가 함께 그 분류를 정한다.
|
||
- missing-verification: 없음 — 분류기와 결정표를 코드로 확인했다
|
||
- relations: `case:a-red-test-misread-as-a-product-defect` · `reference:walk-the-cause-chain-most-specific-wins` · `decision:retry-safety-is-decided-by-evidence`
|
||
|
||
### CASE — 붉은 테스트를 제품 결함으로 읽은 오진 — 듀얼스택 `localhost`가 TLS 실패를 가린다
|
||
- slug: `a-red-test-misread-as-a-product-defect`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#14` · `analysis/11` §51 · `analysis/99-cross-scope.md` §1, §5
|
||
- code: `.../httpclient/.../ApacheFailureClassifier.java` · `.../testkit/MockHttpServer.java:70-72` · `.../security/MutualTlsHandshakeContractTest.java:128-171`
|
||
- evidence: `EVD-332` — 예외 사슬 실측 · 접속 호스트만 바꾼 대조 실험 · `/etc/hosts` 듀얼스택 확인
|
||
- classification: `:adapter:outbound:httpclient:test`가 283 중 3건 실패한다(신뢰불가 CA·만료 인증서·호스트명 불일치). 셋 다 `expected: TLS_HANDSHAKE but was: CONNECT`다. 사이클 1은 `recognize`의 분기 순서(pool → DNS → CONNECT → TLS)를 읽고 "Apache가 TLS 실패를 `HttpHostConnectException`으로 감싸므로 바깥 CONNECT 분기가 안쪽 `SSLHandshakeException`을 가린다"고 결론하고 **P1 제품 결함**으로 올렸다. 사이클 2가 예외 사슬을 실제로 출력하니 사슬은 `ResourceAccessException → HttpHostConnectException("Connection refused")` 둘뿐이고 `SSLHandshakeException`이 아예 없다. 진짜 원인은 `MockHttpServer.uri()`가 호스트명 `localhost`를 돌려주는데 이 컨테이너의 `/etc/hosts`가 `localhost`를 `127.0.0.1`과 `::1` 양쪽에 주고 `MockWebServer`는 IPv4에만 바인딩한다는 것이다. Apache의 다중 주소 연결 루프는 마지막이 아닌 주소의 실패를 삼키므로, 127.0.0.1에서 난 진짜 TLS 실패가 버려지고 `::1`의 연결 거부만 승격된다. 접속 호스트를 `127.0.0.1`로 바꾸면 세 건 모두 `SSLHandshakeException` → `TLS_PERMANENT`가 된다. 핸드셰이크가 성공하는 테스트가 통과하는 이유도 같다 — 첫 주소에서 성공하면 루프가 즉시 반환한다. **판정은 둘로 갈린다.** 픽스처가 호스트명을 쓰는 것은 P3이고, 다중 주소 호스트에서 패밀리별 실패 양상이 다르면 영구 TLS 실패가 재시도 가능한 `CONNECT`로 강등된다는 성질은 P2/기록이며 이 모듈에서는 고칠 수 없다 — 분류기가 볼 수 있는 정보를 엔진이 이미 버렸기 때문이다.
|
||
- missing-verification: `ReactorFailureClassifier`·`JdkFailureClassifier`는 같은 방식으로 실행해 보지 않았다
|
||
- relations: `concept:transport-failure-stage-and-category` · `reference:a-classifier-sees-only-what-the-engine-kept` · `reference:verify-runtime-shape-at-runtime` · `decision:retry-safety-is-decided-by-evidence`
|
||
|
||
### CASE — 로컬 거부 경로에서 회로 브레이커 permission이 반환되지 않는다
|
||
- slug: `a-circuit-breaker-permit-that-leaks-on-local-rejection`
|
||
- readiness: `READY`
|
||
- source: `analysis/11` §22
|
||
- code: `.../httpclient/...`의 회로 브레이커 permit 획득/반환 경로
|
||
- evidence: 없음 — 경로 확인
|
||
- classification: 회로 브레이커 permit을 얻은 뒤 로컬 검증에서 요청이 거부되면 그 permit이 반환되지 않는다. 실패 한 번에 하나씩 줄어드는 형태이고, 같은 저장소의 messaging publish 경로가 명시적으로 막은 것("실패 경로에서 새는 permit은 실패 한 번에 하나씩 줄어들다 아무것도 받지 않게 되는 limiter다")과 같은 결함이다.
|
||
- missing-verification: 반복 호출로 permit 고갈을 재현하지 않았다
|
||
- relations: `concept:transport-failure-stage-and-category` · `reference:check-which-duplicate-is-wired`
|
||
|
||
### REFERENCE — 원인 사슬은 가장 구체적인 분류가 이기도록 순회한다
|
||
- slug: `walk-the-cause-chain-most-specific-wins`
|
||
- readiness: `READY`
|
||
- source: 일반 규칙 — 이 규칙을 처음 끌어낸 사례(`analysis/11` §51)는 사이클 2에서 철회되었다(`EVD-332`). 규칙 자체는 유효하지만, **이 저장소의 `ApacheFailureClassifier`는 그 실패의 사례가 아니다.**
|
||
- classification: 예외 사슬을 바깥에서 안쪽으로 훑으며 처음 인식되는 것을 돌려주면, 라이브러리가 구체적 원인을 일반적 예외로 감쌌을 때 잘못된 분류가 나온다. 기준은 "이 사슬에서 가장 구체적인 분류가 이기는가"이고, 구현은 (a) 구체적 분기를 앞으로 옮기거나 (b) 사슬 전체를 훑어 최선의 매치를 고르는 것이다.
|
||
- scope: 드라이버·클라이언트 예외를 자기 범주로 번역하는 모든 분류기. 특히 전송 계층은 감싸기가 흔하다. `IdentityHashMap` + 최대 깊이로 사이클 안전을 확보하고, `SQLException.getNextException()` 같은 벤더별 곁가지도 따라간다.
|
||
- exceptions: 바깥 예외가 실제로 더 구체적인 경우가 있다 — 그때는 순서가 아니라 우선순위 표가 필요하고, 그 표를 테스트로 고정해야 한다.
|
||
- relations: `case:a-red-test-misread-as-a-product-defect` · `reference:translation-chain-order-is-a-contract`
|
||
|
||
### REFERENCE — 분류기는 엔진이 남긴 것만 볼 수 있다
|
||
- slug: `a-classifier-sees-only-what-the-engine-kept`
|
||
- readiness: `READY`
|
||
- source: `analysis/11` §51.3-51.4 · `EVD-332`
|
||
- classification: 예외를 자기 범주로 번역하는 계층은 그 아래 엔진이 **버리지 않고 남긴 것**만 볼 수 있다. Apache HttpClient 5의 다중 주소 연결 루프는 마지막이 아닌 주소의 실패를 삼키므로, 호스트명이 여러 주소로 풀리면 호출자에게 도달하는 것은 마지막 주소의 오류뿐이다. 분기 순서를 아무리 잘 짜도 사슬에 없는 원인은 분류할 수 없다.
|
||
- scope: 이름 하나가 여러 엔드포인트로 풀리는 모든 클라이언트 — DNS A/AAAA, 서비스 디스커버리, 다중 브로커 부트스트랩. 실패 분류가 재시도 안전성이나 보안 판정으로 이어지는 곳에서 특히 중요하다.
|
||
- exceptions: 모든 주소가 같은 이유로 실패하면 마지막 오류가 대표성을 가지므로 문제가 되지 않는다. 강등은 **패밀리별·엔드포인트별 실패 양상이 다를 때만** 일어난다.
|
||
- relations: `case:a-red-test-misread-as-a-product-defect` · `reference:walk-the-cause-chain-most-specific-wins`
|
||
|
||
### REFERENCE — 런타임의 모양에 대한 주장은 런타임에서 확인한다
|
||
- slug: `verify-runtime-shape-at-runtime`
|
||
- readiness: `READY`
|
||
- source: `analysis/99-cross-scope.md` §5 · `EVD-332` · `EVD-326`
|
||
- classification: "이 라이브러리는 예외를 이렇게 감쌀 것이다", "이 게이트가 이 값을 읽을 것이다", "이 경로가 프로덕션 기본값이다" — 이런 주장은 코드를 읽어서 얻은 **추론**이고, 런타임에서 확인하기 전까지는 가설이다. 사이클 2가 만든 판정 번복 한 건과 자기 교정 세 건은 모두 이 형태였고, 넷 다 측정 하나로 갈렸다. 사슬을 출력하고, 리플렉션으로 private 메서드를 부르고, 조건만 바꿔 대조하는 데 드는 비용은 분 단위다.
|
||
- scope: 프레임워크·드라이버·클라이언트 라이브러리의 런타임 동작에 의존하는 모든 판정. 특히 **실패하는 테스트를 결함의 증거로 읽을 때** — 붉은 테스트는 조사의 시작점이지 결론이 아니다.
|
||
- exceptions: 소스가 저장소 안에 있고 그 경로가 테스트로 고정돼 있으면 읽기로 충분하다. 벤더 코드의 동작에는 해당하지 않는다.
|
||
- relations: `case:a-red-test-misread-as-a-product-defect` · `reference:a-classifier-sees-only-what-the-engine-kept`
|
||
|
||
### REFERENCE — 재시도 가능성은 멱등성과 실패 범주를 함께 봐야 정해진다
|
||
- slug: `retryability-needs-both-idempotency-and-category`
|
||
- readiness: `READY`
|
||
- source: `analysis/11` §51 · `analysis/05` §3.6
|
||
- classification: 실패 범주만으로 재시도를 정하면 비멱등 요청을 재시도하고, 멱등성만으로 정하면 영구 실패를 반복한다. 기준은 두 입력의 곱이다 — 범주가 재시도 가능하고 **동시에** 요청이 재시도 안전할 때만 재시도한다. 그리고 그 판정에 "아무것도 전송되지 않았다"는 증거가 있으면 멱등성 요구가 완화된다.
|
||
- scope: HTTP·gRPC·메시징 클라이언트의 재시도 결정. 이 저장소는 `notSent` 증거를 별도 축으로 두어 그 완화를 표현한다.
|
||
- exceptions: 분류기가 terminal로 표시한 실패는 정책의 화이트리스트로 되살릴 수 없다 — 화이트리스트는 어떤 **범주**가 재시도될 수 있는지를 넓히지 **이 실패**에 대한 판정을 뒤집지 않는다.
|
||
- relations: `concept:transport-failure-stage-and-category` · `reference:translation-chain-order-is-a-contract` · `decision:retry-safety-is-decided-by-evidence`
|
||
|
||
### DECISION — 재시도 안전성은 증거에 기반해 판정한다
|
||
- slug: `retry-safety-is-decided-by-evidence`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-2` · `analysis/11` §51 · `analysis/20` §2.4
|
||
- decision-evidence: `.../httpclient/.../TransportFailure.java`의 `notSent`/`sentNoResponse` 팩토리와 그 javadoc · `.../api/operation/ExecutionEvidence.java`의 네 값과 "A generic engine I/O failure is never upgraded to NOT_SENT" · `docs/adr/ADR-GRPC-003-three-axis-execution-evidence.md`(같은 원칙의 gRPC판)
|
||
- grounds: `concept:transport-failure-stage-and-category` · `reference:retryability-needs-both-idempotency-and-category`
|
||
- classification: 이 모듈의 전체 설계가 "증거에 기반해 재시도 안전성을 판정한다"에 서 있다 — 무엇이 실패했는지가 아니라 **무엇이 관측됐는지**가 판정 입력이다. `NOT_SENT`은 단계 실패가 요청이 서버에 닿지 않았음을 증명할 때만 쓰고, 일반적인 엔진 I/O 실패는 결코 `NOT_SENT`으로 승격되지 않는다. 사이클 2가 §51에서 확인한 것이 이 판정의 취약점을 보여 준다 — 증거를 만드는 계층이 정보를 버리면 이 설계 전체가 틀린 입력 위에 선다.
|
||
- relations: `case:a-red-test-misread-as-a-product-defect` · `decision:three-axes-of-evidence` · `decision:completion-unknown-is-never-retried`
|
||
|
||
---
|
||
|
||
## TOPIC 13 — fileserver-state-and-fencing
|
||
|
||
### CONCEPT — 파일 상태 기계와 READY가 뜻하는 것
|
||
- slug: `file-state-machine-and-ready`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/08` §V1
|
||
- code: `db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql` · `.../persistence/fileserver/FileTransitionRepository.java`
|
||
- classification: 파일이 11개 상태를 지나고 **공개 읽기 가능한 상태는 `READY` 하나뿐**이라는 설계의 설명이다. V1 헤더가 원칙을 적는다 — "파일시스템이 아니라 관계형 레코드가 파일이 공개적으로 읽을 수 있는지 결정한다." 그래서 모든 전이가 `state`와 `version` 둘 다로 가드되고, `READY`의 완전성이 DB CHECK 제약으로 강제된다(`content_key`·`actual_size`·`sha256`·`strong_etag`·`published_at`이 전부 NOT NULL). 그리고 물리 경로·마운트·원본 물리 파일명을 저장하지 않는다 — `content_key`는 서버 생성 opaque key이고 `original_name`은 신뢰할 수 없는 표시용 텍스트다.
|
||
- missing-verification: 컨테이너 레인 미실행
|
||
- relations: `reference:a-publicly-readable-state-must-be-complete-by-constraint` · `decision:no-physical-paths-in-metadata`
|
||
|
||
### CASE — scriptable 콘텐츠 탐지가 BOM·NUL·주석으로 우회된다
|
||
- slug: `scriptable-detection-bypassed-by-a-bom`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/08` §40
|
||
- code: `.../fileserver/.../ScriptableContentPolicy.java`
|
||
- evidence: `evidence/raw/146-fileserver-verification-security-audit-probes.txt`(EXECUTION PROBE 블록)
|
||
- classification: javadoc이 목적을 "Detection is on content, not on the claimed type or the extension, because both are attacker controlled"로 적는데, 구현은 1,024바이트 접두사를 소문자화·`stripLeading()`한 뒤 여섯 마커로 **시작하는지**만 본다. hermetic probe로 실제 판정을 측정하니 셋이 통과한다 — UTF-8 BOM + `<html>` · 선행 HTML 주석 후 `<script>` · NUL 바이트 후 `<html>`. `String.stripLeading()`은 `Character.isWhitespace`만 제거하므로 BOM(U+FEFF)도 NUL도 지우지 않는다. 셋 다 브라우저는 HTML로 렌더링하고, BOM 접두 HTML은 여러 편집기의 기본 출력이다. 형제 검증기와의 대비가 판정을 굳힌다 — `MediaTypeVerifier`의 매직바이트 선두 매칭은 시그니처의 정의가 파일 선두이므로 옳지만, scriptable 마커는 시그니처가 아니라 브라우저가 스니핑하는 패턴이다.
|
||
- missing-verification: 실제 브라우저가 세 입력을 HTML로 렌더링하는지 확인하지 않았다(문서화된 스니핑 동작에 근거)
|
||
- relations: `reference:prefix-matching-fits-signatures-not-sniffing` · `concept:file-state-machine-and-ready`
|
||
|
||
### CASE — cleanup이 읽은 lease와 삭제 사이에 writer가 그 lease를 얻을 수 있었다
|
||
- slug: `a-read-then-delete-race-on-the-upload-lease`
|
||
- readiness: `READY`
|
||
- source: `analysis/08` §V4
|
||
- code: `db/migration/jpa/fileserver/V4__fileserver_upload_terminal_state.sql` 헤더
|
||
- evidence: 없음 — 마이그레이션 헤더의 사후 기록
|
||
- classification: cleanup이 writer lease를 읽고, 없는 것을 확인하고, staging 바이트를 삭제했다. 그 read와 delete 사이에 writer가 바로 그 lease를 획득할 수 있다 — **업로드가 끝났다고 말하는 게 DB에 아무것도 없었으니까** — 그리고 cleanup이 제거한 객체는 업로드가 활발히 append 중이던 것이었다. 해법은 cancel/failed finalize가 cleanup을 큐잉하는 **같은 트랜잭션 안에서** 세션을 `TERMINAL`로 옮기고, acquire/renew/offset commit이 `ACTIVE`를 요구하며, cleanup이 읽은 값으로 판단하는 대신 **조건부 update로 행을 claim**하는 것이다.
|
||
- missing-verification: 컨테이너 레인 미실행
|
||
- relations: `reference:claim-with-a-conditional-update-not-a-read` · `concept:fenced-lease`
|
||
|
||
### CASE — claim이 소유자·토큰·만료를 기록하지 않아 죽은 worker의 항목이 영영 남았다
|
||
- slug: `a-cleanup-claim-without-fencing`
|
||
- readiness: `READY`
|
||
- source: `analysis/08` §V3
|
||
- code: `db/migration/jpa/fileserver/V3__fileserver_fenced_cleanup_lease.sql` 헤더
|
||
- evidence: 없음 — 마이그레이션 헤더의 사후 기록
|
||
- classification: claim이 항목을 `IN_PROGRESS`로 옮기고 **다른 건 아무것도 기록하지 않았다** — owner도, token도, lease expiry도. 두 결과가 나왔다. (1) 물리 delete를 수행하고 DB를 정리하기 전에 죽은 worker가 행을 영원히 `IN_PROGRESS`로 남겼고, 어떤 쿼리도 그것을 살아있는 worker가 활발히 삭제 중인 것과 구별할 수 없어 아무것도 회수하지 않았다. (2) 완료 update가 `cleanup_id`만으로 매칭해, 합리적 lease를 한참 지나 멈춰 있던 worker가 다른 worker가 반쯤 진행한 항목 위에 DONE을 쓸 수 있었다. `NULL` lease를 reaper가 자동 회수하지 않고 운영자에게 넘기는 결정도 함께 기록돼 있다 — "자동 takeover는 아무도 상태를 기록하지 않은 작업에 대해 추측하는 것이다."
|
||
- missing-verification: 컨테이너 레인 미실행
|
||
- relations: `concept:fenced-lease` · `case:a-lease-without-an-owner` · `reference:claim-with-a-conditional-update-not-a-read`
|
||
|
||
### CASE — README가 "노출된 setting도 bean도 없다"고 적은 능력에 production bean 여덟이 있다
|
||
- slug: `readme-said-no-beans-there-are-eight`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/08` §4, §39
|
||
- code: `.../fileserver/README.md` · 해당 능력의 port 구현 여덟과 그 bean 정의
|
||
- evidence: 없음 — README 문장과 bean 목록 대조
|
||
- classification: README가 특정 능력들에 대해 "노출된 setting도 bean도 없다"고 적는데, 여덟 개의 port 구현과 여덟 개의 bean이 있다. 방향이 **과소 진술**이라 위험이 과대보다 낮지만, 이 문서를 읽고 "그 능력은 아직 없다"고 판단한 팀이 같은 것을 다시 만들 수 있다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:fix-overstatement-before-understatement` · `reference:numbers-in-docs-should-be-derived`
|
||
|
||
### REFERENCE — 공개 읽기 가능한 상태는 완전한 identity를 DB 제약으로 요구한다
|
||
- slug: `a-publicly-readable-state-must-be-complete-by-constraint`
|
||
- readiness: `READY`
|
||
- source: `analysis/08` §V1
|
||
- classification: "이 상태의 행은 반드시 이 필드들을 갖는다"를 애플리케이션 코드로만 두면 어떤 경로가 그것을 어겼는지 알 수 없다. 부분 CHECK 제약(`state <> 'READY' OR (… IS NOT NULL …)`)으로 두면 그 상태에 도달하는 모든 경로가 강제된다. 기준은 "이 상태가 외부에 무엇을 약속하는가"이고, 약속이 있으면 제약으로 적는다.
|
||
- scope: 공개·발행·활성 같은 terminal 상태를 갖는 모든 상태 기계. 같은 형태가 messaging outbox delivery의 `ck_..._state_shape`/`ck_..._terminal_shape`에도 있다.
|
||
- exceptions: 제약이 마이그레이션 시점에 기존 행을 재검증하므로, 이미 데이터가 있는 테이블에는 `NOT VALID`로 추가해 앞으로의 쓰기에만 적용하는 선택지가 있다 — 그때는 기존 행의 정합성을 별도로 확인해야 한다.
|
||
- relations: `concept:file-state-machine-and-ready` · `reference:cas-tuple-in-the-where-clause`
|
||
|
||
### REFERENCE — 조건부 update로 행을 claim하고 읽은 값으로 판단하지 않는다
|
||
- slug: `claim-with-a-conditional-update-not-a-read`
|
||
- readiness: `READY`
|
||
- source: `analysis/08` §V4 · `analysis/05` §10
|
||
- classification: 읽어서 확인하고 행동하면 그 사이에 상태가 바뀔 수 있다. 조건부 update가 행을 claim하면 update count가 곧 "내가 얻었는가"의 답이 된다. 기준은 "이 판단과 그에 따른 행동 사이에 다른 참여자가 끼어들 수 있는가"다.
|
||
- scope: cleanup·relay·worker처럼 경쟁 참여자가 있는 모든 작업. 물리 자원(파일·객체)을 지우는 경로에서 특히 결정적이다 — 되돌릴 수 없기 때문이다.
|
||
- exceptions: 단일 참여자가 구조적으로 보장되면 불필요하다. 다만 그 보장이 배포 형태에서 오면 다중 인스턴스로 가는 날 깨진다.
|
||
- relations: `case:a-read-then-delete-race-on-the-upload-lease` · `case:a-cleanup-claim-without-fencing` · `reference:cas-tuple-in-the-where-clause`
|
||
|
||
### REFERENCE — 접두사 시작 매칭은 시그니처에는 맞고 스니핑 패턴에는 맞지 않는다
|
||
- slug: `prefix-matching-fits-signatures-not-sniffing`
|
||
- readiness: `READY`
|
||
- source: `analysis/08` §40
|
||
- classification: 매직바이트는 정의상 파일 선두에 있으므로 접두사 시작 비교가 정확하다. 브라우저가 스니핑하는 패턴은 선두 고정이 아니므로 같은 비교가 우회 가능하다. 기준은 "이 패턴의 정의가 위치를 포함하는가"이고, 아니면 정규화(BOM·NUL·주석 제거) 후 탐색이거나 범위 스캔이어야 한다.
|
||
- scope: 콘텐츠 기반 탐지 전반 — 스크립트 탐지·MIME 스니핑 방어·서명 확인. 정규화 대상은 최소한 BOM(U+FEFF)·NUL·선행 주석이다.
|
||
- exceptions: 시그니처 검증(매직바이트)은 선두 고정이 옳고 정규화하면 오히려 틀린다.
|
||
- relations: `case:scriptable-detection-bypassed-by-a-bom` · `reference:reject-rather-than-sanitize`
|
||
|
||
### DECISION — 물리 경로와 원본 파일명을 저장하지 않는다
|
||
- slug: `no-physical-paths-in-metadata`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `analysis/08` §V1
|
||
- decision-evidence: `db/migration/jpa/fileserver/V1__create_fileserver_metadata.sql` 헤더가 결정과 근거를 적는다
|
||
- grounds: `concept:file-state-machine-and-ready` · `reference:a-publicly-readable-state-must-be-complete-by-constraint`
|
||
- classification: 물리 경로·마운트·원본 물리 파일명을 저장하지 않고 `content_key`(서버 생성 opaque)와 `original_name`(신뢰할 수 없는 표시용 텍스트)만 둔다. 그래서 저장 위치를 바꿔도 메타데이터가 바뀌지 않고, 클라이언트가 준 이름이 경로로 해석될 여지가 없다.
|
||
- relations: `concept:file-state-machine-and-ready`
|
||
|
||
---
|
||
|
||
## TOPIC 14 — objectstorage-staged-lifecycle
|
||
|
||
### CONCEPT — staged lifecycle과 서명된 grant
|
||
- slug: `staged-lifecycle-and-signed-grants`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/09` · `analysis/03` §10
|
||
- code: `.../objectstorage/...`의 staged 업로드 경로와 grant 서명·검증
|
||
- classification: 객체가 staging에서 관리 네임스페이스로 옮겨지는 단계와, 각 단계의 권한이 서명된 grant로 표현되는 구조의 설명이다. opaque identity(클라이언트가 물리 위치를 알 수 없음), privilege separation(업로드 자격과 발행 자격이 다름), 그리고 grant가 endpoint·유효창·operation을 함께 바인딩하는 이유를 다룬다. `application-core`가 이 수명주기를 port로 소유하고 어댑터가 벤더별 구현만 갖는 경계도 함께 설명한다.
|
||
- missing-verification: 컨테이너 레인 미실행
|
||
- relations: `case:apply-is-a-setting-approval-is-not` · `case:the-last-part-cannot-get-a-grant` · `reference:the-powerful-half-must-not-be-one-setting-away`
|
||
|
||
### CASE — APPLY를 켜는 설정은 있고 승인을 검증하는 bean은 없다
|
||
- slug: `apply-is-a-setting-approval-is-not`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-4` · `analysis/09` §49
|
||
- code: `.../objectstorage/.../ObjectStorageLegacyMigrationConfig.java:30` · `.../Ed25519LegacyAdoptionApprovalVerifier.java` · `.../LegacyObjectAdoptionService.requireApproval`
|
||
- evidence: `evidence/raw/155-objectstorage-platform-readiness-probes.txt` §8.1
|
||
- classification: legacy 채택은 이 leaf에서 가장 권한이 센 동작이다(원시 locator로 legacy를 읽어 관리 네임스페이스에 **발행**). 설계가 detached 2인 승인을 요구하고 검증기 구현은 촘촘하다 — 서로 다른 두 승인자의 Ed25519 서명, destination/epoch/operationId/manifest 다이제스트 일치, 유효창 7일 이내, 그리고 코덱이 **decode 후 재인코딩해 원본 바이트와 같아야만** 통과(비정규 인코딩 거부). 그런데 조립이 비대칭이다 — APPLY 실행 경로는 설정 두 줄로 켜지고 bean이 만들어지는데, 검증기를 `main`에서 생성하는 코드가 저장소 전체에 0이고 신뢰 승인자 키·키 id·유효기간에 해당하는 설정 항목이 하나도 없다. 어댑터 쪽 `requireApproval`은 **필드 동등성만** 검사하고 서명은 보지 않는다.
|
||
- missing-verification: APPLY 모드를 실제로 켜서 어떤 실패가 나는지 관측하지 않았다
|
||
- relations: `reference:the-powerful-half-must-not-be-one-setting-away` · `reference:a-binary-approval-codec-must-round-trip` · `reference:a-bean-is-not-composition-evidence`
|
||
|
||
### CASE — 직접 multipart의 마지막 part는 grant를 받을 수 없다
|
||
- slug: `the-last-part-cannot-get-a-grant`
|
||
- readiness: `READY`
|
||
- source: `analysis/09` §38
|
||
- code: `.../objectstorage/...`의 multipart grant 발급 경로
|
||
- evidence: 없음 — 경로 확인
|
||
- classification: 직접 multipart 업로드에서 마지막 part가 grant를 받을 수 없는 경로 결함이다. grant가 업로드 단계별로 발급되는 구조인데 마지막 part의 조건이 그 발급 경로를 벗어난다.
|
||
- missing-verification: 실제 multipart 업로드를 수행해 재현하지 않았다
|
||
- relations: `concept:staged-lifecycle-and-signed-grants` · `case:endpoint-check-only-on-upload`
|
||
|
||
### CASE — 서명된 grant의 endpoint 검증이 upload 경로에만 있다
|
||
- slug: `endpoint-check-only-on-upload`
|
||
- readiness: `READY`
|
||
- source: `analysis/09` §39
|
||
- code: `.../objectstorage/...`의 grant 검증 지점들
|
||
- evidence: 없음 — 검증 지점 전수 확인
|
||
- classification: grant가 endpoint를 바인딩하는데 그 검증이 upload 경로에만 있고 다른 경로에는 없다. grant를 발급받은 주체가 다른 endpoint로 그것을 쓸 수 있다는 뜻이고, 서명이 endpoint를 덮는 이유가 무력화된다.
|
||
- missing-verification: 다른 endpoint로 grant를 제시해 재현하지 않았다
|
||
- relations: `concept:staged-lifecycle-and-signed-grants` · `case:the-last-part-cannot-get-a-grant`
|
||
|
||
### REFERENCE — 권한이 센 절반이 설정 한 줄로 켜지면 안 된다
|
||
- slug: `the-powerful-half-must-not-be-one-setting-away`
|
||
- readiness: `READY`
|
||
- source: `analysis/09` §49
|
||
- classification: 한 능력이 "실행하는 절반"과 "승인하는 절반"으로 나뉠 때, 실행 쪽이 설정으로 켜지고 승인 쪽이 손으로 배선해야 하면 비대칭이 위험 방향으로 기운다. 기준은 "이 설정을 켠 배포에서 승인 검증이 없으면 무엇이 일어나는가"이고, 답이 "실행된다"면 그 설정이 승인 bean의 부재를 startup 실패로 만들어야 한다.
|
||
- scope: 승인·서명·2인 통제를 요구하는 모든 관리 작업. 이 저장소의 admin plane이 반대 사례다 — 파괴적 작업의 bean을 **아예 자동설정하지 않고** 그 부재를 javadoc으로 문서화한다.
|
||
- exceptions: 승인 검증을 애플리케이션 계층이 소유하는 설계면 어댑터에 bean이 없는 것이 옳다. 다만 그때는 어댑터가 승인 없이 실행되는 경로를 갖지 않아야 하고, 이 저장소에서는 어댑터의 검사가 필드 동등성뿐이다.
|
||
- relations: `case:apply-is-a-setting-approval-is-not` · `reference:a-bean-is-not-composition-evidence`
|
||
|
||
### REFERENCE — 이진 승인 코덱은 decode 후 재인코딩이 원본과 같아야 한다
|
||
- slug: `a-binary-approval-codec-must-round-trip`
|
||
- readiness: `READY`
|
||
- source: `analysis/09` §49
|
||
- classification: 서명 대상이 이진 인코딩이면 같은 논리 값에 대해 여러 바이트 표현이 가능할 때 서명 우회가 생긴다. decode한 뒤 재인코딩해 원본 바이트와 동일한지 확인하면 비정규 인코딩이 거부된다. 기준은 "이 인코딩이 canonical인가"이고, 보장이 없으면 round-trip 검사가 그 자리다.
|
||
- scope: 서명·MAC의 대상이 되는 모든 이진·텍스트 인코딩. 길이 프레이밍이 함께 필요한 이유도 같은 축이다.
|
||
- exceptions: 인코딩이 정의상 canonical이면(고정 길이 필드만) 불필요하다. 다만 필드가 하나라도 가변이면 성립하지 않는다.
|
||
- relations: `case:apply-is-a-setting-approval-is-not` · `reference:digest-must-be-length-framed-and-versioned` · `concept:signed-cursor-structure`
|
||
|
||
### DECISION — legacy 채택은 서로 다른 두 승인자의 서명을 요구한다
|
||
- slug: `legacy-adoption-requires-two-approvers`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `analysis/09` §49
|
||
- decision-evidence: `.../Ed25519LegacyAdoptionApprovalVerifier.java`의 구현과 그 테스트(`verifiesCanonicalExactBindingWithTwoDistinctTrustedApprovers`, `rejectsDuplicateApproverAndAnyBindingTamper`) · `sample-portfolio`의 `AdoptLegacyPosterImageUseCase.authorizeApply`가 보여 주는 의도된 조립
|
||
- grounds: `concept:staged-lifecycle-and-signed-grants` · `reference:the-powerful-half-must-not-be-one-setting-away`
|
||
- classification: 원시 locator로 legacy 네임스페이스를 읽어 관리 네임스페이스에 발행하는 동작에 detached 2인 승인을 요구한다. 검증기가 서로 다른 두 승인자를 요구하고(중복 승인자 거부), destination·epoch·operationId·manifest·네임스페이스 다이제스트를 요청과 대조하며, 유효창을 최대 7일로 제한한다. 의도된 조립은 use case가 **호출자가 준 approval을 버리고** 검증 결과로 요청을 다시 만드는 것 — "application에서 검증하고 adapter에서 적용한다". 그 use case가 어디에도 배선돼 있지 않다는 점이 `case:apply-is-a-setting-approval-is-not`이다.
|
||
- relations: `case:apply-is-a-setting-approval-is-not` · `reference:a-binary-approval-codec-must-round-trip`
|
||
|
||
---
|
||
|
||
## TOPIC 15 — self-disclosure-grading
|
||
|
||
### CONCEPT — 네 단계 공시 등급 — modelled에서 production-verified까지
|
||
- slug: `four-grade-disclosure`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-5` · `analysis/16` §45.2, §45.3
|
||
- code: `src/adapter/inbound/graphql/CLAUDE.md`의 등급 정의와 13행 표
|
||
- classification: graphql leaf가 자기 능력을 네 등급으로 공시하는 체계의 설명이다 — `modelled`(타입은 있고 조립 없음) · `wired`(조립됨) · `integration-verified`(통합 테스트로 확인) · `production-verified`(운영 관측). 그리고 규칙 하나 — "현재 등급보다 높게 표현하지 않는다". 이 체계가 왜 작동했는지가 핵심이다 — 13행 중 일곱을 **스스로 강등**했고, 이 분석이 파일 단위 배선 데이터와 전수 대조했을 때 12행이 일치했다. 타입 이름이 등급을 인코딩하는 관행(`*Admission` — "Spring transport handler는 없다, 그래서 타입 이름도 `*Admission`이다")도 이 개념의 일부다.
|
||
- missing-verification: 없음 — 13행 전수 대조
|
||
- relations: `case:seven-rows-downgraded-by-the-family-itself` · `reference:grades-may-understate-never-overstate` · `decision:capability-grade-is-declared-not-inferred`
|
||
|
||
### CASE — 등급표 13행 중 일곱을 스스로 강등하고 한 행만 관측과 어긋났다
|
||
- slug: `seven-rows-downgraded-by-the-family-itself`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-5` · `analysis/16` §20.1, §29.1, §45.2
|
||
- code: `src/adapter/inbound/graphql/CLAUDE.md` 등급표 · `.../graphql/http/...`(해당 행의 대상 타입)
|
||
- evidence: `evidence/raw/213-inbound-graphql-release-probes.txt`
|
||
- classification: `CLAUDE.md`가 4등급을 정의하고 "현재 등급보다 높게 표현하지 않는다"를 규칙으로 선언한 뒤 13행 중 일곱을 스스로 `modelled`로 강등했다. sub-scope 02~06의 파일 단위 배선 데이터와 13행을 전수 대조한 결과 **12행 일치**, `요청 크기/Accept 협상 (http/)` 한 행만 불일치이고 **어긋나는 방향이 표가 금지한 "높게 표현" 쪽**이다 — 인용된 두 증거가 endpoint 테스트가 아닌 순수 단위 테스트이고 대상 타입은 미배선이다. 그리고 기계 매니페스트(`GrpcStableCapabilityManifest`에 해당하는 graphql 쪽)와 사람이 읽는 등급표가 커서에 대해 서로 다른 답을 준다.
|
||
- missing-verification: 없음 — 13행 전수 대조
|
||
- relations: `concept:four-grade-disclosure` · `reference:grades-may-understate-never-overstate` · `case:a-startup-validator-that-requires-a-key-nothing-signs-with`
|
||
|
||
### CASE — 담금 임계값이 열거형에 없는 등급을 위해 쓰여 중간 등급이 상위 등급보다 어려워졌다
|
||
- slug: `a-soak-threshold-written-for-a-grade-that-does-not-exist`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md` §17.4
|
||
- code: `.../grpc-advanced-bootstrap/.../release/GrpcAdvancedPromotionGate.java` · `.../bootstrap/GrpcCapabilityGrade.java` · `.../release/GrpcAdvancedPromotionGateTest.java`
|
||
- evidence: 없음 — `evaluate` 본문과 열거형 값 집합, 두 테스트가 고른 숫자로 판정했다
|
||
- classification: 게이트 javadoc 의 모형은 등급 둘이다 — Advanced Stable(7일)과 "Stable default"(30일). 상수도 둘이다. 그런데 `GrpcCapabilityGrade` 의 값은 `ADVANCED_STABLE`·`EXPERIMENTAL`·`WATCH`·`DISABLED` 넷이고 "Stable default" 는 없다. 선택이 `to == ADVANCED_STABLE ? 7일 : 30일` 이므로 **존재하지 않는 등급을 위해 만든 갈래가 존재하는 세 등급을 삼킨다.** 증거 일곱 항목도 목표 등급과 무관하게 요구되므로 결과가 뒤집힌다 — `EXPERIMENTAL → ADVANCED_STABLE` 은 일곱 + 7일, `WATCH → EXPERIMENTAL` 은 일곱 + 30일. `WATCH` 가 밟도록 강제된 유일한 첫 걸음(같은 메서드의 셋째 차단 사유가 그렇게 적는다)이 상위 등급보다 엄격하고, `WATCH` 의 뜻이 "추적할 뿐 구현되지 않았다" 이므로 담금 기록이 가장 적을 등급에 가장 긴 담금을 요구한다. 테스트가 그 뒤틀림을 그대로 보여 준다 — 30일 갈래를 실행하려고 고른 전이가 `ADVANCED_STABLE → DISABLED`(철회)인데 이름은 "becoming a Stable default" 이고, `WATCH → EXPERIMENTAL` 테스트는 담금을 60일로 줘서 30일 요구가 레인에 걸리지 않는다.
|
||
- missing-verification: 테스트를 실행하지 않았다 — 두 테스트가 고른 숫자와 갈래 조건으로 판정했다
|
||
- relations: `reference:grades-may-understate-never-overstate` · `decision:capability-grade-is-declared-not-inferred` · `case:test-names-that-assert-what-their-bodies-do-not`
|
||
|
||
### CASE — 시작 검증기가 커서 서명 키를 요구하는데 그 키로 서명하는 코드가 없다
|
||
- slug: `a-startup-validator-that-requires-a-key-nothing-signs-with`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-1` · `analysis/16` §24.1
|
||
- code: `.../graphql/.../HmacGraphQlCursorCodec.java` · `.../GraphQlCursorKeyRing.java` · 시작 검증기
|
||
- evidence: `evidence/raw/212-inbound-graphql-data-probes.txt`
|
||
- classification: 시작 검증기가 "unsigned cursors are client-editable"로 프로덕션을 거부하며 커서 서명 키를 요구하고, 설정이 그 키를 받고, 액추에이터가 그것을 확인해 준다 — **세 신호가 전부 긍정**이다. 그런데 그 키로 서명하는 코드가 없다(`HmacGraphQlCursorCodec`·`GraphQlCursorKeyRing` 배선 0). 저장소 자신의 테스트가 이 상태를 알고 있다 — `GraphQlPolicyRequestPathTest`(GQL-INT-003)가 javadoc으로 기록하고 "this test fails the moment somebody wires one half without the other"로 고정한다. 현재 어떤 feature에도 페이지네이션이 붙지 않아 조작 대상 커서 자체가 없다는 것이 위험을 낮춘다.
|
||
- missing-verification: 부팅해서 세 신호를 직접 관측하지 않았다
|
||
- relations: `concept:four-grade-disclosure` · `reference:a-bean-is-not-composition-evidence` · `concept:signed-cursor-structure`
|
||
|
||
### CASE — build-only 등급이 90개 파일의 미조립을 오늘의 사고에서 면제한다
|
||
- slug: `build-only-exempts-ninety-files-from-todays-incident`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#8-3` · `analysis/17` §4.1, §26.6
|
||
- code: `src/config/architecture/modules.json`(`adapter-inbound-websocket`의 `runtime_memberships: []`) · `.../ConditionalTransportCompositionContractTest.java`의 `BUILD_ONLY_TRANSPORTS`
|
||
- evidence: `evidence/raw/228-app-bootstrap-activation-probes.txt`
|
||
- classification: websocket leaf의 `backend.websocket` 플랫폼 약 90개 main 파일에 조립 지점이 없고 세 안전 장치(`WebSocketPlatformStartupValidator` 125줄 · `WebSocketStackExclusivity` 78줄 · settings의 safe-default 규약)가 전부 호출자 0이다. 처음에 P1로 기록했다가 `runtime_memberships=[]`가 기계로 강제되는 build-only 등급임을 확인하고 P2로 내렸다 — 그 실패 시나리오가 현재 출하되는 두 조합 어디에서도 발생할 수 없다. 등급이 심각도를 바꾸되 발견을 없애지는 않는다는 것이 이 Case의 요점이다.
|
||
- missing-verification: 없음
|
||
- relations: `reference:runtime-membership-decides-severity` · `reference:grades-may-understate-never-overstate` · `decision:grpc-stays-build-only-until-the-bridge-is-decided`
|
||
|
||
### REFERENCE — 등급은 네 단계로 나누고 관측보다 높게 적지 않는다
|
||
- slug: `grades-may-understate-never-overstate`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#7-5` · `analysis/16` §45.3 · `analysis/20` §2.6
|
||
- classification: 능력 등급표의 각 행은 "이 능력이 어디까지 검증됐는가"를 말한다. 낮게 적는 것은 보수적이고 안전하지만 높게 적으면 그 행을 읽은 팀이 검증되지 않은 것에 의존한다. 네 단계(`modelled`/`wired`/`integration-verified`/`production-verified`)가 유용한 이유는 "구현됐다"와 "조립됐다"와 "검증됐다"가 다른 사실이기 때문이다.
|
||
- scope: capability 등급표·지원 매트릭스·readiness card. 기계가 소비하는 매니페스트가 따로 있으면 둘 사이의 불일치도 같은 규칙으로 판정한다.
|
||
- exceptions: build-only 가족은 전부 `modelled` 근처에 머무는 것이 정상이고 그 자체가 결함이 아니다 — 다만 그 사실을 운영 문서가 말해야 한다.
|
||
- relations: `concept:four-grade-disclosure` · `case:seven-rows-downgraded-by-the-family-itself` · `reference:runtime-membership-decides-severity`
|
||
|
||
### REFERENCE — `runtime_memberships`를 먼저 읽고 심각도를 정한다
|
||
- slug: `runtime-membership-decides-severity`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#9` 규칙 1 · `analysis/17` §26.6 · `analysis/19` §1.1 · `analysis/20` §1.1
|
||
- classification: 같은 미조립도 그 leaf가 런타임에 오르는지에 따라 "오늘의 사고"와 "채택 시점의 부채"로 갈린다. 기준은 레지스트리의 `runtime_memberships`이고, 그것이 기계로 강제되면(`verifyRuntimeModuleMembership`, 전송별 contract test) 판정의 근거가 된다.
|
||
- scope: 모듈 레지스트리를 갖는 모든 멀티모듈 프로젝트. 이 저장소는 62 leaf 중 27이 build-only(729 파일 / main 479)다.
|
||
- exceptions: build-only leaf라도 **조립되는 소수의 bean**이 있으면 그 부분은 오늘의 문제다 — gRPC의 `GrpcAdmissionController`가 9개 bean 중 하나인 경우가 그것이다.
|
||
- relations: `case:build-only-exempts-ninety-files-from-todays-incident` · `decision:grpc-stays-build-only-until-the-bridge-is-decided` · `reference:grades-may-understate-never-overstate`
|
||
|
||
### DECISION — 능력 등급은 코드가 아니라 실행된 증거에서 파생한다
|
||
- slug: `grades-derive-from-executed-evidence`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-5` · `analysis/19` §6.6 · `analysis/16` §45.3
|
||
- decision-evidence: `CompatibilityMatrix.Entry.hasLiveBrokerCertification()`의 파생 구현과 javadoc · graphql `CLAUDE.md`의 등급 규칙 · `docs/adr/ADR-GRPC-ADV-001-capability-promotion-is-per-capability.md`
|
||
- grounds: `concept:four-grade-disclosure` · `reference:agreement-between-documents-proves-nothing`
|
||
- classification: 등급을 선언된 boolean이 아니라 레인이 낸 증거에서 계산한다. messaging이 그 전환을 명시적으로 기록한다 — "As a field it was a boolean an author set next to the tier, and RabbitMQ carried `true` while no fault scenario had ever been executed against it." graphql은 사람이 읽는 표에 같은 규칙을 적용해 일곱 행을 강등했고, gRPC는 capability별 promotion을 ADR로 고정했다.
|
||
- relations: `decision:capability-grade-is-declared-not-inferred` · `concept:four-grade-disclosure`
|
||
|
||
---
|
||
|
||
## TOPIC 16 — multitenancy-isolation
|
||
|
||
### CONCEPT — RLS가 성립하기 위한 세 전제
|
||
- slug: `rls-three-preconditions`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §13.1
|
||
- code: `db/experimental-rls/V1__tenant_rls.sql` · `.../experimental/multitenancy/RlsPolicyVerifier.java` · `.../RlsTenantSessionBinder.java`
|
||
- classification: PostgreSQL RLS가 실제로 격리하려면 세 가지가 동시에 참이어야 한다는 설명이다. (1) `ENABLE ROW LEVEL SECURITY` — 테이블에 policy를 켠다. (2) `FORCE ROW LEVEL SECURITY` — **테이블 OWNER에게도** 적용한다. 없으면 owner는 자기 policy에서 면제되고 **owner는 흔히 마이그레이션 롤이며 그것이 사람들이 테스트하는 롤이다.** (3) 런타임 롤이 `BYPASSRLS`를 갖지 않는다 — 이것은 테이블 속성이 아니라 롤 속성이라 startup에서 assert해야 한다. policy가 `current_setting('app.tenant_id', true)`를 쓰는 이유(두 번째 인자 `true`가 미설정 시 raise 대신 NULL을 반환하고, NULL은 절대 `tenant_id`와 같지 않으므로 fail-closed)도 함께 다룬다.
|
||
- missing-verification: 실제 RLS 환경을 세워 세 전제를 하나씩 깨보지 않았다
|
||
- relations: `case:three-ways-rls-does-nothing` · `reference:hibernate-filter-is-not-a-security-boundary` · `reference:isolation-settings-must-be-transaction-local`
|
||
|
||
### CONCEPT — 네 가지 멀티테넌시 전략과 각각의 격리 경계
|
||
- slug: `four-multitenancy-strategies`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §13.2
|
||
- code: `.../experimental/multitenancy/`의 column·rls·schema·database 구현
|
||
- classification: 이 저장소가 네 전략(discriminator column · RLS · schema-per-tenant · database-per-tenant)을 각각 플래그 뒤에 두고 구현한 구조의 설명이다. 각 전략의 격리 경계가 다르고 실패 모드도 다르다 — column은 predicate 누락이 곧 유출, RLS는 세 전제(§concept:rls-three-preconditions), schema는 `search_path` 잔존, database는 커넥션 예산의 곱셈. `TenantId`의 정규식이 네 전략 전부의 공통 기반인 이유(스키마 이름·`set_config` 값·라우팅 키에 들어가므로 `../public` 같은 값이 path-traversal이 된다)도 다룬다.
|
||
- missing-verification: 네 전략 중 어느 것도 실제로 켜서 관측하지 않았다 — 전부 experimental 플래그 뒤에 있다
|
||
- relations: `concept:rls-three-preconditions` · `case:tenant-pools-summed-past-the-server-ceiling` · `decision:classpath-presence-is-not-consent`
|
||
|
||
### CASE — RLS가 아무것도 하지 않는 세 가지 방법
|
||
- slug: `three-ways-rls-does-nothing`
|
||
- readiness: `READY`
|
||
- source: `final/document.md#4-1` · `analysis/05` §13.1, §17 P8
|
||
- code: `.../experimental/multitenancy/RlsPolicyVerifier.java` · `db/experimental-rls/V1__tenant_rls.sql`
|
||
- evidence: 없음 — verifier가 검사하는 세 조건과 마이그레이션 주석
|
||
- classification: `RlsPolicyVerifier`가 RLS가 조용히 무력화되는 세 경로를 전부 확인한다 — policy 없음/RLS 미활성 · 런타임 롤이 `BYPASSRLS` 보유 · **런타임 롤이 테이블을 소유**(FORCE 없으면 면제). 세 번째가 가장 놓치기 쉽다. 그리고 unique index 하나에도 같은 축의 주석이 있다 — tenant 격리가 컬럼에 의존하면 그 테이블의 **모든 uniqueness 요구**에 그 컬럼이 들어가야 하고, `(value)`만의 unique index는 다른 tenant가 그 값을 썼다는 이유로 insert를 실패시켜 **버그이자 정보 유출**이 된다. 다만 `analysis/05` §97이 기록하듯 verifier 자신이 "반드시 보호돼야 하는 table"의 부재를 성공으로 인정하는 결함을 갖는다.
|
||
- missing-verification: 세 조건을 하나씩 깨서 verifier가 실제로 잡는지 확인하지 않았다
|
||
- relations: `concept:rls-three-preconditions` · `reference:hibernate-filter-is-not-a-security-boundary` · `reference:tenant-column-belongs-in-every-unique-constraint`
|
||
|
||
### CASE — `search_path`가 풀로 돌아간 커넥션에 남아 다음 tenant가 상속한다
|
||
- slug: `search-path-survived-the-return-to-the-pool`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §13.2
|
||
- code: `.../experimental/multitenancy/SchemaMultiTenantConnectionProvider.java`의 reset 구현과 javadoc
|
||
- evidence: 없음 — 구현과 javadoc
|
||
- classification: `search_path`는 세션 설정이라 풀로 돌아간 커넥션이 여전히 마지막 tenant의 스키마를 들고 있다. 다음 borrower — 아마 다른 tenant, 또는 tenant 없는 백그라운드 job — 가 **어떤 statement도 틀리지 않은 채로** 거기서 읽고 쓴다. 해법은 반환 시 `NEUTRAL_SCHEMA = "pg_catalog"`로 되돌리는 것이다. 같은 성질이 H2의 로컬 타임아웃 설정에도 있고(세션 스코프라 트랜잭션이 끝나도 남는다) 그쪽은 매 트랜잭션 전 재적용으로 실무상 가려진다.
|
||
- missing-verification: 풀 재사용에서 잔존을 실제로 관측하지 않았다
|
||
- relations: `reference:isolation-settings-must-be-transaction-local` · `reference:session-scoped-settings-outlive-the-transaction` · `concept:four-multitenancy-strategies`
|
||
|
||
### CASE — tenant별 풀이 개별적으로 합리적이고 그 합이 서버 상한을 넘긴다
|
||
- slug: `tenant-pools-summed-past-the-server-ceiling`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §13.2
|
||
- code: `.../experimental/multitenancy/TenantPoolBudget.java`의 두 ceiling과 javadoc
|
||
- evidence: 없음 — 구현과 javadoc
|
||
- classification: database-per-tenant는 특정한 방식으로 실패한다 — 각 tenant의 풀은 개별적으로 합리적이고 **그 합이 아니다.** 50 tenant × 10 커넥션 = `max_connections`가 100인 서버에 500 커넥션이고, 실패는 **idle이던 것 포함 모든 tenant에 동시에** connection refusal로 도착한다. `TenantPoolBudget`이 두 ceiling(풀 개수와 커넥션 총합)을 갖는 이유도 적혀 있다 — pool count만으로는 풀 크기 차이를 무시하고, connection total만으로는 각자 스레드와 모니터링을 가진 무한한 수의 작은 풀을 허용한다. 다만 `analysis/05` §98이 기록하듯 예산이 **새 pool 크기를 계산하지 않아** ceiling을 넘길 수 있다.
|
||
- missing-verification: 다중 tenant 풀을 실제로 세워 상한 초과를 재현하지 않았다
|
||
- relations: `concept:requires-new-connection-cost` · `concept:four-multitenancy-strategies` · `reference:deadline-narrows-in-three-stages`
|
||
|
||
### REFERENCE — Hibernate filter는 보안 경계가 아니다
|
||
- slug: `hibernate-filter-is-not-a-security-boundary`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §13.2
|
||
- classification: Hibernate filter는 엔티티 쿼리에 적용되고 native SQL·bulk DML·`getReference`·L2 캐시를 통해 도달하는 것에는 적용되지 않는다. 그래서 filter만 믿는 설계는 다른 tenant의 행으로 가는 경로 여러 개를 열어 둔다. 기준은 "이 격리를 우회하는 경로가 몇 개인가"이고, ORM 기능은 그 답이 항상 0이 아니다.
|
||
- scope: tenant·소유자·가시성 격리 전반. 실제 경계는 DB(RLS)나 별도 가드(`TenantAwareRepositoryGuard`)여야 하고, filter는 편의로만 쓴다.
|
||
- exceptions: 읽기 경로만 있고 native·bulk가 구조적으로 금지된 좁은 컨텍스트면 filter로 충분할 수 있다. 다만 그 금지를 ArchUnit 같은 것이 강제해야 하고, 강제가 없으면 전제가 유지되지 않는다.
|
||
- relations: `concept:rls-three-preconditions` · `case:three-ways-rls-does-nothing` · `reference:tenant-column-belongs-in-every-unique-constraint`
|
||
|
||
### REFERENCE — tenant 컬럼이 있는 테이블의 모든 unique 제약에 그 컬럼이 들어가야 한다
|
||
- slug: `tenant-column-belongs-in-every-unique-constraint`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §13.1
|
||
- classification: tenant 격리가 컬럼에 의존하면 그 테이블의 모든 uniqueness 요구가 그 컬럼을 포함해야 한다. `(value)`만의 unique index는 다른 tenant가 그 값을 이미 썼다는 이유로 한 tenant의 insert를 실패시키는데, 그것은 **버그이자 정보 유출**이다 — 존재하지 않아야 할 행의 존재를 알려준다.
|
||
- scope: discriminator column 전략을 쓰는 모든 테이블. 같은 논리가 partial unique index와 exclusion constraint에도 적용된다.
|
||
- exceptions: 전역적으로 유일해야 하는 값(외부 시스템의 식별자)은 tenant를 포함하지 않는 것이 맞다. 다만 그때는 그 값이 tenant 간에 노출되는 것이 의도임을 적어야 한다.
|
||
- relations: `case:three-ways-rls-does-nothing` · `reference:hibernate-filter-is-not-a-security-boundary`
|
||
|
||
### REFERENCE — 격리 설정은 트랜잭션 로컬이어야 한다
|
||
- slug: `isolation-settings-must-be-transaction-local`
|
||
- readiness: `READY`
|
||
- source: `analysis/05` §13.1, §13.2
|
||
- classification: tenant 바인딩·`search_path`처럼 격리를 결정하는 세션 설정은 트랜잭션과 함께 되돌아가야 한다. 세션 스코프면 풀로 돌아간 커넥션이 그것을 들고 있고 다음 borrower가 상속한다. PostgreSQL에서는 `set_config(name, value, true)`의 세 번째 인자가 그것을 보장한다.
|
||
- scope: RLS tenant 바인딩·schema 라우팅·타임아웃 등 세션 상태로 표현되는 모든 격리. 대응물이 없는 벤더에서는 반환 시 명시적 reset이 대안이다.
|
||
- exceptions: 커넥션이 tenant에 고정 할당되는 database-per-tenant에서는 세션 스코프가 문제가 되지 않는다. 다만 그때는 풀 예산이 새 문제가 된다.
|
||
- relations: `case:search-path-survived-the-return-to-the-pool` · `reference:session-scoped-settings-outlive-the-transaction` · `concept:rls-three-preconditions`
|
||
|
||
### DECISION — 클래스패스에 있는 것은 실행 동의가 아니다
|
||
- slug: `classpath-presence-is-not-consent`
|
||
- readiness: `READY` · decision-status: `ADOPTED`
|
||
- source: `final/document.md#10-3` · `analysis/05` §13
|
||
- decision-evidence: `.../experimental/ExperimentalFeatureGate.java`의 javadoc · 세 클래스(`RlsTenantSessionBinder`·`ConsistencyAwareDataSourceRouter`·`SchemaTenantMigrationOrchestrator`)의 package-private 생성자와 `enabledBy` 정적 팩토리
|
||
- grounds: `concept:four-multitenancy-strategies` · `reference:hibernate-filter-is-not-a-security-boundary`
|
||
- classification: experimental 모듈은 transitive하게 도착할 수 있고, jar가 있다는 이유로 스스로 켜지는 tenant-isolation이나 replica-routing 기능은 가능한 최악의 기본값이다. 게이트가 **결정의 부재를 활성화가 아니라 에러**로 만든다. 그리고 그것이 실제로 강제되도록 세 클래스가 생성자를 package-private으로 바꾸고 `enabledBy` 팩토리만 남겼다 — "게이트를 static이 아니라 **파라미터로** 받는 게 요구사항을 시그니처의 일부로 만든다: caller는 컴파일러가 요구하는 인자를 잊을 수 없다."
|
||
- relations: `concept:four-multitenancy-strategies` · `reference:runtime-membership-decides-severity` · `decision:one-root-owns-the-master-switch`
|
||
|
||
---
|
||
|
||
### CASE — `IdFactory.newId()`의 “never-before-used” 문구 정밀화
|
||
- slug: `a01-f002-idfactory-newid`
|
||
- readiness: `READY`
|
||
- source: `analysis/01-domain-core.md#L216`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: Fact: interface는 저장소 collision check를 요구하지 않고 sample test도 전역 uniqueness를 증명하지 않는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### OPEN QUESTION — response/LRO invariant enforcement boundary
|
||
- slug: `a02-f001-lro`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/02-shared-contract.md#L155`
|
||
- known: `Envelope`, `BulkEnvelope`, `Operation`, `PageMeta`의 valid shape는 factory test에 고정돼 있지만 public canonical constructor는 그 invariant를 강제하지 않는다.
|
||
- unknown: raw constructor가 의도된 extension surface인지, invalid state를 constructor에서 차단해야 하는지 프로젝트 선택이 확인되지 않았다.
|
||
- next-verification: production/raw-constructor 호출자를 전수 확인하고 invalid-shape constructor test를 추가해 현재 허용 surface를 고정한다.
|
||
- decision-criterion: raw constructor가 외부 extension 계약이면 허용 범위와 failure semantics를 문서화한다. 그렇지 않으면 constructor-level invariant를 추가하고 factory와 동일한 규칙을 검증한다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### OPEN QUESTION — DomainContextKey same-name different-type collision
|
||
- slug: `a02-f002-domaincontextkey`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/02-shared-contract.md#L159`
|
||
- known: `DomainContextKey` identity는 name only이고 retrieval은 요청 타입으로 cast한다.
|
||
- unknown: 같은 name의 다른 `Class<T>` key 선언이 forbidden contract인지 의도적으로 허용된 충돌/실패 모델인지 정해져 있지 않다.
|
||
- next-verification: 동일 name·상이 type key fixture를 만들고 creation/retrieval failure를 고정한 뒤 registry의 실제 key 선언을 전수 대조한다.
|
||
- decision-criterion: same-name/different-type가 금지라면 creation/registry 단계에서 충돌을 거부한다. 허용이라면 cast failure semantics와 사용 조건을 계약에 명시한다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### OPEN QUESTION — bounded operational record identifiers
|
||
- slug: `analysis-finding-a02-f003`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/02-shared-contract.md#L163`
|
||
- known: `OperationalRecord` javadoc은 namespace/key를 bounded라고 설명하지만 constructor는 blank 여부만 확인한다.
|
||
- unknown: provider별 key size/character-set 제한을 shared contract가 소유해야 하는지 adapter가 소유해야 하는지 정해지지 않았다.
|
||
- next-verification: 실제 provider/adapter의 identifier 제한과 production 생성 지점을 대조하고 경계값 fixture를 추가한다.
|
||
- decision-criterion: 여러 provider가 공통 최소 bound를 요구하면 shared value object에서 강제한다. provider-specific이면 shared javadoc을 좁히고 adapter 경계에서 검증한다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### OPEN QUESTION — permission component grammar
|
||
- slug: `analysis-finding-a02-f004`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/02-shared-contract.md#L167`
|
||
- known: permission은 colon segment 수·blank·normalization은 강제하지만 segment character grammar는 제한하지 않는다.
|
||
- unknown: registry SSOT가 shared value object보다 좁은 문자 grammar를 계약으로 요구하는지 확인되지 않았다.
|
||
- next-verification: registry의 모든 permission literal을 수집해 허용 문자 집합과 shared parser를 parity test로 대조한다.
|
||
- decision-criterion: registry가 더 좁은 grammar를 실제 SSOT로 사용하면 shared value object가 동일 grammar를 강제한다. 아니면 현재 넓은 grammar가 의도임을 문서화한다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### OPEN QUESTION — messaging schema qualification boundary
|
||
- slug: `analysis-finding-a02-f005`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/02-shared-contract.md#L171`
|
||
- known: 현재 JDK-only test는 exact resource/digest/selected semantic vector를 검증한다.
|
||
- unknown: 실제 Draft 2020-12 validator와의 호환성은 qualification evidence가 없어 현재 test 성공만으로 주장할 수 없다.
|
||
- next-verification: 채택할 Draft 2020-12 validator로 committed schema와 positive/negative vectors를 실행하고 결과를 evidence로 남긴다.
|
||
- decision-criterion: 실제 validator가 동일 semantic vectors를 통과해야 호환성을 주장한다. 실패하면 schema 또는 지원 범위를 수정하고 JDK-only gate의 표현을 좁힌다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### CASE — notification admin atomic claim contract가 service에서 사용되지 않음
|
||
- slug: `analysis-finding-a03-f001`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L344`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: - Fact: `AdminOperationStorePort.claim()`과 `JpaAdminOperationStore.claim()`은 존재하지만 `NotificationAdminApplicationService`는 redrive/reconcile/suppress/provider-state에서 `findByOperationId -> side effect -> save`를 사용한다. - Why it matters: 동일 operation id의 concurrent 요청이 둘 다 side effect를 실행할 수 있으며, 이는 claim javadoc이 명시한 과거 race와 동일하다. - Verification: 동일 operation id/command를 barrier로 동시에 호출하고 destructive action invocation count가 1인지 검증하는 concurrency regression test. - Candidate direction: service가 command fingerprint를 계산해 atomic claim을 먼저 수행하고 claimed/replay/conflict/in-progress를 분기. - Tech-Log: CASE + OPEN QUESTION/DECISION 후보.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### OPEN QUESTION — notification derived idempotency key가 32-bit hash
|
||
- slug: `analysis-finding-a03-f002`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/03-application-core.md#L352`
|
||
- known: fallback idempotency key는 `Integer.toHexString(Objects.hash(...))`인 32-bit hash이고 canonical fingerprint 비교는 별도로 존재한다.
|
||
- unknown: 서로 다른 canonical request가 실제 collision을 만들 때 downstream이 false conflict로 끝나는지, 이 위험을 허용할지 확인되지 않았다.
|
||
- next-verification: known Java hash collision 또는 property search로 서로 다른 canonical request의 동일 derived key를 만들고 downstream conflict behavior를 고정한다.
|
||
- decision-criterion: distinct canonical request collision이 재현되면 canonical plan의 SHA-256/HMAC 계열 digest로 교체한다. 재현하지 못해도 uniqueness javadoc은 실제 보장 수준으로 좁힌다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### REFERENCE — legacy storage/notification compatibility surface의 제거 조건 추적
|
||
- slug: `analysis-finding-a03-f003`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L360`
|
||
- classification: deprecated/legacy라는 이름은 삭제 가능성의 증거가 아니다. production wiring에 남은 compatibility surface는 **external production reference 0 + replacement characterization + config path removal**이 함께 확인된 뒤에만 제거 후보가 된다.
|
||
- scope: deprecated/legacy contract가 아직 production adapter/runtime wiring에 연결된 migration 구간.
|
||
- exceptions: 외부 호환 계약을 의도적으로 유지하거나 replacement path가 아직 동일 behavior를 증명하지 못한 경우에는 제거하지 않는다. 실제 제거 여부는 별도 Decision evidence가 필요하다.
|
||
- relations: `case:analysis-finding-a03-f001` 및 실제 legacy consumer Case와 연결; 제거 자체는 Decision evidence가 생길 때 별도 기록
|
||
|
||
### OPEN QUESTION — isolation vocabulary와 legacy routing capability의 시차
|
||
- slug: `analysis-finding-a03-f004`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/03-application-core.md#L367`
|
||
- known: public `Isolation`은 stricter level을 표현하지만 legacy `TransactionPort` template은 READ_COMMITTED로 고정되고 test도 stricter routing을 planned로 적는다.
|
||
- unknown: stricter isolation을 legacy port까지 확장할지 canonical policy transaction path에서만 제공할지 아직 정해지지 않았다.
|
||
- next-verification: 현재 production use-case의 isolation 요구와 legacy `TransactionPort` 호출자를 대조하고 향후 routing owner를 하나로 정하는 설계/contract test를 만든다.
|
||
- decision-criterion: 선택한 owner에서 use-case policy → adapter transaction definition이 1:1로 검증돼야 하며 지원하지 않는 경로는 capability로 노출하지 않는다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### CASE — notification consumer는 diagnostic failure를 authoritative failure로 바꿀 수 있다
|
||
- slug: `analysis-finding-a04-f002`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L232`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 이 finding은 support logger의 consumer semantics를 추적하면서 발견했다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — notification fail-open consumer가 logger failure를 격리하지 않는다
|
||
- slug: `analysis-finding-a04-f004`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L585`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: - Observed fact: `FailOpenNotificationProvider`는 send와 logSuccess를 동일 try에 두고 catch 안의 logFailure를 보호하지 않는다. - Runtime evidence: successful send 뒤 debug logger failure가 warn failure observation을 만들었고, provider failure 뒤 warn logger failure는 caller까지 전파됐다. - Comparison: messaging은 같은 shared logger를 `observeQuietly`로 이미 격리하고 regression test를 갖는다. - Why it matters: diagnostics가 business/provider outcome을 바꿔서는 안 된다는 non-authoritative observation 원칙이 consumer마다 달라진다. - Verification: `022a` focused probe; notification에 throwing-logger regression 추가. - Candidate: notification에서 observation isolation 또는 shared logger no-throw contract. - Tech-Log: CASE + DECISION 후보.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — support README가 current architecture registry/history와 drift
|
||
- slug: `analysis-finding-a04-f005`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L595`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: - Observed fact: SSOT 위치, CLAUDE.md 존재 여부, HTTP logger 존재 여부가 current source와 불일치. - Why it matters: support module의 dependency policy와 비교 설계를 읽는 사람이 현재 architecture를 잘못 이해한다. - Verification: `019` raw search/history. - Candidate: README를 `modules.json`/current consumer topology에 맞춰 갱신. - Tech-Log: 보통 refactor/doc maintenance; 독립 CASE 우선순위는 낮음.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### OPEN QUESTION — cache-redis/httpclient의 support project dependency 필요성 재검증
|
||
- slug: `analysis-finding-a04-f006`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/04-adapter-outbound-support.md#L603`
|
||
- known: 두 leaf 모두 Gradle support dependency는 있지만 현재 production Java support reference와 support resource는 0이다.
|
||
- unknown: build/test/reflection/후속 bounded scope에서 이 edge가 필요한지 확인되지 않아 dead dependency로 확정할 수 없다.
|
||
- next-verification: 각 downstream leaf exhaustive analysis 결과를 확인한 뒤 support dependency를 제거한 상태로 focused test와 app composition test를 실행한다.
|
||
- decision-criterion: production/build/test/runtime 소비자가 0이고 dependency 제거 후 관련 lane이 통과하면 edge를 제거한다. 소비자가 발견되면 그 owner와 이유를 문서화한다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### CASE — encode가 발급한 2046~2048-byte cursor를 decode가 거부한다
|
||
- slug: `analysis-finding-a05-f001`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L382`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 문제는 decoded payload size를 decode 전에 추정하는 helper다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### OPEN QUESTION — `CapabilitySupport.constraints`의 bounded/report-safe 계약이 타입에서 강제되지 않음
|
||
- slug: `a05-f003-capabilitysupport-constraints`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L661`
|
||
- known: 100,000-character constraint가 허용되고 capability list는 actuator report model에 포함되지만 현재 shipped composition은 짧은 static literal만 만든다.
|
||
- unknown: 외부/fork/dynamic producer가 public API에 arbitrary constraint를 넣는 실제 경로가 있는지, bound를 타입과 report projection 중 어디가 소유할지 확인되지 않았다.
|
||
- next-verification: public API의 외부 producer와 fork extension point를 추적하고 oversized constraint의 constructor/report boundary test를 추가한다.
|
||
- decision-criterion: dynamic/external producer가 확인되면 bound를 타입 또는 report projection에서 강제하고 Case로 승격한다. 없다면 public invariant/javadoc을 현재 composition의 보장 수준으로 좁힌다.
|
||
- relations: candidate-ledger의 topic/disposition provenance와 기존 관련 Case/Reference/Decision을 record 생성 시 연결
|
||
|
||
### CASE — `RetryDecision.reason`의 “bounded” 설명과 constructor contract 불일치
|
||
- slug: `a05-f004-retrydecision-reason`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L670`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: - Observed: 100,000-character reason accepted. - Observed: current retry metrics는 reason을 tag로 사용하지 않음. - Impact: 현재 cardinality defect로 확인되지 않음. - Candidate: length bound를 추가하거나 javadoc의 low-cardinality claim을 실제 사용 범위에 맞게 좁힘.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — application-supplied `JpaRetryPolicy`가 valid execution에서 무시된다
|
||
- slug: `a05-f005-jparetrypolicy`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L991`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `JpaTransactionAutoConfiguration`은 명시적으로 다음 overload를 제공한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — Stable completion-evidence capability가 shipped composition에 설치되지 않는다
|
||
- slug: `a05-f006-stable`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1122`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: ### 23.1 custom manager production construction = 0
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `TransactionProfileRegistry`는 declarative retry 제거 후 legacy residue 후보
|
||
- slug: `a05-f007-transactionprofileregistry`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1304`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 단 repository 밖 reflection/external direct construction은 source search로 알 수 없으므로 즉시 삭제 가능성까지 확정하지 않는다. module의 non-api package는 intended external이 아니라는 architecture policy와 함께 보면 cleanup 우선순위는 높아진다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — canonical transaction boundary documentation과 실제 dual stack 불일치
|
||
- slug: `analysis-finding-a05-f010`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1542`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: - docs/source comment는 coordinator가 PolicyTransactionPort를 구현한다고 설명. - 실제 구현체는 SpringTransactionPort. - coordinator/runtime bean은 별도로 계속 존재. - 우선순위: P2 architecture consistency / Decision 필요.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — property-access `IDENTITY` entity가 batch guard를 우회한다
|
||
- slug: `a05-f012-identity`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1731`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `HibernateBatchConfigurationGuard`는 batching-required profile에서 `GenerationType.IDENTITY`를 거부한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `SpecificationPolicy`는 `Specification.unrestricted()`를 bounded로 오인한다
|
||
- slug: `a05-f013-specificationpolicy-specification-unrestricted`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2021`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: > a specification with no predicate is a full table scan wearing a builder's clothing
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `collection-fetch-pagination` blocking release gate가 실제 위험을 증명하지 않는다
|
||
- slug: `a05-f014-collection-fetch-pagination`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2257`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: support matrix는 다음 gate를 blocking release gate로 선언한다.
|
||
- missing-verification: ### 52.2 release registry가 가리키는 producer task는 그 test를 실행하지도 않는다
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — query SQL naming/observability composition 부재
|
||
- slug: `a05-f018-sql`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2556`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 분석 문서가 `query SQL naming/observability composition 부재`를 P2 finding으로 분류했다. 원문 source anchor를 record 작성 시 다시 열어 코드·테스트·실행 증거를 그대로 승계한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — export surface split SSOT
|
||
- slug: `a05-f019-ssot`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2562`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: - leaf export list와 app-bootstrap consumer list가 중복 정의되고 이미 다름 - current tests pass하지만 두 목록 간 drift를 막는 single-source rule 없음 - architecture governance hardening
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `inspect()`와 `claim()`이 만료된 COMPLETED row를 동시에 다른 상태로 해석한다
|
||
- slug: `a05-f020-inspect-claim`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2675`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P1 — production idempotency lifecycle/reconciliation inconsistency.
|
||
- missing-verification: `inspect()`는 row가 `COMPLETED`이고 response payload가 있으면 `replayUntil`이 이미 지난 값인지 확인하지 않고 무조건 `COMPLETED_REPLAY`를 반환한다. 반면 claim path는 DB time과 expiry를 보고 만료된 row를 takeover 가능 상태로 처리한다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `complete()`의 replay 판정이 `replayTtl` 변경을 무시한다
|
||
- slug: `a05-f021-complete-replayttl`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2704`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 첫 호출은 1시간, 두 번째 호출은 동일 operation/response에 9시간을 전달했다. 두 번째 호출은 semantic argument가 다른데도 same-result로 판정됐고 DB에는 최초 1시간 window가 그대로 남았다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — Stable runtime-role verification이 startup에서 실제 policy를 적용하지 않는다
|
||
- slug: `a05-f022-stable`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2991`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P1 — production security/runtime-composition contract violation.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — persistent byte quota가 실제 admission에서 집행되지 않는다
|
||
- slug: `analysis-finding-a05-f023`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3177`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P1 production cross-scope contract violation — persistent scope/tenant byte quota enforcement missing.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — quota reclaim은 최대 64개 committed row만 처리하고 남은 byte를 조용히 버린다
|
||
- slug: `analysis-finding-a05-f024`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3246`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2 production accounting correctness defect.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — direct `FileQuotaService.commit()`은 만료 reservation을 commit한다
|
||
- slug: `a05-f025-filequotaservice-commit`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3266`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2 production API-contract defect.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — recovery queue의 `enqueue()`는 concurrent upsert가 아니다
|
||
- slug: `a05-f026-enqueue`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3287`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2 production concurrency/idempotency defect.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — cleanup crash-reclaim은 `MAXIMUM_ATTEMPTS`를 우회해 poison item을 무한 재시도할 수 있다
|
||
- slug: `a05-f027-maximum-attempts`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3316`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2 production liveness / bounded-retry defect.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — provider 호출 뒤 recipient projection write가 lease fencing을 우회한다
|
||
- slug: `analysis-finding-a05-f028`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3460`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P1 production concurrency/correctness defect. provider side effect와 authoritative outcome write 사이의 lease handoff에서 stale writer가 살아남는다. 결과에 따라 중복 전송 위험 판단, retry/reconciliation state, attempt count가 새 holder의 흐름과 충돌할 수 있다.
|
||
- missing-verification: 실제 fenced helper도 완전하지 않다. `saveProjectionHeldBy()` / `transitionHeldBy()`는 `id + lease_owner + lease_fence`만 조건으로 두고 `lease_until > now`는 확인하지 않는다. PostgreSQL에서 이미 만료되어 `stillHeld` 조건이 0건인 row에 동일 owner/fence write를 실행하면 `UPDATE 1`이었다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — reconciliation `FOR UPDATE SKIP LOCKED`는 worker 처리 구간을 claim하지 않는다
|
||
- slug: `a05-f029-for-update-skip-locked`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3494`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2 production multi-instance coordination defect. reconciliation은 send 자체가 아니라 provider 상태 조회/상태 projection이어서 recipient dispatch P1보다 영향도를 낮게 잡지만, 두 worker가 같은 job을 처리할 수 있다는 class contract는 깨진다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — V8 atomic admin claim은 production service에 연결되지 않았고 completion 모델도 미완성이다
|
||
- slug: `analysis-finding-a05-f030`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3525`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2 production idempotency/wiring defect. V8에서 만든 fix가 dead path이며 completion state machine도 이어지지 않는다. DB transaction 안에서 수행되는 redrive/suppress 일부 경로는 마지막 unique conflict가 loser transaction을 rollback시켜 결과를 완화하지만, reconcile/provider runtime control처럼 action과 final audit insert가 하나의 동일 DB transaction으로 묶이지 않는 경로까지 전체적으로 exactly-once operation claim을 보장하지 못한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — vendor selector의 fail-fast 계약이 shipped composition에 설치돼 있지 않다
|
||
- slug: `analysis-finding-a05-f031`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4089`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2 confirmed. 오타 난 vendor 값은 startup을 실패시키기는 하지만, 그 실패는 property를 지목하지 않는다 — `PersistenceVendorSettings`가 막겠다고 선언한 바로 그 증상이다. app-bootstrap의 `PersistenceVendorProdSafetyValidator`도 도움이 되지 않는다. 그 validator는 prod profile에서 값이 `h2`인지만 보고 알 수 없는 값은 통과시킨다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — nightly workflow가 광고하는 세 가지 중 하나를 lane이 실제로 관측하지 않는다
|
||
- slug: `analysis-finding-a05-f032`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4273`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `.github/workflows/jpa-nightly.yml:122-129`는 이 lane이 검사하는 것을 세 가지로 적는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — selected base card `jpa-flyway-migration`의 producer가 현재 revision에서 실패한다
|
||
- slug: `a05-f033-jpa-flyway-migration`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4391`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P1 confirmed. 수정은 assertion을 stream의 현재 applied set으로 갱신하는 것이고, 재발 방지는 `PostgreSqlOptionalStreamLifecycle`이 이미 쓰는 방식(stream별 버전 목록을 한 곳에 고정)을 base stream에도 적용하는 것이다. 더 근본적으로는 이 lane이 tag lane과 완전히 분리돼 있다는 구조 자체가 재검토 대상이다 — 5개 tag lane이 green이라는 사실이 readiness lane의 상태에 대해 아무것도 말해주지 않는다.
|
||
- missing-verification: `postgresqlSecurityBaselineIntegrationTest`의 실패는 **분석 환경 제약**이지 결함이 아니다. `verifyFullAcceptsTrustedHostAndRejectsHostnameMismatchAndUntrustedCertificate`는 `PostgreSqlTlsMaterial`이 `CN=localhost` / `SAN=DNS:localhost`로 발급한 인증서를 `verify-full`로 검증하므로 컨테이너의 매핑 포트가 **테스트 JVM의 loopback**에서 열려 있어야 한다. 이번 분석은 Docker 소켓을 공유하는 형제 컨테이너 안에서 실행돼 매핑 포트가 Docker 브리지(172.17.0.1)에만 열렸고, 실패는 `java.net.ConnectException`이다. 이 lane은 skip이 아니라 실패하도록 설계돼 있으므로(no-skip) 동작 자체는 의도대로다. 다만 "no-skip"의 대가로 **Docker 호스트와 테스트 JVM이 loopback을 공유하는 환경**이 이 lane의 암묵적 전제가 된다는 사실은 기록해 둔다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — selected base card 3개의 evidence tag가 production code 없는 fixture로 충족된다
|
||
- slug: `analysis-finding-a05-f034`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4462`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2 confirmed. 대비되는 반례가 같은 card 집합 안에 있다는 점이 판단을 쉽게 해 준다 — `jpa-transaction-runtime`의 7개 scenario는 production `SpringTransactionPort` + `PostgreSqlLocalTimeoutConfigurer` + `PersistenceExceptionTranslator`를 실제 서버에서 돌리고, deadlock 40P01, serializable 재시도, lock/statement timeout 경계, pool admission 거부, `pg_terminate_backend`로 만든 commit 유실의 `INDETERMINATE` 판정까지 확인한다. 즉 이 결함은 체계적인 것이 아니라 세 card에 국한된다. 수정은 tag를 옮기는 문제다 — 이미 존재하는 강한 test를 scenario로 등재하거나, 약한 scenario의 `covers`에서 과대 tag를 떼는 것.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 폐기된 namespace guard의 탐색 domain이 operator가 읽는 두 문서를 덮지 않는다
|
||
- slug: `analysis-finding-a06-f002`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L132`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3 confirmed. guard가 막겠다고 명시한 형태(문서가 operator에게 폐기 키를 쓰라고 말하는 것)가 guard의 사각지대에서 그대로 살아 있고, 그중 하나는 복사해 쓰라고 제시된 예제다. 런타임은 영향받지 않는다 — Compose lane은 `SPRING_MONGODB_URI`를 공급하고, 폐기는 제거가 아니다. 수정은 두 문서의 키를 `spring.mongodb.*`로 바꾸고, guard의 domain에 leaf의 `*.md`를 추가하는 것이다(추가하면 위 세 곳이 즉시 red가 되므로 함께 고쳐야 한다).
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `change-streams=true`는 거부되지 않고 조용히 버려지며, 그 결과 startup validator의 한 분기가 production에서 도달 불가다
|
||
- slug: `a06-f003-change-streams-true`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L156`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3 confirmed. 현재 잘못된 동작을 만들지는 않는다 — change stream 실행체는 애초에 shipped되지 않는다고 CLAUDE.md가 명시한다. 문제는 (a) 문서가 refuse라고 말하는 것이 discard이고, (b) 그 결과 capability 검사 한 갈래가 test에서만 살아 있다는 점이다. 수정은 두 방향 중 하나다 — 값을 정말로 거부하거나(`requiredSecondaries`와 같은 형태), 아니면 flag를 record component에서 제거해 존재하지 않는 스위치로 만드는 것.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — schema version 실패는 두 경로 중 어느 쪽도 온전하지 않다
|
||
- slug: `analysis-finding-a06-f004`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L332`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MongoFailureCategory`에는 이 실패를 위한 전용 값 `SCHEMA_VERSION_UNSUPPORTED`("The stored document's schema version is outside the supported range")가 있고, 전용 예외 `MongoDataSchemaUnsupportedException`이 `documentVersion` / `minimumSupported` / `currentVersion` 세 정수를 공개 accessor로 노출한다. production 생성 지점은 정확히 둘이고, 각각 반쪽만 맞다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 예외 계층의 "cause를 붙이지 않는다" 규칙에 문서화되지 않은 예외가 하나 있다
|
||
- slug: `analysis-finding-a06-f005`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L347`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MongoPersistenceException`의 javadoc은 두 번째 규칙을 절대적으로 서술한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — D3 gateway가 문서화한 검사 순서에 존재하지 않는 단계가 있다
|
||
- slug: `analysis-finding-a06-f007`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L505`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 수정은 문서를 실제 검사로 줄이거나(정직), 선언된 `timeout`/`maxResults`를 gateway가 실제로 적용하도록 만드는 것이다. 후자를 택하면 `hasBody()`가 처음으로 호출자를 갖게 된다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 서버 측 deadline이 경로마다 다르게 적용되고, 문서가 지목한 메커니즘은 production 호출자가 0이다
|
||
- slug: `analysis-finding-a06-f008`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L600`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 데이터 손상은 아니지만 platform이 스스로 선언한 자원 경계가 자신의 세 실행 경로에서 서버에 도달하지 않는다. 수정은 `MongoPlatformCollectionAccess`가 `rawOperations()` 대신 deadline이 붙은 접근자를 내보내거나, 세 executor가 query를 만들 때 `context.timeout()`을 붙이는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — timeout 초과 경로가 한 observation에 success와 failure를 모두 기록한다
|
||
- slug: `analysis-finding-a06-f009`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L622`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MongoOperationRejectedException`은 `MongoPersistenceException`의 하위 타입이고, 이 throw는 같은 `try` 블록 안에 있으므로 바로 다음 `catch (MongoPersistenceException alreadyTranslated)`가 잡아 `observation.failure(...)`를 호출한 뒤 다시 던진다. 결과적으로 하나의 observation에 `success`와 `failure`가 차례로 호출된다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — collection 이름 불변식이 aggregation executor의 서명에서 깨진다
|
||
- slug: `analysis-finding-a06-f010`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L717`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 현재 노출은 없다(§41: 미배선). 그러나 fork가 이 executor를 배선하는 순간 두 가지가 동시에 생긴다 — registry가 보장한다고 적힌 불변식의 예외 하나, 그리고 관측·실패번역 없이 도는 실행 경로 하나. 판정: P2. 수정은 서명에서 `String collection`을 없애고 `context.collectionProfile()`을 registry로 해석하는 것, 그리고 실행을 `DefaultMongoImperativeExecutor.executeInternal(...)` 안으로 옮기는 것이다. 후자는 §32에서 본 deadline 문제도 함께 해결한다(현재 aggregation은 `maxTime`을 스스로 붙이므로 그 부분만은 이미 옳다).
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `MongoRegexPolicy.forbidden()`은 금지하지 않는다
|
||
- slug: `a06-f011-mongoregexpolicy-forbidden`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L740`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: "금지"가 별도 상태가 아니라 최대 길이 1로 표현돼 있다. `validate(pattern, flags)`의 네 검사를 길이 1짜리 패턴 `^`에 대해 따라가면 — 길이 1 ≤ 1 통과, flags 없음 통과, `requireAnchored && startsWith("^")` 통과, `hasNestedQuantifier("^")`는 그룹이 없으므로 false 통과 — 수용된다. 그리고 `^`는 모든 문자열에 매치된다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `recordApplied`는 문서화된 fence 계약을 구현하지 않고, 보호를 역전시킨다
|
||
- slug: `a06-f013-recordapplied`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L884`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 수정은 `saveCheckpoint`와 같은 모양이다 — `recordApplied`도 저장된 fence를 조건으로 삼고, duplicate-key를 잡아 platform 예외로 번역하는 것. 지금은 test도 이 경계를 보지 않는다: `MongoMigrationFencingTest.ledgerWritesCarryTheirFence`는 fence 값이 전달되는지만 보고, `MongoMigrationLaneTest`의 superseded 테스트는 checkpoint만 다룬다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — index diff가 실제로 비교하는 것은 두 필드뿐이다
|
||
- slug: `analysis-finding-a06-f014`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L910`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MongoIndexDescriptorView`의 javadoc이 "reduced to the fields a diff can compare"라고 스스로 한정하는 것은 사실이지만, 그 축소의 결과(무엇이 감지 불가가 되는지)는 어디에도 적혀 있지 않고, `MongoIndexDiff.render()`가 CI artifact로 쓰이도록 설계돼 있으므로 "빈 보고서 = 일치"로 읽힌다. 판정: P2. 최소 수정은 `MongoIndexDescriptorView`에 `expireAfter`와 `sparse`를 추가하고 `compare`에서 비교하는 것, 그리고 `actual.hidden() && !declared.hidden()`에 대한 `unhide` 항목을 두는 것이다. 그것이 과하다면 최소한 비교 대상 필드 집합을 diff 출력에 함께 적어 "빈 보고서"가 무엇을 뜻하는지 읽는 사람이 알 수 있게 해야 한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — TTL이 두 곳에 선언되고, 규칙을 가진 쪽은 아무도 쓰지 않는다
|
||
- slug: `analysis-finding-a06-f015`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L927`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 1. `MongoIndexManifest.expireAfter(Duration)` — 검증은 생성자의 `isNegative()` 하나. 2. `MongoTtlPolicy` / `MongoTtlIndexDescriptor` + `MongoTtlPolicyValidator` — 세 가지 실질 규칙: 최소 보존기간 1분(그 아래는 한 번의 sweep으로 전체 population을 지운다), expiry 필드의 BSON 타입이 `date`인지(아니면 MongoDB가 조용히 무시한다), 그리고 읽기가 `expiresAt > applicationNow`를 거는지(TTL monitor는 임의 간격으로 돌므로 만료된 문서는 그때까지 계속 읽힌다).
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — Flamingock lease로는 어떤 migration도 실행할 수 없고, javadoc은 다르게 적는다
|
||
- slug: `a06-f016-flamingock`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L942`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `FlamingockLockAdapter.fence()`는 `UNFENCED`(-1)를 반환하고, 그 이유를 정직하게 적는다 — 로컬 카운터로 fencing을 흉내내면 "look like fencing and protect nothing". 여기까지는 옳다. 문제는 그 다음 문장이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `changeStreams` flag는 `false`로 고정돼 있는데, 소비자 bean은 그것과 무관하게 조립된다
|
||
- slug: `a06-f018-changestreams-false`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1069`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 수정은 셋 중 하나다: `changeStreams`를 실제 flag로 되살려 소비자 조립의 조건으로 쓰거나, 소비자 bean이 조립될 때 CHANGE_STREAM capability를 startup에서 검증하거나, 최소한 `MongoPlatformSettings`의 주석을 현재 사실("source는 출하됐고 소비자도 조립된다")로 고치는 것. 지금 주석은 운영자가 읽으면 틀린 결론에 도달한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — recovery package에 쓰이는 어휘와 쓰이지 않는 어휘가 나란히 있다
|
||
- slug: `analysis-finding-a06-f019`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1088`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `135-...` §8.3의 검색 결과를 정리하면, 소비자가 실제로 쓰는 것과 아닌 것이 갈린다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 프로파일의 TLS·타임아웃·풀·Stable API가 driver에 도달하지 않는다
|
||
- slug: `a06-f020-tls-stable`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1161`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P1. 수리는 이미 있는 형태를 따르면 된다 — `MongoClientSettingsBuilderCustomizer` bean 하나가 `MongoClientSettingsFactory`(또는 그 `build` 로직)를 Boot의 빌더에 적용하게 하는 것. 그때 `MongoCredentialResolver`도 비로소 경로에 들어온다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — admin gateway의 두 audit 경로 중 하나만 fail-closed다
|
||
- slug: `analysis-finding-a06-f021`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1189`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MongoAdminGateway.execute`는 모든 audit 쓰기를 `audit(...)` 헬퍼로 보내고, 그 헬퍼는 sink 실패를 `MongoOperationRejectedException`으로 바꾼다 — "an administrative operation that cannot be audited does not run". `MongoAdminAuditStateMachineTest.anUnauditableCommandDoesNotRun`이 그것을 고정한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 태그 allowlist는 규약이지 강제가 아니다
|
||
- slug: `analysis-finding-a06-f022`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1195`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MongoObservationConvention`의 javadoc은 강제라고 말한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — sharding admin gateway의 네 작업 중 셋은 어떤 입력으로도 완료될 수 없다
|
||
- slug: `analysis-finding-a06-f023`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1281`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 데이터 위험은 없다 — 거부는 fail-closed이고, 오히려 안전한 방향으로 틀렸다. 위험은 능력이 문서상 존재하고 실제로는 없다는 것이며, 그 사실이 발견되는 시점은 운영자가 프로덕션 클러스터에서 reshard를 실행하려는 순간이다. 수정은 세 메서드가 `MongoAdminCommand.over(...)` + `MongoAdminApproval.of(command, approver, expiry)`를 만들어 2인자 `execute`에 넘기고, `ReshardApproval`의 증거를 그 승인의 전제로 쓰는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — promotion 증거 어휘가 둘이고, gate는 하나만 검사한다
|
||
- slug: `analysis-finding-a06-f024`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1305`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MongoAdvancedPromotionEvidence.REQUIRED`는 여섯 범주다: `stable-platform`, `actual-topology`, `security`, `migration`, `failure`, `runbook`. `MongoAdvancedPromotionGate.verify(...)`가 그 여섯을 전부 검사한다 — 그리고 그 파일에는 고쳐진 결함이 주석으로 남아 있다: "`migration` was in `MongoAdvancedPromotionEvidence.REQUIRED` and not here, so the gate demanded five of the six categories it declares… which is the shape MNG-008 names: a gate that certifies more than it ran."
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 구현 없는 4개의 계약 중 셋은 그 사실을 적고, 하나는 적지 않는다
|
||
- slug: `analysis-finding-a06-f025`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1324`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MongoSearchOperations`·`MongoTimeSeriesOperations`·`MongoVectorSearchOperations`는 모두 동일한 문단을 담는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 커버리지 gate 둘이 나란히 있고, 하나는 발화할 수 없다
|
||
- slug: `analysis-finding-a06-f026`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1383`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 두 인증 lane(7.0/8.0)의 커버리지 주장이 무효다. 수정은 형제를 따르면 된다 — `run(...)`이 실행할 contract 집합을 인자로 받거나, runner가 실제로 호출된 것만 `executed`에 넣는 것.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — release gate가 실제로 차단하는 것은 hermetic test 3개이고, mongo용 CI workflow는 없다
|
||
- slug: `analysis-finding-a06-f027`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1410`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 이것은 개별 코드 결함이 아니라 이 leaf의 검증 지형이다. 그리고 앞선 sub-scope들에서 찾은 것들 — §67의 change stream 소실, §75의 TLS 미적용, §85의 sharding 미완료, §56의 fence 계약 — 이 왜 살아남았는지를 설명한다: 그것들을 잡을 lane은 릴리스를 막지 않고 CI에서 돌지 않는다. 수정은 두 갈래다. (a) 컨테이너 lane 중 최소한 `mongoReplicaSetTest`·`mongoMigrationTest`·`mongoSecurityIntegrationTest`를 blocking contract로 승격하고, (b) JPA와 같은 형태의 workflow를 추가하는 것.
|
||
- missing-verification: 차단 계약 **셋 전부가 `topology=none`**, 즉 컨테이너가 필요 없는 hermetic 클래스다. experimental 셋은 **어느 build 파일에도 등록되지 않은 task**를 가리킨다(`grep mongoShardedTest build.gradle` → 매치 0; 스크립트가 그 사실을 스스로 적는다: "registered by no build file"). 그리고 컨테이너가 필요한 여섯 lane — `mongoReplicaSetTest`·`mongoFailoverTest`·`mongoMigrationTest`·`mongoCompatibilityTest`·`mongoSecurityIntegrationTest`·`mongoPerformanceTest` — 은 **차단 목록에 하나도 없다**.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 소비자가 없는 fixture 셋
|
||
- slug: `analysis-finding-a06-f028`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1433`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `138-...` §8.1의 소비자 계수에서 test·testkit 양쪽 모두 0인 타입이 셋이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 모듈의 존재 논거인 `UuidCodec`에 production 소비자가 없다
|
||
- slug: `a07-f001-uuidcodec`
|
||
- readiness: `READY`
|
||
- source: `analysis/07-adapter-outbound-identifier.md#L73`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 코드 자체에는 결함이 없다 — 30줄짜리 유틸이고 자기 스펙을 통과한다. 문제는 §1의 논거다. 모듈을 `adapter-outbound` 밖에 두는 근거로 "UUID id/codec 능력"을 들고 있는데, 그 능력은 아무도 쓰지 않고 같은 일이 저장소 곳곳에서 각자 수행된다. 나머지 두 타입(가명화·업로드 식별자)만으로도 non-IO 능력 모듈의 논거는 성립하므로, 수정은 둘 중 하나다: `UuidCodec`을 실제 단일 경로로 만들거나(그러면 §4가 먼저 고쳐져야 한다), 모듈의 논거에서 빼는 것.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `normalize`는 canonical이 아닌 입력을 받아 다른 UUID로 조용히 바꾼다
|
||
- slug: `a07-f002-normalize`
|
||
- readiness: `READY`
|
||
- source: `analysis/07-adapter-outbound-identifier.md#L89`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 도달성. 지금 이 메서드를 부르는 production 코드는 없다(§3). 그래서 현재 노출은 0이고, `UuidCodec`을 단일 경로로 승격하는 순간 결함이 된다. 판정: P2. 수정은 `input.length() != 36`이거나 대시 위치가 8-13-18-23이 아니면 먼저 거부하는 것 — 또는 계약 문구를 실제 동작(JDK 관대 파싱)에 맞추는 것이다. 전자가 문서가 말하는 바다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — CLAUDE.md의 의존성 서술이 세 항목 모두 틀렸다
|
||
- slug: `a07-f004-claude`
|
||
- readiness: `READY`
|
||
- source: `analysis/07-adapter-outbound-identifier.md#L131`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: > `:application-code`, `:domain-core`, `:shared-contract` (Gradle matrix). Currently only `:domain-core` + `com.github.f4b6a3:uuid-creator` are declared in build.gradle.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — README의 세 가지 사실 오류
|
||
- slug: `analysis-finding-a07-f005`
|
||
- readiness: `READY`
|
||
- source: `analysis/07-adapter-outbound-identifier.md#L150`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 셋 다 메커니즘 자체는 실재하고 동작한다 — 틀린 것은 이름과 버전이다. P3.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — CLAUDE.md가 대는 두 가드 중 하나는 저장소에 없다
|
||
- slug: `a07-f006-claude`
|
||
- readiness: `READY`
|
||
- source: `analysis/07-adapter-outbound-identifier.md#L160`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 1. ArchUnit `identifier_adapter_does_not_depend_on_other_adapters_or_bootstrap` — 존재한다(§2, 상수명은 대문자). confirmed. 2. `.claude/hooks/ca_import_gate.py` G4가 쓰기 시점에 차단 — `.claude/` 디렉터리에는 `settings.local.json` 하나뿐이고 `hooks/` 하위 디렉터리도 `ca_import_gate.py`도 tracked 되어 있지 않다(`140-...` §8.4e).
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — R1과 R2의 설정 취급이 비대칭이고, 검증된 쪽은 하나뿐이다
|
||
- slug: `analysis-finding-a08-f002`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L108`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: R2에서는 `strict-path-securty` 같은 오타가 컨텍스트를 실패시킨다. R1에서는 `app.file-export.maximum-rowz=10` 같은 오타가 조용히 무시되고, 설정했다고 믿는 상한이 적용되지 않은 채 기본값 1,000,000이 쓰인다. 두 selector가 같은 leaf의 같은 성격 설정인데 한쪽만 fail-closed다. P3 — R1은 문서상 "compatibility only"이므로 우선순위를 낮춘다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 문서가 지목한 기본값 위치와 test 목록이 실제와 다르다
|
||
- slug: `analysis-finding-a08-f003`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L122`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: - README는 R2 selector가 "`app-bootstrap/application.yml`에서 `false`로 기본값을 갖는다"고 적는다. 그 파일에 `app.fileserver.enabled`도 `app.file-export.enabled`도 없다(`142-...` §8.4e; `app.fileserver`로 걸리는 두 줄은 주석이다). 실효 기본값은 "속성 부재 → `@ConditionalOnProperty` 미매치 → bean 없음"이고 동작은 옳지만, 문서가 가리킨 자리에는 그 키가 없다. - README의 Tests 목록 첫 항목 `FilePublicationContractTest`는 이 leaf가 아니라 `application-core`에 있다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 발행 rename만 경로 기반이고, 그것을 지키는 것은 이 모듈이 "근사에 불과하다"고 적은 사전검사다
|
||
- slug: `analysis-finding-a08-f004`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L376`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 실제 악용에는 스토리지 루트 안쪽 쓰기 권한이 필요하고, 이 leaf가 그 루트의 소유자·권한을 증명하는 것은 R2 경로(`LocalPersistentRootAttestor`)뿐이며 플랫폼 저장소 루트의 증명은 `app-bootstrap`의 startup validator 몫이다. 그래서 도달성은 배포 형상에 달려 있다. 심각도를 P3로 두는 이유는 그것이고, 그럼에도 기록하는 이유는 모듈 자신의 문서가 이 패턴을 명시적으로 불충분하다고 선언했다는 점이다. 수정은 발행 rename을 `SecureDirectoryWalk.inParentOf`로 옮겨 부모 서술자 상대 `move`를 쓰고, `sizeOf`를 `channels.readAttributes`로 바꾸는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `TransferBufferPool.maxBorrowedBytes()`가 자기 회귀 test를 지목하는데 그 test가 읽지 않는다
|
||
- slug: `a08-f005-transferbufferpool-maxborrowedbytes`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L410`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `TransferBufferPool`은 대여 중 바이트의 최대치를 추적하고 javadoc에 이렇게 적는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — production 판정이 두 개의 리터럴 프로파일 이름에 걸려 있다
|
||
- slug: `analysis-finding-a09-f001`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L106`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 이 저장소가 `application-prod.yml`을 싣고 있으므로 현재 형상에서는 맞는다. 그리고 `150-...` §8.2c에서 확인했듯 같은 방식으로 production을 판정하는 leaf는 이것 하나뿐이다 — 저장소 전체가 공유하는 production 판별 장치가 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 그러나 R0 경계가 문서에만 있고 compile 경로에서 닫히지 않는다
|
||
- slug: `analysis-finding-a09-f004`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L485`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 데이터 위험은 없다 — 없는 port는 호출될 수 없다. 위험은 (a) 운영자가 켰다고 믿는 기능이 없다는 것과 (b) 아무도 쓰지 않는 서명 자격증명 핸들이 프로세스 수명 동안 살아 있다는 것이다. 수정은 셋 중 하나다: coordinator를 조건부로 조립하거나, R0인 동안 `DIRECT_UPLOAD`/`DIRECT_MULTIPART` 요구를 compile 단계에서 provider 종류와 무관하게 거부하거나, capability가 켜져도 presigner를 만들지 않도록 조립을 뒤로 미루거나.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — nonce replay 경계가 결과를 읽고 버린다
|
||
- slug: `analysis-finding-a09-f006`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L589`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `ClaimResult`는 `CLAIMED` / `EXACT_REPLAY` / `TERMINAL_REPLAY` 셋인데, 어느 값이든 발행은 그대로 진행된다. claim 결과가 바꾸는 것은 terminal 기록을 쓸지 여부뿐이다. 인터페이스 javadoc은 자신을 "Durable compare-and-set nonce replay boundary"라고 부르지만, 경계로서 무엇도 막지 않는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — README readiness 표와 build.gradle 주석이 실제 소스와 어긋난다
|
||
- slug: `a10-f001-readme`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L121`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 코드 결함이 아니라 문서 결함이지만 이 저장소 기준으로는 무겁다. 첫째, 이 leaf의 README는 "readiness는 서로 다른 세 가지 질문이며 하나로 합치면 안 된다"는 문장으로 시작하는, 정직한 readiness 보고를 자기 주제로 삼는 문서다. 둘째, 방향이 이례적이다 — 보통의 drift는 없는 것을 있다고 하는데 여기는 있는 것을 없다고 한다. fork가 이미 있는 4,900 LOC를 다시 구현하거나, 조립되지 않은 채 존재하는 코드의 존재 자체를 모르게 된다. 셋째, `RedisSdkAutoConfiguration`의 javadoc이 "until this class existed the method had no production caller at all"이라고 적는 것으로 보아 이 클래스가 README 문장보다 나중이다 — 조립이 진행됐는데 서술이 따라가지 않았다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — SDK가 선언한 두 진입점에 구현이 없다
|
||
- slug: `analysis-finding-a10-f003`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L251`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 데이터 위험은 없다 — 없는 타입은 잘못된 답을 주지 않는다. 위험은 API 계약의 신뢰다: 이 leaf의 공개 표면 중 가장 먼저 읽히는 두 타입이 구현되지 않은 상태이고, 대칭 test가 그 사실을 가리지 못한다(인터페이스끼리만 비교하므로). sub-scope 01의 §5와 방향이 반대이면서 원인은 같다 — 조립이 절반이다. 수정은 이미 존재하는 26개 구현을 묶는 `LettuceRedisOperations` / `LettuceReactiveRedisOperations` 두 클래스를 추가하고, `ApiParityTest`에 "두 facade는 구현을 가진다"는 검사를 더하는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — Pub/Sub 채널만 렌더 크기 검증을 받지 않는다
|
||
- slug: `a10-f004-pub-sub`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L263`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `RedisKeyRules.requireRenderedSize(...)`는 조립된 키 문자열이 설정된 최대 바이트를 넘지 않는지 본다. 세 형제 중 하나만 그것을 부른다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 다중 키 fan-in 중 HyperLogLog `merge`만 budget이 없다
|
||
- slug: `a10-f005-hyperloglog-merge`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L277`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 같은 성격(여러 키를 읽어 하나에 쓰는, 비용이 입력 크기에 비례하는 연산)의 세 형제를 비교하면 요구가 다르다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `requireIdentifier`의 다섯 검사 중 둘은 도달할 수 없다
|
||
- slug: `a10-f006-requireidentifier`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L375`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 보안 효과는 그대로다 — 두 형태 모두 거부된다. 잃는 것은 진단 품질(운영자가 "must not contain a mail address" 대신 일반적인 문자 클래스 메시지를 본다)과, 두 분기가 실제로는 아무 일도 하지 않으면서 검증이 다섯 겹인 것처럼 보이게 만드는 점이다. 수정은 두 분기를 지우고 문자 클래스 메시지에 그 의도를 포함시키거나, 검사 순서를 뒤집어 구체적 형태를 먼저 판정하는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 패턴 구독의 R2 승인만 호출자가 아니라 배포에 대해 이루어진다
|
||
- slug: `analysis-finding-a10-f007`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L485`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 배포 수준 게이트는 실재하고 네임스페이스 봉쇄도 있으므로 열린 구멍은 아니다. 기록하는 이유는 같은 위험 등급에 두 가지 다른 승인 모델이 적용되고 그 차이가 문서화돼 있지 않기 때문이다. 수정은 `patternSubscribe` 서명에 `AdvancedOperationPermit`을 추가하거나, javadoc에 "배포 수준 승인"임을 명시하는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — permit 정책 이름이 세 곳에 문자열로 존재하고 교차 검사가 없다
|
||
- slug: `analysis-finding-a10-f008`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L502`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 확정은 sub-scope 05로 이월한다 — `RedisCommandPolicyLoaderTest`가 정책 이름 집합을 검사하는지 그 sub-scope에서 확인한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `close()`가 실패하면 drain 스케줄러 스레드가 남는다
|
||
- slug: `a11-f001-close`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L121`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 스레드가 데몬이라 JVM 종료를 막지는 않고, 레지스트리당 하나이며, 닫기 실패라는 조건이 필요하다. 그러나 주석이 "a thread per rotation cycle"을 명시적 위험으로 적고 resource-bound suite가 그것을 잡으려 존재하는데, 정확히 그 누수가 실패 경로에 남아 있다. 수정은 스케줄러 종료를 `finally`로 옮기는 한 줄이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `POOL_ROUTE_EXCEEDS_TOTAL` 위반 코드는 발화할 수 없다
|
||
- slug: `a11-f002-pool-route-exceeds-total`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L146`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: // ClientProfileValidator:192 if (profile.pool().maxConnectionsPerRoute() > profile.pool().maxTotalConnections()) { out.add(violation("POOL_ROUTE_EXCEEDS_TOTAL", profile, "pool.max-connections-per-route")); } ```
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 위반 코드 34종 중 22종이 어떤 test에서도 이름으로 확인되지 않는다
|
||
- slug: `analysis-finding-a11-f003`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L164`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `ClientProfileValidator`가 내는 코드는 34종이다. 저장소 전체의 `test`/`testkit` source set에서 그 문자열을 참조하는 파일 수를 세면(`168-...` §8.4b):
|
||
- missing-verification: test 두 개(`ClientProfileValidatorTest` 106줄)가 그룹으로 몇 개를 묶어 확인하지만(`rejectsSimpleFactoryAndUnacknowledgedHttp3AndJdkRoutePool`), 나머지 22종은 분기를 지워도 초록으로 남는다. 코드 자체는 현재 옳다 — 위험은 회귀다. **P3.** 수정은 `@ParameterizedTest`로 코드별 최소 케이스를 한 벌 놓는 것이고, 34종이 모두 결정적으로 정렬된 목록을 내므로 그 형태가 자연스럽다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `Number`가 허용 목록에 있어 가변 숫자 타입이 REPLAYABLE로 인증된다
|
||
- slug: `a11-f004-number`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L249`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 도달성이 좁고(원자 카운터를 요청 DTO에 넣어야 한다), 검사 전체의 방향은 보수적이며, `aMutableValueIsOneShot` test가 일반적인 가변 객체는 잡는다. 기록하는 이유는 이 검사가 "records, enums, strings, boxed primitives and immutable collection views replay; anything else is treated as one-shot"라고 선언하는데 `Number` 한 줄이 그 선언보다 넓기 때문이다. 수정은 boxed primitive 여덟 종과 `BigInteger`/`BigDecimal`을 명시하거나, `java.util.concurrent.atomic` 패키지를 제외하는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `BoundedDataBufferFlux`의 두 연산자가 이름만 있고 아무것도 하지 않는다
|
||
- slug: `a11-f006-boundeddatabufferflux`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L424`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 수정은 두 연산자를 지우고 javadoc이 `doOnDiscard`와 드라이버의 역할을 정확히 적게 하거나, 취소 경로에서 실제로 해제해야 할 것이 있다면 그것을 구현하는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 동적 대상 DNS 핀 능력 검사가 블로킹 오버로드에만 있다
|
||
- slug: `a11-f007-dns`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L583`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 위험은 fork가 리액티브 전송을 추가하면서 `dynamicTargetStable=true, validatedDnsPinning=false`로 선언하는 경우 — 주석이 "cannot serve a dynamic target no matter what its `dynamicTargetStable` flag says"라고 못박은 정확히 그 조합이 리액티브 쪽에서는 통과한다. 수정은 같은 세 줄을 리액티브 오버로드에 복사하는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `check`에 붙은 `verifyJsonSchemaRuntimeGraph`가 실행되면 실패한다
|
||
- slug: `a12-f001-check-verifyjsonschemaruntimegraph`
|
||
- readiness: `READY`
|
||
- source: `analysis/12-adapter-outbound-messaging.md#L82`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P2. 금지 조건 쪽(YAML 계열·Jackson 2 `core`/`databind` 부재)은 여전히 옳게 동작하지만, 필수 조건 쪽이 버전 드리프트로 고장 나 있어 게이트 전체가 통과할 수 없다. 결과는 이 저장소가 다른 곳에서 반복해 경계한 바로 그 상태다 — 붙어 있으나 초록일 수 없는 게이트는 사람들이 건너뛰는 법을 배우게 만든다. 수정은 필수 좌표에서 버전을 떼고 `group:name`만 확인하거나(닫힘 조건은 "무엇이 없는가"이지 "어느 패치인가"가 아니다), 잠금 파일에서 버전을 읽어 비교하는 것이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — README의 `jackson-databind` 부재 주장이 현재 상태와 어긋난다
|
||
- slug: `a12-f002-jackson-databind`
|
||
- readiness: `READY`
|
||
- source: `analysis/12-adapter-outbound-messaging.md#L118`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 판정: P3. 코드 결함은 아니다 — `OutboxEnvelopeJson`의 손수 짠 직렬화는 그 자체로 문제가 없다. 기록하는 이유는 그 선택의 근거로 적힌 사실이 더 이상 성립하지 않는다는 점이고, fork가 그 문장을 읽고 "databind가 없다"를 전제로 다른 결정을 내릴 수 있기 때문이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `AUTHENTICATION_FAILED`를 지우지 않는다는 `resumeHealthy`의 보장이, 관리자 평면에 노출된 2단계 시퀀스로 우회된다
|
||
- slug: `a13-f002-authentication-failed-resumehealthy`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L320`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `ProviderRuntime.resumeHealthy`는 자신이 지키는 성질을 javadoc에 명시한다:
|
||
- missing-verification: **테스트가 이것을 잡지 못하는 이유** — 해당 테스트는 세 전이를 **각각 새 런타임에서** 확인하고 합성을 확인하지 않는다:
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — "모든 reveal은 감사된다"고 선언한 `AccessContext`를 읽는 코드가 저장소에 하나도 없다
|
||
- slug: `a13-f003-accesscontext`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L430`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: // ContactPointProtector.java:11 / Decrypt a value for an audited purpose. */ ContactPointValue reveal(ProtectedContactPoint protectedValue, AccessContext context); ```
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — Thymeleaf 예외 메시지 삭제 가드가 프로덕션이 타지 않는 오버로드에만 있다
|
||
- slug: `a13-f004-thymeleaf`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L476`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `ThymeleafStringTemplateEngine`은 같은 클래스에 `render` 두 개를 갖는다. 모드 없는 쪽은 예외를 잡아 메시지를 버리고, 그 이유를 명시한다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 음수 `Retry-After` 헤더가 throttle 결과 대신 `IllegalArgumentException`을 만든다
|
||
- slug: `a13-f005-retry-after-illegalargumentexception`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L636`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 파서는 `Long.parseLong`이 받아들이는 값을 그대로 `Duration`으로 만든다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — "상한을 두고 읽는다"는 본문 핸들러가 전부 읽은 뒤에 자른다
|
||
- slug: `analysis-finding-a13-f007`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L795`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `BodySubscribers.mapping(upstream, finisher)`의 finisher는 upstream이 완료된 뒤 그 결과에 적용된다. upstream은 `ofByteArray()`이고, 그것은 무제한으로 요청하여 본문 전체를 힙에 모은다. 잘라내기는 그 다음이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — SigV4가 서명한 `host`에 포트가 없어, 기본 포트가 아닌 엔드포인트에서 서명이 어긋난다
|
||
- slug: `a13-f008-host`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L831`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: SigV4의 정규 요청은 실제로 전송되는 `Host` 헤더 값을 서명해야 하고, 기본이 아닌 포트는 그 값에 포함된다. 요청 자체는 `host` 헤더를 싣지 않으며(`JdkNotificationHttpGateway.RESTRICTED`가 거부하고 JDK가 URI에서 채운다), JDK는 `localhost:4566` 같은 값을 보낸다. 서명은 `localhost`에 대해 이루어졌다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — SigV4 서명 키 파생이 비밀을 지울 수 없는 `String`으로 승격시킨다
|
||
- slug: `a13-f009-sigv4-string`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L844`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `secretAccessKey`는 `NotificationSecretMaterialHandle`이 제공하는 지울 수 있는 가변 사본이다. 그 핸들의 존재 이유가 "operation-scoped mutable secret copy that wipes itself on close"이고, `close()`가 `Arrays.fill(bytes, (byte) 0)`을 한다. 이 한 줄이 그 바이트를 불변 `String`으로 복사하며, 그 `String`은 GC가 가져갈 때까지 힙에 남고 어떤 `close()`도 지울 수 없다. 힙 덤프 한 장이면 회수된다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — FCM만 "커밋 후 응답 손실 = ambiguous" 규칙 밖에 있고, 그 FCM이 두 계약 집합 어디에도 없다
|
||
- slug: `analysis-finding-a13-f010`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L957`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `ProviderResults`의 존재 이유가 클래스 javadoc에 쓰여 있다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 공유 provider 계약이 8종 중 3종에서만 상속되고, 강제 장치가 없다
|
||
- slug: `analysis-finding-a13-f011`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L990`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §28.1·§28.3. `ProviderAdapterContract`의 javadoc이 약속하는 성질("a new provider cannot be added without answering the same three questions")을 지키는 장치가 없다. 이 저장소는 같은 실패 양식에 대해 `EndpointGuardCallSiteTest`와 `verifyNotificationApiSurface`라는 구조적 강제를 이미 두 번 만들었으므로, 형태는 이미 있다 — 적용되지 않았을 뿐이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `WebProblemSanitizer.alreadySafe`가 죽은 메서드이고 그 안의 조건도 죽어 있다
|
||
- slug: `a14-f002-webproblemsanitizer-alreadysafe`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L380`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 호출자 0(프로덕션·테스트 모두). 그리고 `!input.isBlank()`가 이미 통과했으므로 `input.trim()`은 비어 있을 수 없고, `toLowerCase`는 공백 여부를 바꾸지 않는다 — 삼항의 `REDACTED` 가지는 도달 불가다. javadoc이 약속하는 용도("for asserting a message is already safe")를 수행하는 코드가 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 플랫폼 요청 컨텍스트가 서블릿에는 생산자가 없고, 리액티브에는 익명 액터로 고정되어 있다
|
||
- slug: `analysis-finding-a14-f003`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L503`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 서블릿 절반. `WebMvcRequestContextHolder.store(...)`의 호출자가 저장소 전체에서 0이다. 그런데 그것을 읽는 쪽은 자동설정이 등록한다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 프레임워크 자유 신원 모델과 교차 테넌트 가드가 프로덕션에서 한 번도 참조되지 않는다
|
||
- slug: `analysis-finding-a14-f004`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L562`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §11.1. `security` 패키지 11개 파일이 서로만 참조하고 바깥에서 들어오는 화살표가 없다. `AuthenticationView`를 만드는 프로덕션 코드가 없으므로 `WebSecurityContextBridge.resolve(...)`가 호출될 수 있는 상태 자체가 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `publicPaths`가 `RestrictedPathRule`보다 먼저 등록되어, 넓은 공개 경로 하나가 관리 평면 규칙을 조용히 덮는다
|
||
- slug: `a14-f005-publicpaths-restrictedpathrule`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L582`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §11.3. `RestrictedPathRule`의 javadoc은 이 규칙이 존재하는 이유를 "an application-level policy consulted later cannot recover from a transport that already let the request through"로 설명한다. 그런데 `permitAll(publicPaths)`이 그 규칙보다 먼저 등록되어 정확히 그 일을 한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 용량 보호 계층 전체(41 main files)가 자기 테스트 픽스처 안에서만 실행된다
|
||
- slug: `analysis-finding-a14-f006`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L677`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 즉 이 플랫폼을 그대로 배포하면 요청 본문 크기 상한도, 응답 크기 상한도, 요청 데드라인도, 동시성 상한도, 큐 상한도 없다. `WebRequestBudget.standard()`가 정의하고 `WebBudgetCatalog`가 담고 있는 값들은 아무도 읽지 않는다(§15.3).
|
||
- missing-verification: **실패 시나리오** — 배포된 API에 무제한 청크 본문이 도착한다. `WebMvcBudgetFilter`가 필터 체인에 없으므로 `BoundedHttpServletRequest`가 스트림을 감싸지 않고, `WebBudgetMeter`가 바이트를 세지 않는다. 컨테이너 기본값(Tomcat `maxPostSize`는 `multipart/form-data`와 폼 인코딩에만 적용되고 임의 본문에는 적용되지 않는다) 외에 상한이 없다. 같은 요청에 대해 동시성 상한도 없으므로 `SemaphoreAdmissionController`가 내기로 되어 있던 503도 나오지 않는다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 리액티브 전송에는 속도 제한 경로가 하나도 없다
|
||
- slug: `analysis-finding-a14-f007`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L701`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §15.2. 배선된 유일한 속도 제한기 `RateLimitInterceptor`는 `WebMvcConfigurer.addInterceptors`로 붙는 MVC 전용 장치다. `WebFluxThrottleFilter`가 리액티브 대응물이지만 등록되지 않는다(§16.1). `webflux/autoconfigure/WebFluxPlatformAutoConfiguration`의 11개 빈에도 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 멱등 실행 계층과 durable-operation 표면이 픽스처에서만 조립된다
|
||
- slug: `analysis-finding-a14-f008`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L793`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §19.1·§19.2. 이 sub-scope에서 프로덕션 컨텍스트에 들어가는 것은 `WebExecutionEvidenceTracker`(두 요청 필터가 만든다)와 빈 `InMemoryWebOperationCatalog` 둘뿐이다. 나머지 38개 main 파일 — 게이트, 두 invoker, 응답 writer, 지문 공장, 헤더 정책, 명령 인코더, 승인 판정, 응답 계획, 코덱, 그리고 `operationasync` 9종 전부 — 는 테스트와 testkit에서만 생성된다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 의미 지문이 길이 프레이밍 없이 구분자로 만들어진다
|
||
- slug: `analysis-finding-a14-f009`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L807`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §19.4. 경로 변수 값과 헤더 값이 이스케이프 없이 구분자로 이어붙는다. 값 자체에 그 구분자가 들어가면(퍼센트 인코딩 `%1F`를 Spring이 디코딩해 `@PathVariable`로 전달한다) 서로 다른 두 요청이 같은 정규 문자열을 만들 수 있다 — 경로 변수가 둘 이상인 연산에서 하나를 통제하면 구성 가능하다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 배선된 캐시 필터의 `no-store`가 배선된 조건부 읽기 경로를 무력화하고, 둘을 조정하려고 만든 패키지는 참조 0이다
|
||
- slug: `a14-f010-no-store`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L899`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `CacheControlFilter`가 모든 응답에 `Cache-Control: no-store`를 붙인다. RFC 9111에서 `no-store`는 "어떤 캐시에도 저장하지 말라"는 지시다. 규격을 지키는 클라이언트는 응답을 보관하지 않으므로, 나중에 그 리소스에 대해 `If-None-Match`를 보낼 근거(저장된 표현과 그 ETag)를 갖지 못한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `maxArrayElements`가 선언만 되고 강제되지 않으며, 바이트 예산 백스톱도 없다
|
||
- slug: `a14-f011-maxarrayelements`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1006`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 읽는 코드가 저장소 전체에 0개다(§27.1). Jackson 3의 `StreamReadConstraints`에는 배열 원소 수 상한이 없으므로 `BoundedJsonFactory`가 넘길 자리도 없고, 매퍼 쪽에서도 검사하지 않는다.
|
||
- missing-verification: **백스톱이 없다.** 이 위험을 막을 상위 장치가 `WebMvcBudgetFilter`의 요청 바이트 상한인데, SS4에서 확인했듯 그 필터는 등록되지 않는다(§16.1). 서블릿 컨테이너의 기본값도 임의 본문에는 적용되지 않는다. 따라서 지금 이 플랫폼에는 **JSON 배열 원소 수에 대한 상한이 어느 계층에도 없다.**
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 선언된 Advanced 능력 11개 중 9개는 켜는 방법이 없다
|
||
- slug: `a14-f014-advanced`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1267`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §35.1. `webAdvancedTest` 레인이 이 능력들을 전부 돌리고(`web-advanced-nightly.yml:46` · `web-advanced-release.yml:53`), `WebAdvancedRollbackIT`가 능력마다 플래그를 켰다 껐다 하며 롤백을 검증한다. 그러나 그 검증은 `WebAdvancedFeatureFlags.of(feature)`라는 테스트 전용 값에 대한 것이고, 배포가 실제로 조작할 수 있는 스위치는 두 개뿐이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `VirtualThreadProfile.propertyName()`이 아무것도 게이트하지 않는 이름을 반환한다
|
||
- slug: `a14-f015-virtualthreadprofile-propertyname`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1279`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §35.3. `VirtualThreadProfile:75`는 `"backend.web.advanced.virtual-threads.enabled"`를 하드코딩한다. 실제 게이트는 `mvc-virtual-threads`이고, 같은 능력에 대해 enum이 계산하는 이름도 `mvc-virtual-threads`다. 이 메서드는 호출자가 0이므로 지금 오작동을 만들지는 않지만, "The property that turns this on"이라는 javadoc과 함께 잘못된 이름을 발행한다 — 운영자가 이 문서를 보고 설정하면 켜지지 않는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 이 leaf의 리액티브 절반 29개 파일은 어떤 출하 배포에서도 활성화될 수 없다
|
||
- slug: `analysis-finding-a14-f016`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1378`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `@ConditionalOnWebApplication(type = REACTIVE)`는 Spring Boot의 `WebApplicationType`이 `REACTIVE`일 때만 참이다. `deduceFromClasspath()`는 `DispatcherServlet`과 `ServletContainerInitializer`가 있으면 WebFlux가 함께 있어도 `SERVLET`을 고른다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `SpringMvcRouteInventoryCollector` 138줄에 참조가 하나도 없다
|
||
- slug: `a14-f017-springmvcrouteinventorycollector`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1494`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 저장소 전체에서 이 타입 이름이 등장하는 곳은 자기 파일의 클래스 선언과 생성자 두 줄뿐이다. 테스트도 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `WebPlatformStartupValidator`가 시작 시 실행되지 않는다
|
||
- slug: `a14-f018-webplatformstartupvalidator`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1500`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §43.1·§43.2. 이름이 약속하는 시점에 아무도 부르지 않는다. 같은 leaf의 fileserver 하위 트리는 같은 종류의 시작 검증을 app-bootstrap의 `@Bean`으로 연결했고(SS10 §39.1), 그 근거를 "Better to refuse to start"로 적었다. 플랫폼 쪽 검증기에는 그 연결이 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 크로스 스택 게이트가 검증하는 조립은 픽스처의 조립이고, 플랫폼의 조립이 아니다
|
||
- slug: `analysis-finding-a14-f019`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1575`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 이 leaf는 이 저장소에서 가장 정교한 검증 장치를 갖고 있다 — 여섯 소스셋, 다섯 커스텀 레인, 세 런타임 패리티 비교, 실제 Nginx 컨테이너, 실제 소켓 고장 주입. 그리고 §47.1이 보여주듯 그 장치가 세우는 애플리케이션은 픽스처 애플리케이션이다.
|
||
- missing-verification: 이 leaf는 이 저장소에서 가장 정교한 검증 장치를 갖고 있다 — 여섯 소스셋, 다섯 커스텀 레인, 세 런타임 패리티 비교, 실제 Nginx 컨테이너, 실제 소켓 고장 주입. 그리고 §47.1이 보여주듯 **그 장치가 세우는 애플리케이션은 픽스처 애플리케이션이다.**
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 원인 사슬 순회가 2-순환에서 무한 루프에 빠지고, 저장소는 이미 그 사례를 이름으로 적어 두었다
|
||
- slug: `analysis-finding-a15-f001`
|
||
- readiness: `READY`
|
||
- source: `analysis/15-adapter-inbound-grpc.md#L171`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 나머지 세 지점의 영향도 — `MvcDisconnectDetector`와 `WebFluxDisconnectDetector`는 요청 처리 중 클라이언트 연결 끊김을 판정하는 곳이고, `TransactionRetryClassifier`는 트랜잭션 재시도 여부를 판정하는 곳이다. 셋 다 요청 스레드 위에서 실행된다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 설정 바인딩이 마스터 스위치 밖에서 일어난다. 컴포지션 루트의 자기 규칙과 어긋난다
|
||
- slug: `analysis-finding-a15-f002`
|
||
- readiness: `READY`
|
||
- source: `analysis/15-adapter-inbound-grpc.md#L187`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §3.4. `GrpcServerProperties`는 두 경로로 등록된다 — `GrpcServerConfig`의 `@EnableConfigurationProperties`(게이트 안쪽)와 `CaSkeletonApplication`의 `@ConfigurationPropertiesScan`(게이트 바깥). 후자가 있으면 `ca-skeleton.grpc.enabled`와 무관하게 바인딩이 일어난다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 스키마 조립·계약 정체성·해시 사슬이 통째로 미배선이고, 그것을 발행할 액추에이터 엔드포인트도 등록되지 않는다
|
||
- slug: `analysis-finding-a16-f001`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L258`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 1. 결정적 병합 순서. SDL 조각의 정렬을 조립기가 강제하도록 설계돼 있고(§7.4), 실제로는 Spring GraphQL의 탐색 순서를 그대로 쓴다. 조각이 하나(`skeleton.graphqls`)뿐인 지금은 무해하지만, adopter가 자기 `.graphqls`를 추가하는 순간 — 그것이 이 leaf의 문서화된 확장 방식이다 — 충돌 선언의 승자와 스키마 해시가 패키징 방식에 따라 달라질 수 있다. 2. 네 부분 계약 정체성. `GraphQlSchemaContract`가 "해시만으로는 호환성을 판정할 수 없다"는 판단을 타입으로 만들었는데, 그 타입을 만드는 코드가 없다. 호환성 판정이 필요한 곳(릴리스 게이트)은 `compat`의 비교기를 직접 쓴다. 3. 운영 가시성. `GraphQlPlatformActuatorEndpoint.report()`가 배포된 스키마 해시 · 실행 프로파일 · 배포 모드 · 활성 능력 · 등록된 연산/페치 프로파일 수를 하나의 보고서로 낸다. 등록되지 않으므로 운영자가 "이 배포가 무엇을 켜고 있는가"를 물을 표면이 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `@oneOf` 게이트와 런타임 검증기가 미배선이고, "플랫폼이 강제한다"는 서술이 그것을 넘어선다
|
||
- slug: `a16-f002-oneof`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L281`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `GraphQlOneOfPolicy`의 클래스 javadoc은 "The September 2025 `@oneOf` input rules the platform enforces"로 시작한다. 강제하는 두 코드 — 시작 게이트와 런타임 검증기 — 는 프로덕션 호출자가 0이다.
|
||
- missing-verification: 기록하는 것은 두 가지다: (a) 플랫폼 계층의 강제가 서술과 달리 존재하지 않는다, (b) `GraphQlOneOfSchemaGate.verify(sdl)`가 확인하는 것은 라이브러리가 확인하지 않는 부분(멤버가 전부 nullable이고 기본값이 없어야 한다는 **선언 시점** 규칙)이므로, adopter가 잘못된 `@oneOf` 입력 타입을 선언하면 시작 시점이 아니라 첫 요청에서 드러난다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 5계층 예산 모델에서 요청 계층만 강제되고, 나머지 파생이 전부 미배선이다
|
||
- slug: `analysis-finding-a16-f003`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L382`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `GraphQlDeadlinePropagator`의 javadoc이 계층 분리의 이유를 정확히 적는다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 연산 이름 정책의 두 구현 중 하나만 배선되고, 미배선 쪽만 `GraphQlOperationNamePolicy`를 쓴다
|
||
- slug: `a16-f004-graphqloperationnamepolicy`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L403`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §11.2. 익명 연산 거부는 배선된 `GraphQlOperationSelectionHandler`가 수행하므로 강제 자체는 존재한다. 기록하는 것은 정책 객체의 이원화다 — `GraphQlOperationNamePolicy`(85줄)의 참조자가 미배선 인터셉터와 자기 자신뿐이고, 배선된 핸들러는 별개의 `policy`를 쓴다. 두 정책이 "이름 있는 연산을 요구하는가"에 대해 서로 다른 답을 낼 수 있는 구조이며, 지금은 한쪽만 답한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 설정으로 정한 파서 한계가 graphql-java에 설치되지 않는다
|
||
- slug: `analysis-finding-a16-f005`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L511`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 파서 계층은 GraphQL DoS 방어의 첫 번째 관문이다 — 복잡도 계산도 구조 분석도 문서를 파싱한 뒤에 일어나므로, 파싱 자체를 폭발시키는 문서는 그 앞에서 막아야 한다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 프로파일별 정책 매니페스트가 미배선이라, 자격에서 해석된 프로파일이 아무 예산도 선택하지 않는다
|
||
- slug: `analysis-finding-a16-f006`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L525`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §15.2. 클라이언트 프로파일은 검증된 principal에서 정확히 해석되고 요청 컨텍스트에 실린다. 그리고 그 값이 선택하는 것은 캐시 키와 지표 태그뿐이다 — 정책은 프로파일과 무관하게 단일 빈이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 파싱·검증 실패에 플랫폼 매퍼가 없다
|
||
- slug: `analysis-finding-a16-f008`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L680`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §19.3. `GraphQlRequestErrorMapper`(57)가 그 목적으로 존재하고 미배선이다. 배선된 두 매퍼(`GraphQlExceptionResolver` · `GraphQlWireErrorMapper`)는 각각 리졸버 예외와 플랫폼 거부를 덮는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 기계가 읽는 능력 매니페스트와 사람이 읽는 등급표가 커서 서명에 대해 다르게 답한다
|
||
- slug: `analysis-finding-a16-f011`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L889`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 즉 이 매니페스트는 "Stable 스타터에서 켜도 되는가"를 판정한다. cursor 서명은 켤 수 있는 것으로 판정되고, 켜는 코드는 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — "기본 비활성"은 존재하지 않는 스위치의 기본값을 서술한다
|
||
- slug: `analysis-finding-a16-f012`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L1002`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 권고 — 그 문단을 등급에 맞춘다: "Advanced capability 는 현재 `modelled` 등급이며 활성화 경로가 없다. `GraphQlAdvancedFeatureFlags`·`GraphQlAdvancedModuleGuard`는 그 경로가 생길 때 쓸 판정 모델이다."
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 연결 티켓·origin 정책·메시지 권한·연결 예산이 요청 경로 밖이고, 그중 일부는 STOMP 어댑터가 다른 방식으로 대체한다
|
||
- slug: `a17-f002-stomp`
|
||
- readiness: `READY`
|
||
- source: `analysis/17-adapter-inbound-websocket.md#L273`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §12.1·§12.2. 실제 배포에서 적용되는 보안은 `stomp` 패키지의 두 인터셉터이고, 그것은 CLAUDE.md가 서술하는 범위다("HTTP-handshake principal enforcement and client-inbound STOMP destination authorization").
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 능력 프로퍼티 이름을 만드는 코드와 실제 게이트가 다른 접두사를 쓴다
|
||
- slug: `analysis-finding-a17-f003`
|
||
- readiness: `READY`
|
||
- source: `analysis/17-adapter-inbound-websocket.md#L426`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §21.2. `WebSocketAdvancedCapability.propertyName()`이 반환하는 `backend.websocket.advanced.*`를 읽는 `@ConditionalOnProperty`가 없다. 운영자가 그 메서드가 알려 주는 키를 설정하면 아무 일도 일어나지 않고, 실제로 능력을 켜는 키는 `app.websocket-platform.advanced.*`다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 출하되는 web 어댑터의 스위치가 활성화 모델 밖에 있다
|
||
- slug: `analysis-finding-a18-f001`
|
||
- readiness: `READY`
|
||
- source: `analysis/18-app-bootstrap.md#L214`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §4.1. `adapter-inbound-web`은 두 런타임 멤버이므로 build-only 예외에 해당하지 않는다. 그런데 그 네 개 스위치가 조건 안의 문자열 리터럴로만 존재해 `MasterSwitch`의 javadoc이 경계하는 상태다 — "Spread across conditions as string literals, a rename becomes a silent activation change."
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 실패는 환경 원인이며, 그 테스트의 도구 가드가 불완전하다
|
||
- slug: `analysis-finding-a18-f002`
|
||
- readiness: `READY`
|
||
- source: `analysis/18-app-bootstrap.md#L549`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 저장소 결함이 아니다. 분석 컨테이너에 `jq`가 없고, 위임된 스크립트가 그것을 요구하며 exit 78로 정직하게 실패한다.
|
||
- missing-verification: **저장소 결함이 아니다.** 분석 컨테이너에 `jq`가 없고, 위임된 스크립트가 그것을 요구하며 exit 78로 정직하게 실패한다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — capability 12개 중 main 코드가 읽는 것은 3개, 거부하는 것은 1개
|
||
- slug: `analysis-finding-a19-f001`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L231`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `MessagingCapabilities`의 클래스 javadoc이 이 record의 계약을 선언한다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 8개 profile validator 중 조립에서 실행되는 것은 3개
|
||
- slug: `analysis-finding-a19-f002`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L288`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `StartupProfileValidation`의 javadoc이 이미 한 번 고쳐진 같은 결함을 서술한다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `messaging-reliability-api`는 main 13파일 · 817 LOC에 테스트가 0개다
|
||
- slug: `a19-f003-messaging-reliability-api`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L325`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: sub-scope 01의 네 leaf 중 유일하게 `src/test`가 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 스키마 호환성 검증기는 출하 leaf에 있고, main 코드에서 호출되지 않는다
|
||
- slug: `analysis-finding-a19-f004`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L390`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `SchemaCompatibilityValidator`(`messaging-schema-api`, 출하)의 main 참조는 0건이다. 테스트 1개뿐.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 호환성 게이트를 가진 두 포맷은 build-only이고, 출하되는 유일한 코덱에는 게이트가 없다
|
||
- slug: `analysis-finding-a19-f005`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L415`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 위험 방향이 뒤집혀 있다. 스키마 진화 검사가 존재하는 두 포맷(Avro·Protobuf)은 어떤 런타임에도 오르지 않고, 실제로 wire에 바이트를 쓰는 유일한 코덱(JSON)에는 포맷 수준의 호환성 게이트가 없다. §4.3의 포맷 독립 검증기(`SchemaCompatibilityValidator`)가 그 공백을 메울 자리인데 그것도 호출되지 않는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `messaging-cloudevents`는 출하 leaf이고 starter의 의존이며 소비자가 없다
|
||
- slug: `a19-f006-messaging-cloudevents`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L431`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 그런데 main 코드에서 `DefaultCloudEventMapper`를 만드는 곳은 0곳이고, `CloudEventMapper`·`CloudEventExtensions`를 참조하는 main 파일은 `DefaultCloudEventMapper` 자신뿐이다. 자동설정 28개 클래스 어디에도 CloudEvents 이름이 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 브로커 ACL 매니페스트의 자기 점검이 존재하지 않는다
|
||
- slug: `a19-f008-acl`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L495`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `BrokerAclManifest`(`messaging-security`, 출하) — main 참조 0건, 테스트 1건.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 접근 검사가 두 갈래로 존재하고, 조립된 쪽이 진단이 약한 쪽이다 (§8.3)
|
||
- slug: `analysis-finding-a19-f009`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L511`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: (a) 조립된 쪽 — `DefaultMessagePublisher`가 `DestinationAccessPolicy`를 직접 호출:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 지원 매트릭스가 "모든 messaging leaf는 build-only"라고 적고, 가족 권위 문서는 그 문장이 틀렸다고 이미 기록했다
|
||
- slug: `analysis-finding-a19-f013`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L682`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 이 드리프트의 실질적 무게는 이 문서 전체의 심각도 판정 축과 같다(§1.1). 지원 매트릭스만 읽은 운영자는 messaging이 아무것도 출하하지 않는다고 결론 내리는데, 실제로는 `messaging-kafka`·`messaging-rabbit`·`messaging-spring-boot-starter`·`messaging-security`·`messaging-observability`·`messaging-cloudevents`·outbox/inbox/claim-check·admin plane이 전부 `app-bootstrap` 아티팩트에 실려 있다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 한 아티팩트 안의 서로 모르는 Kafka 스택 두 개 (MSG-015, 가족 문서가 미해결로 표시)
|
||
- slug: `a19-f014-kafka-msg`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L698`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `src/messaging/CLAUDE.md`가 MSG-015를 P0 미해결로 들고 있다. 현재 상태를 코드로 확인했다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `CompatibilityMatrix`에 `EXTENSION` 등급이 있고 항목이 없으며, bridge leaf가 표 밖에 있다
|
||
- slug: `a19-f015-compatibilitymatrix-extension`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L757`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `Tier` enum은 세 값을 갖는다 — `STABLE`, `EXPERIMENTAL`, `EXTENSION`("Adapter SPI only; outside the supported set"). `ENTRIES` 5개는 전부 STABLE 또는 EXPERIMENTAL이고 `EXTENSION`을 쓰는 항목은 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — claim-check는 starter에 배선 코드가 한 줄도 없다
|
||
- slug: `analysis-finding-a19-f018`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L926`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `messaging-claim-check`(6 main, 418 LOC, 출하)의 `ClaimCheckPublisher`·`ClaimCheckResolver`는 main 참조 0건이고, `MessagingReliabilityAutoConfiguration`에 `ClaimCheck` 문자열이 등장하지 않는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — admin 스위치가 가드를 켜고 서비스는 켜지 않는다
|
||
- slug: `analysis-finding-a19-f019`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L975`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `app.messaging.admin.enabled=true`가 만드는 bean은 넷이다: `DestructiveOperationGuard`, `AdminOperationJournal`, `MessagingAdminDurabilityValidator`, (`BrokerTopologyInspector`가 있을 때) `CompositeTopologyValidator`.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다
|
||
- slug: `a19-f020-messaging-admin-api`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L997`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `messaging-admin-api`가 담고 있는 것은 승인·다이제스트·토폴로지 계약이다 — `ApprovalVerifier`, `HmacApprovalVerifier`, `ApprovalGrant`, `VerifiedApproval`, `PlanDigest`, `ApprovedRedrivePlan`, `ApprovedReplayPlan`, `DestructiveOperation`, `TopologyManifest`, `TopologyValidationReport` 등 보안에 직결되는 타입들이다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다
|
||
- slug: `a19-f022-messagingpublicsurfacecontracttest`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L1114`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: CLAUDE.md의 public surface 정책이 "이 규칙은 문서가 아니라 `MessagingPublicSurfaceContractTest`가 붙들고 있다"고 말한다. 그 테스트의 위치:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 증거 등급 모델 전체가 자동 실행 경로 밖에 있고, CLAUDE.md는 현재 시제로 서술한다
|
||
- slug: `a20-f003-claude`
|
||
- readiness: `READY`
|
||
- source: `analysis/20-grpc-platform.md#L260`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: > "현재 in-process·Netty·fault lane은 실제로 실행되어 통과하지만, 실제 배포 환경에서의 soak·performance baseline은 없다."
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 조립 경계가 정책 객체 9개를 만들고 서버를 만들지 않는다
|
||
- slug: `analysis-finding-a20-f004`
|
||
- readiness: `READY`
|
||
- source: `analysis/20-grpc-platform.md#L298`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `GrpcPlatformAutoConfiguration`이 등록하는 9개는 전부 프로파일·정책·레지스트리다. 서버도, 인터셉터 체인도, 서비스 어댑터 등록도 없다. 그리고 그것을 담당하는 타입들이 main 참조 0이다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — 저장소 어디에도 참조가 없는 타입 3개
|
||
- slug: `analysis-finding-a20-f005`
|
||
- readiness: `READY`
|
||
- source: `analysis/20-grpc-platform.md#L321`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `main = 0`이면서 `test = 0`인 것, 즉 선언 파일 외에 아무 곳에서도 이름이 등장하지 않는 타입:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `GrpcAdmissionController.tryAdmit()`의 동시성 경계가 동시성 아래에서 성립하지 않는다
|
||
- slug: `a20-f006-grpcadmissioncontroller-tryadmit`
|
||
- readiness: `READY`
|
||
- source: `analysis/20-grpc-platform.md#L518`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 이 클래스는 조립된다 — `GrpcPlatformAutoConfiguration`의 9개 bean 중 하나(`grpcAdmissionController`)다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `GrpcStreamAdmission`도 같은 형태이고, per-caller 맵이 줄지 않는다
|
||
- slug: `a20-f007-grpcstreamadmission`
|
||
- readiness: `READY`
|
||
- source: `analysis/20-grpc-platform.md#L572`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: §7.1과 동일한 TOCTOU이고, 이쪽은 javadoc이 서술하는 실패 시나리오가 곧 고동시성 상황이다:
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `GrpcSerializedStreamWriter`의 `DROP_OLDEST`가 잘못된 메시지의 바이트를 뺀다
|
||
- slug: `a20-f008-grpcserializedstreamwriter-drop-oldest`
|
||
- readiness: `READY`
|
||
- source: `analysis/20-grpc-platform.md#L595`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: 버려지는 것은 `dropped`인데 빼는 값은 새로 들어오는 메시지의 크기 `nextBytes`다. `GrpcStreamEnvelope`는 7개 성분(`streamId`·`sequence`·`kind`·`snapshotVersion`·`resumeToken`·`terminationReason`·`payload`) 중 크기를 담지 않으므로, 이 지점에서 버려지는 메시지의 크기를 알 방법이 애초에 없다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `GrpcOutcomeReplay`가 제거 경로 없는 인메모리 저장소다
|
||
- slug: `a20-f010-grpcoutcomereplay`
|
||
- readiness: `READY`
|
||
- source: `analysis/20-grpc-platform.md#L664`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: javadoc은 "a small inline store"라고 부르지만 작게 유지하는 장치가 없고, 크기를 넘는 응답은 거부하면서("store it behind an object reference instead") 개수는 거부하지 않는다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### CASE — `GrpcCompletionReconciler`가 요청 경로에서 동기화 없는 `ArrayList`를 변경한다
|
||
- slug: `a20-f011-grpccompletionreconciler-arraylist`
|
||
- readiness: `READY`
|
||
- source: `analysis/20-grpc-platform.md#L678`
|
||
- code: 분석 문서의 해당 finding 절에 기록된 production path · symbol · test/probe를 record 생성 시 재개방
|
||
- evidence: 분석 문서의 해당 finding 절 및 연결된 `evidence/raw/**`가 있으면 함께 재개방
|
||
- classification: `synchronized`·`Concurrent*`·`volatile`·`Lock` 전부 0건이고, 단일 스레드 전용이라는 javadoc 표기도 없다. 이 leaf에서 스레드 안전성을 명시적으로 다루는 유일한 클래스는 `GrpcSerializedStreamWriter`이며(그쪽은 9개 마커로 제대로 닫혀 있다), 그 사실이 이 leaf가 동시성을 인지하고 있음을 보여준다.
|
||
- missing-verification: 분석 문서의 해당 finding 절에 별도 미실행 검증이 명시되지 않았다. record 생성 시 source anchor의 code/evidence를 다시 연다.
|
||
- relations: candidate-ledger의 topic/disposition provenance를 기준으로 record 생성 시 기존 Reference/Decision과 연결
|
||
|
||
### OPEN QUESTION — 실제 성능·용량 특성을 어떤 모듈에서도 측정하지 않았다
|
||
- slug: `performance-and-capacity-unmeasured`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/99-cross-scope.md` §6
|
||
- known: cross-scope 분석은 어떤 모듈에서도 성능을 측정하지 않았고 gRPC는 성능 레인이 존재하지만 기본 `test`에서 제외된 사실까지만 확인했다.
|
||
- unknown: 현재 revision의 latency·throughput·pool saturation·backpressure·capacity ceiling이 실제 workload에서 어느 수준인지 알 수 없다.
|
||
- next-verification: 릴리스 correctness gate와 분리된 dedicated benchmark lane에서 workload·warmup·sample count·환경을 고정하고 baseline을 기록한다.
|
||
- decision-criterion: 재현 가능한 benchmark baseline과 허용 threshold가 생기면 성능/용량 주장을 그 evidence 범위에서만 닫는다. baseline 전에는 성능을 보장한다고 쓰지 않는다.
|
||
- relations: `decision:performance-measurement-is-not-a-release-gate` · `open-question:container-lanes-not-executed`
|
||
|
||
## TOPIC 17 — capability-declaration-vs-proof
|
||
|
||
### CONCEPT — 능력 선언의 세 출처와 그것이 파생되지 않을 때
|
||
- slug: `three-sources-of-a-capability-answer`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md` §4.12 · `analysis/99-cross-scope.md` §3.2
|
||
- code: `.../messaging-core-api/.../destination/MessagingCapabilities.java` · 각 어댑터의 `CAPABILITIES` 상수 · 각 어댑터의 `*ProfileValidator.capabilities(...)` · `docs/messaging/support-matrix.md`
|
||
- classification: 이 플랫폼에서 "이 어댑터가 무엇을 증명할 수 있는가" 에 답하는 곳이 셋이다. 전송의 `MessagingCapabilities` 상수(SPI `capabilities(DestinationName)` 가 런타임에 돌려주는 값), 검증기의 같은 이름 메서드(기동 시점 판정용), 그리고 운영자가 읽는 지원 매트릭스 문서. 열두 성분은 전부 `boolean` 이고 의미는 record javadoc 이 소유한다 — `brokerAcknowledgement` · `replicationOrPersistenceEvidence` · `perMessageSettlement` · `batchSettlement` · `orderedStream` · `keyedOrdering` · `replay` · `delayedDelivery` · `brokerTransaction` · `deduplicatedPublish` · `nativeDeadLetter` · `topologyManagement`. 세 출처가 같은 값을 답해야 한다는 것이 계약인데, 그것을 붙드는 장치가 없다. 그리고 열둘의 무게가 같지 않다 — 부재가 예외를 만드는 것은 `deduplicatedPublish` 하나이고(`DefaultMessagePublisher`), 나머지는 읽히지 않거나 분기에만 쓰인다. record javadoc 이 그 위험을 미리 서술한다 — "a silently weakened guarantee is indistinguishable from a working one until the incident."
|
||
- missing-verification: 세 출처를 전수 대조하는 스크립트를 돌리지 않았다. 어댑터별 SSOT 의 §능력 절을 읽어 대조했다
|
||
- relations: `reference:a-capability-constant-must-derive-from-the-profile` · `reference:the-weight-of-a-flag-is-set-by-the-code-that-reads-it` · `case:capability-constant-outlives-its-condition` · `case:support-matrix-said-the-opposite-of-the-code`
|
||
|
||
### CASE — 브로커 트랜잭션을 무조건 참으로 선언하고, 그 조건을 검사하는 검증기는 기동 시 돌지 않는다
|
||
- slug: `a-transaction-capability-true-and-its-validator-never-run`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka.md` §17.2 · `analysis/messaging/messaging-spring-boot-starter.md` §17.2
|
||
- code: `.../messaging-kafka/.../KafkaMessagingTransport.java`(`CAPABILITIES`) · `.../messaging-kafka/.../KafkaTransactionProfileValidator.java` · `.../messaging-spring-boot-starter/.../MessagingPlatformAutoConfiguration`(`StartupProfileValidation` 감싸기)
|
||
- evidence: 없음 — 능력 상수와 검증기의 감싸임 여부 대조로 판정했다
|
||
- classification: 능력 상수의 `brokerTransaction` 이 프로파일과 무관하게 참이다. Kafka 트랜잭션은 `transactional.id` 와 그에 맞는 소비자 격리 수준이 있어야 성립하고, 그 조건을 검사하는 `KafkaTransactionProfileValidator` 가 이 저장소에 있다. 그런데 스타터가 검증기를 빈으로 발행하면서 `StartupProfileValidation` 으로 감싸지 않는다 — 같은 자동 설정 안에서 다른 검증기들은 감싸인다. 검증기가 빈으로 존재하는 것과 기동 시 실행되는 것이 다르다는 것을 그 클래스의 javadoc 이 이미 이름 붙였다 — "the context published a validator per broker and validated nothing." 그 수정이 이 가족에 적용됐고 한 곳만 남았다. 결과: 트랜잭션 없이 구성된 배포가 트랜잭션 능력을 참으로 광고한 채 기동한다.
|
||
- missing-verification: 컨텍스트를 세워 검증기가 실제로 건너뛰는지 확인하지 않았다. 감싸기 목록과 검증기 목록 대조로 판정했다
|
||
- relations: `concept:three-sources-of-a-capability-answer` · `reference:a-validator-is-enforced-by-injection` · `reference:a-capability-constant-must-derive-from-the-profile` · `case:capability-constant-outlives-its-condition`
|
||
|
||
### CASE — 지연 배달을 참으로 선언하는데 그 지연을 제공할 토폴로지가 조립되지 않는다
|
||
- slug: `a-delayed-delivery-flag-without-the-topology-that-delivers-it`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-rabbit.md` §17.4
|
||
- code: `.../messaging-rabbit/.../RabbitMessagingTransport.java`(능력 상수) · 같은 리프의 토폴로지 선언부
|
||
- evidence: 없음 — 능력 상수와 토폴로지 조립 경로 대조로 판정했다
|
||
- classification: RabbitMQ 의 지연 배달은 브로커가 기본으로 주는 기능이 아니다. 지연 교환 플러그인이나 TTL + 데드레터 라우팅으로 만들어야 하고, 그 토폴로지가 없으면 지연 요청은 즉시 배달로 조용히 강등된다. 능력 상수는 그 조건과 무관하게 참이다. 그리고 이 리프 전체가 production 호출자를 갖지 않으며 `MessagingProviderSelection.BROKERS_WITHOUT_A_TRANSPORT` 가 `rabbit` 을 이름으로 거절한다 — 즉 선언은 미래의 배선을 위해 남아 있고, 그 배선이 생기는 날 이 플래그는 이미 참이다.
|
||
- missing-verification: 브로커를 띄워 지연 요청의 실제 배달 시점을 관측하지 않았다. 토폴로지 선언의 부재로 판정했다
|
||
- relations: `concept:three-sources-of-a-capability-answer` · `reference:a-capability-constant-must-derive-from-the-profile` · `case:an-unselectable-broker-listed-with-features`
|
||
|
||
### CASE — 같은 어댑터의 능력을 전송과 검증기가 다르게 답하고, 런타임이 쓰는 쪽이 record 의 의미와 어긋난다
|
||
- slug: `the-transport-and-the-validator-answer-differently`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-pulsar-experimental.md` §17.1
|
||
- code: `.../messaging-pulsar-experimental/.../PulsarMessagingTransport.java`(`KEY_SHARED_CAPABILITIES`) · `.../PulsarProfileValidator.java`(`capabilities(...)`) · `.../messaging-core-api/.../destination/MessagingCapabilities.java:11-36`
|
||
- evidence: 없음 — 두 열두-성분 리터럴의 성분별 대조로 판정했다
|
||
- classification: Key_Shared 구독에 대해 전송은 `orderedStream=false, keyedOrdering=true` 를, 검증기는 `orderedStream=true, keyedOrdering=true` 를 답한다. 성분 문서가 판정 기준이다 — `orderedStream` 은 "순서 단위 안에서 순서가 보존되는가" 이고 Key_Shared 의 순서 단위는 키다. 그러므로 검증기 쪽이 문서화된 의미와 맞고, 전송 쪽은 자기 안에서 모순이다. 그리고 어긋난 쪽이 런타임이 읽는 쪽이다 — `capabilities(DestinationName)` 이 SPI 메서드이고 `orderedStream` 은 production 코드가 실제로 읽는 세 능력 중 하나다(`DefaultRetryDecisionEngine` 이 그 값으로 순서 보존 재시도를 고른다). 두 리터럴을 묶는 것은 아무것도 없고, 테스트는 `keyedOrdering` 만 단언해 `orderedStream` 을 보지 않는다. 자매 어댑터 NATS 는 두 곳이 같은 값을 답하지만 그 일치도 공유가 아니라 손으로 복사한 리터럴이다.
|
||
- missing-verification: 두 답이 실제 재시도 선택을 어떻게 가르는지 실행으로 재현하지 않았다
|
||
- relations: `concept:three-sources-of-a-capability-answer` · `reference:the-weight-of-a-flag-is-set-by-the-code-that-reads-it` · `reference:check-which-duplicate-is-wired` · `case:capability-constant-outlives-its-condition`
|
||
|
||
### CASE — 운영자용 지원 매트릭스가 런타임 편입을 반대로 적고, 틀린 쪽이 옳은 쪽을 권위로 지목한다
|
||
- slug: `the-support-matrix-says-nothing-is-deployed-and-eighteen-are`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md` §17
|
||
- code: `docs/messaging/support-matrix.md:23` · `src/messaging/CLAUDE.md:46-59` · `src/config/architecture/modules.json`
|
||
- evidence: `evidence/raw/270-messaging-runtime-membership-drift.txt`
|
||
- classification: 지원 매트릭스가 "registry 의 messaging leaf 는 모두 `runtime_memberships` 가 비어 있고 어느 composition root 에도 편입되지 않았다" 고 적는다. 현재 레지스트리는 25개 중 18개가 `["app-bootstrap"]` 이고 `messaging-core-api` 자신이 그 안에 있다. 형태가 특이한 것은 **틀린 문단이 권위로 지목하는 문서가 이미 정정을 마쳤다**는 점이다 — `src/messaging/CLAUDE.md` 는 같은 사실을 고쳤고 "정확한 목록은 registry 가 소유하므로 여기서 세지 않는다 — 세는 순간 다시 drift 한다" 는 결론까지 적었다. 그 결론이 지원 매트릭스에는 적용되지 않았다. 운영자는 배포 아티팩트가 실제로 이 리프들을 싣고 `app.messaging.enabled` 하나로 켜진다는 사실을 문서에서 알 수 없다.
|
||
- missing-verification: 없음 — 레지스트리와 두 문서를 전수 대조했다
|
||
- relations: `reference:numbers-in-docs-should-be-derived` · `reference:fix-overstatement-before-understatement` · `concept:three-sources-of-a-capability-answer` · `case:five-documents-say-nineteen-leaves`
|
||
|
||
### REFERENCE — 능력 선언은 프로파일에서 파생되어야 하고 상수는 그것을 할 수 없다
|
||
- slug: `a-capability-constant-must-derive-from-the-profile`
|
||
- readiness: `READY`
|
||
- source: `analysis/99-cross-scope.md` §3.2 · `analysis/messaging/messaging-nats-experimental.md` §17.1
|
||
- classification: 능력은 "이 어댑터가 무엇을 증명할 수 있는가" 가 아니라 "이 프로파일로 구성된 이 목적지에서 무엇이 성립하는가" 에 대한 답이다. 두 질문의 답이 갈리는 조건이 프로파일에 있으면 상수는 그 조건을 담을 수 없다. 판정은 한 줄이다 — 이 플래그가 참이 되는 조건을 문장으로 쓰고, 그 문장에 프로파일 필드가 등장하는지 본다. 등장하면 상수는 틀린 표현이다.
|
||
- scope: `MessagingCapabilities` 열두 성분과 gRPC 쪽의 대응 선언 전부. 이 저장소의 실제 사례 — NATS `deduplicatedPublish`(창이 있을 때만), Kafka `brokerTransaction`(`transactional.id` 가 있을 때만), Rabbit `delayedDelivery`(지연 토폴로지가 있을 때만).
|
||
- exceptions: 어댑터가 브로커와 무관하게 항상 제공하는 성질은 상수가 맞다. 구분 기준은 "이 값을 거짓으로 만드는 구성이 존재하는가" 이고, 존재하지 않으면 상수다.
|
||
- relations: `concept:three-sources-of-a-capability-answer` · `case:capability-constant-outlives-its-condition` · `case:a-transaction-capability-true-and-its-validator-never-run` · `case:a-delayed-delivery-flag-without-the-topology-that-delivers-it`
|
||
|
||
### REFERENCE — 능력 플래그의 무게는 그것을 읽는 코드가 정한다
|
||
- slug: `the-weight-of-a-flag-is-set-by-the-code-that-reads-it`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md` §17.1 · `analysis/messaging/messaging-pulsar-experimental.md` §17.1
|
||
- classification: 같은 record 의 성분이라고 무게가 같지 않다. 어떤 플래그는 아무도 읽지 않고, 어떤 플래그는 분기에만 쓰이며, 어떤 플래그는 부재가 예외를 만든다. 과대 선언의 대가는 그 플래그를 읽는 코드가 무엇을 하느냐로 정해지므로, 심각도를 매기기 전에 소비자를 먼저 세어야 한다.
|
||
- scope: 능력·기능 플래그 record 전부. 세는 방법은 성분 접근자 이름으로 저장소를 훑어 production 호출자를 분류하는 것이다 — 미사용 · 분기 · 예외 발생.
|
||
- exceptions: 아직 배선되지 않은 블록에서는 모든 플래그의 현재 무게가 0 이다. 그때의 판정은 "배선되면 무엇이 그것을 읽게 되는가" 이고, 그 답은 같은 가족의 배선된 리프에서 가져온다.
|
||
- relations: `concept:three-sources-of-a-capability-answer` · `case:the-transport-and-the-validator-answer-differently` · `reference:runtime-membership-decides-severity` · `case:capability-constant-outlives-its-condition`
|
||
|
||
---
|
||
|
||
## TOPIC 18 — non-atomic-check-then-act
|
||
|
||
### CONCEPT — 원자 타입 위의 검사 후 실행과 비교 후 교체 루프
|
||
- slug: `check-then-act-on-atomic-types`
|
||
- readiness: `READY`
|
||
- source: `analysis/99-cross-scope.md` §3.5 · `analysis/grpc/grpc-policy.md` §17.1, §17.2
|
||
- code: `.../grpc-policy/.../resilience/GrpcRetryBudget.java`(`tryConsume`) · `.../resilience/GrpcHedgingBudget.java` · `.../streaming/GrpcDemandController.java`
|
||
- classification: 이 저장소가 같은 문제를 세 가지로 푼다. (1) 비교 후 교체 루프 — `GrpcRetryBudget.tryConsume` 이 `get()` 으로 현재 값을 읽고 `compareAndSet` 이 실패하면 다시 읽는다. (2) `synchronized` — `GrpcDemandController` 가 같은 형태를 락으로 닫는다. (3) 검사 후 실행 — `get()` 으로 조건을 확인하고 별도 연산으로 `set`·`incrementAndGet`·`put` 한다. 셋째는 원자 타입을 쓰면서 원자성을 얻지 못하는 형태이고, 두 스레드가 같은 조건을 통과한 뒤 각자 쓴다. `AtomicReference` 에서는 나중 쓰기가 먼저 쓰기를 덮고, `AtomicInteger` 에서는 경계가 초과되며, `ConcurrentMap` 에서는 `get` 뒤의 `put` 이 다른 스레드의 갱신을 지운다. 정본이 같은 저장소에 있다는 것이 이 개념의 핵심이다 — 저자들이 올바른 형태를 알고 있었고, 열 곳 남짓에서 쓰지 않았다.
|
||
- missing-verification: 어느 사례도 경합을 실행으로 재현하지 않았다. 전부 읽기와 쓰기가 별개 연산이라는 코드 형태로 판정했다
|
||
- relations: `reference:atomic-type-is-not-atomicity` · `reference:beginning-a-transition-and-the-set-it-covers-are-one-operation` · `case:the-same-rotation-defect-closed-once-and-reproduced` · `case:complete-drain-rolls-back-a-rotation`
|
||
|
||
### CASE — 회전이 비교 후 교체가 아니라 덮어쓰기이고, 세대 계수기는 음수가 되면 회수되지 않는다
|
||
- slug: `a-rotation-that-overwrites-and-a-generation-that-cannot-be-reclaimed`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-client.md` §17.1, §17.2
|
||
- code: `.../grpc-client/.../GrpcChannelRuntimeRegistry.java`(`rotate`) · `.../grpc-client/.../GrpcChannelRuntime.java`(`finishUnaryCall`·`closeStream`)
|
||
- evidence: 없음 — 읽기와 쓰기가 별개 연산이라는 코드 형태로 판정했다
|
||
- classification: 두 결함이 같은 형태다. `rotate` 는 현재 런타임을 `get()` 으로 읽어 판단한 뒤 조건 없는 `set` 을 한다 — 두 회전이 겹치면 나중 것이 먼저 것을 덮고, 덮인 쪽이 이미 반환한 채널은 회수되지 않는다. `finishUnaryCall` 과 `closeStream` 은 `get() > 0` 을 확인하고 별도 연산으로 감소시킨다 — 계수기가 음수가 되면 `quiescent()` 가 영원히 거짓이 되어 그 세대를 회수할 수 없다. 후자가 더 나쁜 이유는 되돌릴 경로가 없다는 것이다. 자격증명 회전에서 같은 형태가 이미 두 가족을 건너 재현됐고, 이 리프는 그 사슬의 세 번째 지점이다.
|
||
- missing-verification: 경합을 실행으로 재현하지 않았다
|
||
- relations: `concept:check-then-act-on-atomic-types` · `case:the-same-rotation-defect-closed-once-and-reproduced` · `case:complete-drain-rolls-back-a-rotation` · `reference:atomic-type-is-not-atomicity`
|
||
|
||
### CASE — 승인 경계가 동시성 아래에서 새고, 큐 계수기를 되돌리는 경로가 없다
|
||
- slug: `an-admission-boundary-that-leaks-and-a-counter-that-cannot-return`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-server.md` §17.4 · `analysis/grpc/grpc-policy.md` §17.1
|
||
- code: `.../grpc-server/.../GrpcAdmissionController.java`(`tryAdmit`·`release`·`promoteFromQueue`) · `.../grpc-policy/.../GrpcStreamAdmission.java`
|
||
- evidence: 없음 — 세 메서드의 읽기·쓰기 분리와 계수기 쌍 대조로 판정했다
|
||
- classification: 승인 제어기의 세 메서드가 전부 읽은 뒤 별도로 쓴다. 그 자체로 경계가 초과되고, 여기에 계수기 쌍의 비대칭이 겹친다 — 큐를 거쳐 들어온 승인은 `queued` 만 증가시키는데 `release()` 는 `inFlight` 만 감소시킨다. 그래서 큐를 거친 요청이 끝날 때마다 `queued` 가 줄지 않고 남는다. 경계가 한 번 새는 것과 경계가 영구히 느슨해지는 것은 다른 결함이고, 이 리프에는 둘 다 있다. 스트림 승인 쪽은 여기에 하나를 더한다 — 호출자별 맵이 줄지 않아 재접속 폭풍에서 가장 많이 샌다. 세 메서드 모두 production 호출자가 0 이므로 오늘의 사고는 아니다.
|
||
- missing-verification: 경합과 큐 경로를 실행으로 재현하지 않았다
|
||
- relations: `concept:check-then-act-on-atomic-types` · `reference:beginning-a-transition-and-the-set-it-covers-are-one-operation` · `reference:atomic-type-is-not-atomicity`
|
||
|
||
### CASE — 배수를 시작한 뒤에도 한 서비스가 다시 SERVING 이 될 수 있다
|
||
- slug: `draining-began-and-a-service-came-back-serving`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-admin.md` §17.4
|
||
- code: `.../grpc-admin/.../GrpcServiceHealthRegistry.java`(`beginDraining`·`markServing`·`recomputeGlobal`)
|
||
- evidence: 없음 — 두 메서드의 순서와 가드 위치 대조로 판정했다
|
||
- classification: `beginDraining()` 이 두 단계다 — 먼저 `draining = true` 를 쓰고, 그다음 `states.replaceAll(...)` 로 모든 서비스를 DRAINING 으로 바꾼다. `markServing` 은 첫 줄에서 `if (draining) return;` 으로 자기를 막는다. 그 가드를 통과한 스레드가 두 단계 사이에 쓰면, 그 서비스만 SERVING 으로 남고 `recomputeGlobal()` 이 전체 상태를 SERVING 으로 되돌린다. 배수 중인 인스턴스가 로드밸런서에 준비됐다고 답하는 상태이고, 배수의 목적이 정확히 그것을 막는 것이다. 같은 리프의 `rejectNewAdmission()` 이 단계만 기록하고 아무것도 거절하지 않는 것과 짝을 이룬다.
|
||
- missing-verification: 경합을 실행으로 재현하지 않았다
|
||
- relations: `concept:check-then-act-on-atomic-types` · `reference:beginning-a-transition-and-the-set-it-covers-are-one-operation` · `case:reject-new-admission-that-rejects-nothing`
|
||
|
||
### REFERENCE — 전이를 시작하는 쓰기와 그 전이가 덮는 집합은 한 연산이어야 한다
|
||
- slug: `beginning-a-transition-and-the-set-it-covers-are-one-operation`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-admin.md` §17.4 · `analysis/grpc/grpc-policy.md` §17.2
|
||
- classification: 상태 전이가 "플래그를 세운다" 와 "그 플래그가 지배하는 대상을 갱신한다" 두 단계로 나뉘면, 그 사이에 플래그를 읽고 통과한 쓰기가 존재한다. 그 쓰기는 전이 이전의 판단으로 전이 이후의 상태를 만든다. 판정은 "플래그를 읽는 가드와 그 플래그가 덮는 쓰기 사이에 다른 스레드가 낄 수 있는가" 이고, 낄 수 있으면 두 단계를 하나로 합치거나 그 구간을 락으로 닫아야 한다.
|
||
- scope: 배수·종료·회전·차단처럼 "이제부터 다르게 동작한다" 를 선언하는 모든 전이. 이 저장소의 사례 — 헬스 레지스트리의 배수 시작, 자격증명 회전의 배수 완료, 채널 런타임의 세대 교체.
|
||
- exceptions: 전이 이후의 쓰기가 무해하면(집합에 다시 넣어도 결과가 같으면) 두 단계로 나눠도 된다. 다만 그 무해함은 전이가 덮는 대상 전체에 대해 성립해야 하고, 대상이 늘어나면 다시 확인해야 한다.
|
||
- relations: `concept:check-then-act-on-atomic-types` · `case:draining-began-and-a-service-came-back-serving` · `case:complete-drain-rolls-back-a-rotation` · `reference:atomic-type-is-not-atomicity`
|
||
|
||
---
|
||
|
||
## TOPIC 19 — retention-and-unbounded-growth
|
||
|
||
### CONCEPT — bounded 와 unbounded 오버로드를 나란히 둔 포트
|
||
- slug: `bounded-and-unbounded-side-by-side`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md` §17 · `analysis/messaging/messaging-inbox-jdbc-postgresql.md` §17
|
||
- code: `.../messaging-reliability-api/.../InboxRepository.java:36-52` · `.../OutboxRepository.java:132-151`
|
||
- classification: 두 포트가 각각 `purge*Before(Instant)` 와 `purge*Before(Instant, int)` 를 나란히 선언한다. 뒤쪽이 안전한 형태이고 그 이유가 javadoc 에 있다 — "The cleanup jobs describe themselves as bounded by batch size; this is the parameter that makes that true." 두 형태를 가르는 표시는 없다. `@Deprecated` 도, 이름 차이도, 호출을 막는 가시성 차이도 없다. 그래서 호출자는 인자가 적은 쪽을 고른다. 같은 리프가 다른 곳에서 같은 형태를 이미 기록했다 — 한 인터페이스가 같은 전이의 두 세대를 갖고 안전하지 않은 쪽에 표시가 없다. 이 개념의 요지는 결함이 호출자에게 있지 않다는 것이다. 포트의 형태가 오용을 가능하게 했고, 두 리프에서 같은 방향으로 발생했다.
|
||
- missing-verification: 없음 — 두 포트의 선언과 저장소 전역 호출 지점을 전수 확인했다
|
||
- relations: `case:the-cleanup-that-causes-the-outage-it-prevents` · `reference:a-port-that-offers-both-forms-has-chosen-the-unsafe-one` · `reference:two-vocabularies-for-one-concept`
|
||
|
||
### CASE — cleanup 이 스스로 막겠다고 적은 장애를 일으키는 형태로 호출된다
|
||
- slug: `the-cleanup-that-causes-the-outage-it-prevents`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md` §17 P1 · `analysis/messaging/messaging-outbox-jdbc-postgresql.md` §17 P1
|
||
- code: `.../messaging-inbox-jdbc-postgresql/.../InboxCleanupJob.java:56` · `.../messaging-outbox-jdbc-postgresql/.../OutboxCleanupJob.java:50` · `.../JdbcInboxRepository.java:141` · `.../JdbcOutboxRepository.java:486`
|
||
- evidence: `evidence/raw/294-bounded-purge-never-called.txt`
|
||
- classification: 두 cleanup 잡이 무제한 오버로드를 부른다. bounded 구현은 `LIMIT` + `FOR UPDATE SKIP LOCKED` 로 두 리프 모두에 존재하고 호출 지점이 0 이다. `InboxCleanupJob` 의 javadoc 이 실행되는 코드의 동작을 그대로 서술한다 — "A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent." bounded 쪽 javadoc 은 한 발 더 나간다 — 배치 크기로 제한된다는 잡의 자기 서술을 참으로 만드는 것이 바로 이 파라미터라고 적는다. 그 파라미터를 아무도 넘기지 않는다. 두 리프 다 `app-bootstrap` 소속이고 두 잡 다 스타터 빈이지만 스케줄러가 등록되지 않는다 — 그것이 의도된 설계다. 그래서 상시 결함이 아니라 잠재 결함이고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 첫 스윕에서 발현한다. 회귀 테스트가 성립하려면 대역도 고쳐야 한다 — 현재 대역의 bounded 구현은 전부 지우고 숫자만 깎는 형태라 차이를 재현하지 못한다.
|
||
- missing-verification: 백로그가 쌓인 실제 테이블에서 두 형태의 락 보유 시간을 측정하지 않았다. 호출 지점 부재와 두 SQL 의 형태로 판정했다
|
||
- relations: `concept:bounded-and-unbounded-side-by-side` · `reference:a-port-that-offers-both-forms-has-chosen-the-unsafe-one` · `reference:a-fake-that-cannot-show-the-property-is-not-a-witness` · `case:outbox-chain-behind-an-unsatisfiable-condition`
|
||
|
||
### CASE — 재생 저장소와 중복 제거 맵에 제거 경로가 없다
|
||
- slug: `a-replay-store-with-no-eviction-path`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md` §17.3 · `analysis/grpc/grpc-advanced-streaming.md` §17.1
|
||
- code: `.../grpc-policy/.../GrpcResultReplayStore.java` · `.../grpc-advanced-streaming/.../GrpcClientMessageDeduplicator.java`
|
||
- evidence: 없음 — 두 자료구조의 삽입·제거 경로 대조로 판정했다
|
||
- classification: 결과 재생 저장소가 항목을 넣기만 하고 지우지 않는다. 만료·용량·세션 종료 어느 축으로도 제거 경로가 없으므로, 프로세스 수명 동안 단조 증가한다. 형제 리프의 중복 제거기가 같은 형태다 — 그 클래스는 다른 무제한 증가를 비판하는 javadoc 을 갖고 있으면서 자기 `replayableOutcomes` 맵을 세션 안에서 무제한으로 늘린다. 두 사례가 한 쌍인 이유는 둘 다 "정확히 한 번" 계열의 보장을 위해 과거를 기억하는 자료구조라는 점이다. 그 종류의 자료구조에서 보존 경계는 기능이 아니라 전제다 — 무엇을 언제까지 기억하는지가 정해지지 않으면 그 보장은 메모리가 버티는 동안만 성립한다.
|
||
- missing-verification: 장시간 실행으로 증가를 관측하지 않았다. 배선 경로가 없어 실행 대상이 없다
|
||
- relations: `concept:bounded-and-unbounded-side-by-side` · `reference:one-formula-and-one-enforcement-point-per-safety-rule` · `reference:runtime-membership-decides-severity`
|
||
|
||
### REFERENCE — 두 형태를 나란히 내놓는 포트는 이미 안전하지 않은 쪽을 고른 것이다
|
||
- slug: `a-port-that-offers-both-forms-has-chosen-the-unsafe-one`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md` §17 · `analysis/99-cross-scope.md` §3.1
|
||
- classification: 인터페이스가 안전한 형태와 그렇지 않은 형태를 함께 노출하면 호출자는 짧은 쪽을 고른다. 그것은 호출자의 부주의가 아니라 포트가 만든 기본값이다. 판정 기준은 "안전하지 않은 쪽을 부르는 것이 컴파일되는가" 이고, 컴파일된다면 그 형태는 언젠가 호출된다.
|
||
- scope: 정리·삭제·조회처럼 결과 크기가 데이터에 비례하는 모든 연산. 이 저장소의 사례 — inbox·outbox 의 두 purge 오버로드, outbox 전이의 두 세대 메서드.
|
||
- exceptions: 두 형태가 진짜로 다른 용도를 가지면 공존이 맞다. 그때는 이름이 그 차이를 말해야 하고(`purgeAll` 대 `purgeBatch`), 위험한 쪽에는 그것을 부르는 조건이 javadoc 에 있어야 한다. 오버로드로 두는 것은 그 차이를 이름에서 지우는 선택이다.
|
||
- relations: `concept:bounded-and-unbounded-side-by-side` · `case:the-cleanup-that-causes-the-outage-it-prevents` · `reference:two-vocabularies-for-one-concept`
|
||
|
||
### REFERENCE — 같은 안전 규칙은 하나의 공식과 하나의 강제 시점을 갖는다
|
||
- slug: `one-formula-and-one-enforcement-point-per-safety-rule`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md` §17 · `analysis/messaging/messaging-transport-spi.md` §17
|
||
- classification: 같은 종류의 안전 여유가 여러 곳에서 독립적으로 정해지면 공식과 강제 시점이 갈린다. 이 저장소에서 inbox 보존 여유는 한 곳에서 곱셈으로, 다른 곳에서 덧셈으로 표현되고, 한쪽은 cleanup 잡을 만들 때만 검증하며 다른 쪽은 항상 검증한다. 드레인 마감 30초는 세 곳에서 각자 정해지고 그중 public 상수만 테스트가 붙든다. 판정은 "이 값을 바꾸려면 몇 군데를 고쳐야 하는가" 이고, 하나가 아니면 나머지는 조용히 옛 값으로 남는다.
|
||
- scope: 보존 기간·마감·재시도 상한·배치 크기처럼 안전을 위해 고른 모든 수치. 소유자를 한 곳으로 정하고 나머지가 그것을 참조하게 한다.
|
||
- exceptions: 층마다 다른 값이 정당한 경우가 있다 — 클라이언트 마감이 서버 마감보다 짧아야 하는 것처럼. 그때는 값이 아니라 **관계**가 한 곳에 있어야 하고, 그 관계를 검증하는 코드가 있어야 한다.
|
||
- relations: `case:a-replay-store-with-no-eviction-path` · `case:an-order-contract-with-no-implementation` · `reference:numbers-in-docs-should-be-derived`
|
||
|
||
---
|
||
|
||
## TOPIC 20 — drain-and-shutdown-ordering
|
||
|
||
### CONCEPT — 8단계 종료 순서 계약과 실제 종료 경로
|
||
- slug: `the-eight-phase-shutdown-contract`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md` §17
|
||
- code: `.../messaging-transport-spi/.../MessagingLifecycle.java`(`ShutdownPhase`) · `.../messaging-spring-boot-starter/.../MessagingShutdownLifecycle.java`
|
||
- classification: `MessagingLifecycle` 이 종료를 8단계로 선언하고 javadoc 이 그 순서를 계약이라고 못 박는다 — "The order in ShutdownPhase is the contract, not an implementation detail. Each adapter implements the phases; none of them chooses the order." 순서가 계약인 이유는 각 단계가 앞 단계의 결과 위에 서기 때문이다. 새 승인을 멈추기 전에 드레인하면 드레인이 끝나지 않고, 핸들러가 끝나기 전에 플러시하면 그 핸들러가 만들 정착을 잃는다. 실제 종료는 스타터의 `MessagingShutdownLifecycle` 이 하고 8단계 중 셋만 명시적으로 수행한다 — 승인 정지 · 새 핸들러 정지 · 드레인. 나머지는 Spring 의 `getPhase()` 정수와 빈 소멸 순서에 위임되거나 명시 단계가 없다. 즉 순서를 결정하는 것은 `ShutdownPhase` 가 아니다.
|
||
- missing-verification: 실제 종료 시퀀스를 부팅해 관측하지 않았다. 두 클래스의 코드로 판정했다
|
||
- relations: `case:an-order-contract-with-no-implementation` · `case:reject-new-admission-that-rejects-nothing` · `case:the-last-step-of-secret-erasure-is-not-wired` · `reference:a-declaration-order-test-is-a-gate-only-if-something-reads-that-order`
|
||
|
||
### CASE — 순서 계약을 구현하는 것이 없고, 그것을 검증한다는 테스트는 enum 선언 순서만 본다
|
||
- slug: `an-order-contract-with-no-implementation`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md` §17 P2
|
||
- code: `.../messaging-transport-spi/.../MessagingLifecycle.java` · `.../MessagingLifecycleTest.java`
|
||
- evidence: `evidence/raw/280-transport-spi-lifecycle-unimplemented.txt`
|
||
- classification: 세 겹이다. 첫째, `MessagingLifecycle` 의 구현체가 저장소에 없고 `ShutdownPhase` 의 외부 소비자도 없다 — `MessagingTransport` 를 구현하는 네 어댑터 중 어느 것도 이 인터페이스를 구현하지 않는다. 둘째, 순서를 검증한다는 다섯 테스트가 전부 `List.of(ShutdownPhase.values()).indexOf(A) < indexOf(B)` 형태다. 통과하는 것은 시스템 동작이 아니라 **소스에 상수가 적힌 순서**이고, 테스트 이름과 `as(...)` 문구는 시스템 동작을 서술한다 — "flushing before the handlers finish would lose the settlements they produce". 이 테스트들은 enum 상수를 재배열하면 실패하고, 재배열해도 시스템은 바뀌지 않는다. 셋째, 인터페이스가 컴파일 강제를 만들지 않는다. `MessagingTransport` 는 구현하지 않으면 빌드가 깨지고, 이것은 아무것도 깨지지 않는다. 같은 리프의 드레인 마감 30초가 세 곳에서 독립적으로 정해지는 것이 같은 사건의 일부다 — 살아 있는 값(private 복사본)이 죽은 인터페이스의 public 상수를 참조하지 않는다.
|
||
- missing-verification: 없음 — 구현체 검색과 테스트 단언 형태를 전수 확인했다
|
||
- relations: `concept:the-eight-phase-shutdown-contract` · `reference:a-declaration-order-test-is-a-gate-only-if-something-reads-that-order` · `reference:one-formula-and-one-enforcement-point-per-safety-rule` · `reference:omission-that-passes-is-not-a-gate`
|
||
|
||
### CASE — 새 승인을 거절한다는 메서드가 단계만 기록하고 아무것도 거절하지 않는다
|
||
- slug: `reject-new-admission-that-rejects-nothing`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-admin.md` §17.1
|
||
- code: `.../grpc-admin/.../GrpcDrainCoordinator.java`(`rejectNewAdmission`)
|
||
- evidence: 없음 — 메서드 본문과 승인 경로 대조로 판정했다
|
||
- classification: 배수의 첫 단계는 새 요청을 받지 않는 것이다. `rejectNewAdmission()` 은 이름이 그것을 말하고, 본문은 현재 단계를 기록하는 것이 전부다. 승인을 실제로 판정하는 곳은 다른 리프의 승인 제어기이고, 그 제어기는 이 조정자의 단계를 읽지 않는다. 그래서 배수를 시작해도 새 요청은 계속 승인된다. 같은 리프의 헬스 레지스트리가 배수 중에 서비스를 다시 SERVING 으로 돌릴 수 있다는 것과 겹치면, 배수라는 절차 전체가 상태 기록으로만 존재하고 트래픽에 대해서는 아무 효과가 없다.
|
||
- missing-verification: 배수 중 승인 시도를 실행으로 재현하지 않았다. 두 리프 사이에 참조가 없다는 것으로 판정했다
|
||
- relations: `concept:the-eight-phase-shutdown-contract` · `case:draining-began-and-a-service-came-back-serving` · `reference:a-validator-is-enforced-by-injection`
|
||
|
||
### CASE — 비밀을 힙에서 지우는 마지막 단계가 종료 경로에 연결되지 않았다
|
||
- slug: `the-last-step-of-secret-erasure-is-not-wired`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md` §17
|
||
- code: `.../messaging-security/.../CredentialRuntimeRegistry.java`(`clearAll`)
|
||
- evidence: 없음 — `clearAll` 의 호출자 전수 검색으로 판정했다
|
||
- classification: 이 리프 전체가 "비밀이 힙에 남지 않게 한다" 를 목적으로 설계됐다 — 자격증명을 `char[]` 로 들고, 사용 후 `clear()` 하고, 회전 시 즉시 소거한다. `clearAll()` 의 javadoc 이 "Clears every held credential, for shutdown" 이라고 적고, 호출자가 저장소에 없다. 프로세스가 끝나면 힙도 사라지므로 사소해 보이지만, 종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 겨냥하는 상황이다. 8단계 종료 계약에 자격증명 소거 단계가 있고 그 단계를 수행하는 코드가 없다는 점에서, 이 사례는 계약이 구현되지 않았다는 사실의 구체적 결과 하나다.
|
||
- missing-verification: 힙 덤프로 잔존을 확인하지 않았다. 호출자 부재로 판정했다
|
||
- relations: `concept:the-eight-phase-shutdown-contract` · `case:an-order-contract-with-no-implementation` · `reference:a-validator-is-enforced-by-injection`
|
||
|
||
### REFERENCE — 선언 순서를 단언하는 테스트는 그 순서를 읽는 코드가 있을 때만 게이트다
|
||
- slug: `a-declaration-order-test-is-a-gate-only-if-something-reads-that-order`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md` §17 P2
|
||
- classification: enum 상수의 순서나 목록의 원소 순서를 단언하는 테스트는 소스에 적힌 순서를 지킨다. 그것이 시스템 동작을 지키려면 그 순서를 읽어 동작을 결정하는 코드가 있어야 한다. 없으면 그 테스트가 지키는 것은 타이핑 순서이고, 이름과 설명 메시지가 시스템 동작을 서술할수록 그 어긋남은 커진다. 판정은 한 줄이다 — 이 enum 의 `values()` 나 `ordinal()` 을 읽는 production 코드가 있는가.
|
||
- scope: 순서가 의미를 갖는 모든 열거형과 목록 — 종료 단계, 필터 체인, 실패 번역 사슬, 마이그레이션 순서.
|
||
- exceptions: 순서 자체가 문서인 경우가 있다. 그때는 테스트 이름이 "이 순서가 문서에 적힌 것과 같다" 여야 하고 시스템 동작을 주장하면 안 된다.
|
||
- relations: `case:an-order-contract-with-no-implementation` · `reference:omission-that-passes-is-not-a-gate` · `case:test-names-that-assert-what-their-bodies-do-not` · `concept:the-eight-phase-shutdown-contract`
|
||
|
||
---
|
||
|
||
### REFERENCE — 그 속성을 보일 수 없는 대역 위에서 통과한 테스트는 증인이 아니다
|
||
- slug: `a-fake-that-cannot-show-the-property-is-not-a-witness`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md` §17 · `analysis/messaging/messaging-admin-runtime.md` §17 P1
|
||
- classification: 테스트 대역이 실제 구현의 불변식을 재현하지 못하면, 그 대역 위에서 통과한 단언은 그 불변식에 대해 아무 말도 하지 않는다. 이름이 속성을 주장할수록 그 공백은 커진다. 판정은 "이 대역이 결함 있는 구현과 올바른 구현을 구분할 수 있는가" 이고, 구분하지 못하면 그 테스트는 회귀를 잡지 못한다.
|
||
- scope: 저장소·브로커·파일 시스템처럼 상태를 갖는 협력자의 인메모리 대역 전부. 이 저장소의 사례 — bounded purge 대역이 `Math.min(unbounded(), limit)` 로 전부 지우고 숫자만 깎는 것, 리드라이브 대역의 `settle` 이 staged 목록을 줄이지 않는 것, 청구 대역의 `save` 가 INSERT 를 흉내 내 UPSERT 와의 차이를 가리는 것.
|
||
- exceptions: 대역이 협력자의 존재만 필요로 하는 테스트에는 해당하지 않는다. 구분 기준은 "단언하는 속성이 그 협력자의 상태 변화에 달려 있는가" 이다.
|
||
- relations: `case:the-cleanup-that-causes-the-outage-it-prevents` · `case:a-resumed-redrive-skips-what-it-could-not-move` · `case:assigned-id-turns-claim-into-upsert` · `reference:omission-that-passes-is-not-a-gate`
|
||
|
||
---
|
||
|
||
## TOPIC 21 — operator-approval-and-destructive-operations
|
||
|
||
### CONCEPT — 승인·검증·실행의 분리와 그것을 타입으로 표현하기
|
||
- slug: `approval-verification-execution`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md` §4 · §17
|
||
- code: `.../messaging-admin-api/.../VerifiedApproval.java` · `.../ApprovalGrant.java`(`canonicalForm`) · `.../HmacApprovalVerifier.java` · `.../DestructiveOperationGuard.java`
|
||
- classification: 운영자 도구가 파괴적 작업을 부를 때 세 가지가 분리되어야 한다. 승인을 발급하는 능력, 그 승인이 진짜인지 검증하는 능력, 그리고 작업을 실행하는 능력. 이 리프가 그 분리를 타입으로 표현한 이력이 javadoc 에 남아 있다 — 예전에는 승인된 계획 타입이 public 생성자를 가진 평범한 record 라서 "이 계획은 승인됐다" 가 호출자가 자기에 대해 한 주장이었고, 실행 메서드에 닿을 수 있는 코드는 무엇이든 승인을 지어낼 수 있었다. 그 수정이 `VerifiedApproval` 이다 — 검증을 통과했다는 사실 자체를 타입으로 만들어 생성자를 막았다. 가드는 네 조건이 아니라 여섯을 본다(작업 종류 일치와 출처 일치가 javadoc 목록에 빠져 있다). 그중 다섯 번째의 인라인 주석이 이 개념의 요점을 말한다 — "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion."
|
||
- missing-verification: 이 표면 전체에 production 소비자가 없어 실행으로 확인한 것이 없다
|
||
- relations: `case:the-forgeable-approval-survived-on-the-irreversible-half` · `case:blocking-means-startup-fails-and-nothing-runs-it` · `reference:signing-and-verifying-do-not-share-an-object` · `reference:the-powerful-half-must-not-be-one-setting-away`
|
||
|
||
### CASE — 재개된 리드라이브가 옮기지 못한 메시지를 영구히 건너뛴다
|
||
- slug: `a-resumed-redrive-skips-what-it-could-not-move`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md` §17 P1
|
||
- code: `.../messaging-admin-runtime/.../RedriveService.java`(`resumeFrom` · `subList`) · `.../DefaultMessagingAdminService.executeRedrive` · `.../RedriveResumptionTest.java:192-200`
|
||
- evidence: `evidence/raw/306-redrive-resume-skips-failed-items.txt` · `evidence/raw/302-redrive-result-predicate-uncalled.txt`
|
||
- classification: 재개 지점 `resumeFrom` 은 "시도한 개수"(`moved + failed`)인데, `subList` 로 건너뛰는 대상은 매번 새로 peek 한 목록이고 그 목록에서 사라진 것은 성공한 것뿐이다. 실패분과 미시도분이 앞쪽에 남아 있으므로 건너뛰기가 정확히 그것들을 지운다. 결과: 리드라이브가 성공으로 보고되고, 승인이 소진되고, 일부 메시지가 DLQ 에 남으며, 어떤 기록도 그것들을 지목하지 않는다. 사건 복구 중에 실행되는 작업이라는 점이 심각도를 올린다. 그리고 이 결함을 잡는 술어가 이미 존재한다 — `RedriveResult.isFullyAccounted()` 의 production 호출부가 0 이다. 회귀 테스트가 성립하려면 대역도 고쳐야 한다. 현재 대역의 `settle` 은 `settled` 에 추가만 하고 `staged` 를 줄이지 않아, 실제 불변식인 "정착된 것은 다음 peek 에서 사라진다" 를 재현하지 못한다.
|
||
- missing-verification: 실제 브로커로 리드라이브를 재개해 관측하지 않았다. 인덱스 계산과 peek 결과의 변화로 판정했다
|
||
- relations: `concept:approval-verification-execution` · `reference:resume-by-identity-not-by-index` · `reference:a-fake-that-cannot-show-the-property-is-not-a-witness` · `reference:unknown-is-a-third-result`
|
||
|
||
### CASE — 위조 가능한 승인이 하필 되돌릴 수 없는 작업 쪽에만 남았다
|
||
- slug: `the-forgeable-approval-survived-on-the-irreversible-half`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md` §17 · `analysis/messaging/messaging-admin-api.md` §17
|
||
- code: `.../messaging-admin-runtime/.../DestructiveMessagingAdmin.java:23-38`(`Approved`) · `.../messaging-admin-api/.../VerifiedApproval.java:9-13`
|
||
- evidence: `evidence/raw/307-destructive-approval-not-verified.txt`
|
||
- classification: 승인된 계획을 위조할 수 없게 만드는 수정이 `REPLAY` 와 `REDRIVE` 에는 적용됐고 `PURGE` · `DELETE_DESTINATION` · `OFFSET_RESET` 에는 적용되지 않았다. 남은 쪽의 `Approved` record 는 public 생성자를 갖고 생성자가 null 과 음수만 본다 — 그 승인이 이 작업을 인가하는지, 이 목적지를 인가하는지, 영향 메시지 수가 승인 상한 이하인지 아무것도 검사하지 않고 계획 다이제스트 필드 자체가 없다. 방향이 뒤집혀 있다는 것이 이 사례의 요점이다. 적용된 두 작업은 복구 가능하고, 빠진 세 작업은 복구 불가능하다. 현재 구현체가 0 건이라 실행되는 결함은 아니지만, 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고 그 도구가 생기는 순간의 모양이 이것이다.
|
||
- missing-verification: 없음 — 두 record 의 생성자와 적용 범위를 대조했다
|
||
- relations: `concept:approval-verification-execution` · `reference:the-powerful-half-must-not-be-one-setting-away` · `reference:signing-and-verifying-do-not-share-an-object` · `case:apply-is-a-setting-approval-is-not`
|
||
|
||
### CASE — BLOCKING 이면 기동이 실패한다는 보장이 어떤 배선에서도 실행되지 않는다
|
||
- slug: `blocking-means-startup-fails-and-nothing-runs-it`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md` §17 P2
|
||
- code: `.../messaging-admin-api/.../TopologyIssue.java`(`Severity.BLOCKING`) · `.../TopologyValidationReport.java`(`requireAcceptable`) · `.../MessagingAdminDurabilityValidator.java`(대조군)
|
||
- evidence: `evidence/raw/302-messaging-admin-service-unwired.txt`
|
||
- classification: `BLOCKING` 의 javadoc 이 "The destination cannot deliver a declared guarantee; startup must fail" 이라고 적고, `requireAcceptable()` 의 javadoc 이 "Fails startup when any blocking issue was found" 라고 적는다. 그 메서드의 production 호출부가 0 이고, 그것을 부를 수 있는 유일한 진입점도 호출부가 0 이며, 그 서비스 빈을 스타터가 만들지 않는다. 결과: 복제 계수 1인 목적지에 내구성을 선언해도 컨텍스트는 정상 기동한다. 타입은 그 상황을 정확히 표현할 수 있고 표현한 것을 아무도 읽지 않는다. 고치는 방법이 같은 리프에 이미 있다 — `MessagingAdminDurabilityValidator` 가 `InitializingBean` 으로 저널 내구성을 기동 시점에 검사하고 실패시킨다. 같은 모양의 빈 하나면 된다. 이 항목이 무거운 이유는 이 리프가 `app-bootstrap` 소속이고 보장이 문서·타입·테스트 세 겹으로 존재하는데 배선만 없다는 점이다 — 읽는 사람은 보장이 있다고 믿을 근거를 세 개 갖는다. 그리고 토폴로지 검증 스택이 이 리프에 두 벌 있고 파티션 스케일업에 대한 판정이 서로 반대라, 배선하는 순간 어느 스택을 고르느냐가 스케일업한 배포의 기동 여부를 가른다.
|
||
- missing-verification: 컨텍스트를 세워 기동 성공을 관측하지 않았다. 호출부 부재로 판정했다
|
||
- relations: `concept:approval-verification-execution` · `reference:a-validator-is-enforced-by-injection` · `reference:the-startup-validator-follows-the-autoconfiguration-root` · `case:startup-validator-is-the-only-reader-of-four-keys`
|
||
|
||
### REFERENCE — 서명 능력과 검증 능력은 같은 객체에 두지 않는다
|
||
- slug: `signing-and-verifying-do-not-share-an-object`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md` §17
|
||
- classification: 대칭키 서명에서는 검증하려면 서명할 수 있는 키를 가져야 한다. 그러므로 승인을 검증하는 프로세스는 정의상 승인을 발급할 수 있고, 그 프로세스가 운영자 도구라면 "운영자가 자기 승인을 지어낼 수 없다" 는 성립하지 않는다. 판정은 객체가 아니라 키의 소재로 한다 — 검증하는 쪽이 서명 키를 쥐는가.
|
||
- scope: 승인·토큰·커서처럼 발급자와 검증자가 다른 모든 서명. 두 방향의 해법이 있다. 비대칭 서명으로 바꿔 검증 측이 공개키만 갖게 하거나, 발급자 타입을 분리해 "누가 서명 능력을 쥐는가" 를 타입에 드러낸다. 후자를 고를 때 정규 형식은 한 곳에 남겨야 두 번째 구현이 드리프트하지 않는다.
|
||
- exceptions: 발급과 검증이 같은 신뢰 경계 안에서만 일어나면 분리가 필요 없다. 다만 그 경계는 코드가 아니라 배포가 정하므로, 경계가 바뀔 수 있으면 지금 분리해 두는 편이 싸다.
|
||
- relations: `concept:approval-verification-execution` · `case:the-forgeable-approval-survived-on-the-irreversible-half` · `decision:legacy-adoption-requires-two-approvers` · `reference:a-binary-approval-codec-must-round-trip`
|
||
|
||
### REFERENCE — 재개는 인덱스가 아니라 신원으로 한다
|
||
- slug: `resume-by-identity-not-by-index`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md` §17 P1
|
||
- classification: 중단된 배치 작업을 "몇 개까지 했다" 로 재개하면, 재개 시점의 목록이 처음과 같아야 그 숫자가 유효하다. 목록이 처리 결과에 따라 줄어드는 종류라면 그 전제가 깨지고, 건너뛴 구간은 처리되지 않은 항목이 된다. 판정은 "재개 시점에 목록을 다시 만드는가" 이고, 다시 만든다면 재개 지점은 개수가 아니라 이미 처리한 항목의 신원이거나 브로커 오프셋이어야 한다.
|
||
- scope: 리드라이브·재생·마이그레이션처럼 승인과 저널을 갖는 모든 재개 가능 작업. 저널이 개수만 들고 있으면 필드를 늘려야 하고, 그것이 이 규칙의 실제 비용이다.
|
||
- exceptions: 목록이 불변이면 인덱스로 충분하다. 다만 "불변" 은 재개 사이에 다른 생산자가 없다는 뜻이며, DLQ 처럼 계속 유입되는 대상에는 성립하지 않는다.
|
||
- relations: `case:a-resumed-redrive-skips-what-it-could-not-move` · `reference:unknown-is-a-third-result` · `reference:a-fake-that-cannot-show-the-property-is-not-a-witness`
|
||
|
||
---
|
||
|
||
## TOPIC 22 — failure-category-across-adapters
|
||
|
||
### CASE — 권한 거부가 보안이 아니라 구성 오류로 기록된다
|
||
- slug: `an-authorization-denial-recorded-as-a-configuration-error`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md` §17 P2
|
||
- code: `.../messaging-security/.../DestinationAccessValidator.java`(`requirePublish`) · `.../messaging-runtime-core/.../DefaultMessagePublisher.java:104-116`(`rejected`)
|
||
- evidence: `evidence/raw/287-messaging-security-access-path.txt`
|
||
- classification: 권한 거부를 `AUTHORIZATION` 범주로 기록하는 코드가 있고 소비자가 0 이다. 실제 발행 경로는 접근 정책을 직접 묻고 거절을 만드는데, 그 거절 헬퍼가 붙이는 범주는 `CONFIGURATION` 이다. `FailureCategory` 는 "재시도 엔진과 DLQ 라우터와 대시보드가 함께 합의하는 안정된 분류" 로 정의되어 있으므로, 권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못하고 구성 오류 알림이 권한 거부로 오염된다. 그리고 `AUTHORIZATION` 을 쓰는 유일한 코드가 미사용 클래스 안에 있다 — 올바른 분류를 아는 코드와 실행되는 코드가 서로 다른 파일이다.
|
||
- missing-verification: 실제 거부를 발생시켜 대시보드 분류를 관측하지 않았다. 두 경로의 범주 상수 대조로 판정했다
|
||
- relations: `reference:a-category-is-a-contract-between-retry-dlq-and-dashboard` · `reference:check-which-duplicate-is-wired` · `concept:transport-failure-stage-and-category`
|
||
|
||
### CASE — 종료 중이라는 사정이 업무의 영구 실패로 분류된다
|
||
- slug: `a-closing-transport-reported-as-a-permanent-business-failure`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md` §17.2 · `analysis/messaging/messaging-pulsar-experimental.md` §17.2
|
||
- code: `.../messaging-nats-experimental/.../NatsJetStreamTransport.java`(`rejectedLocally`) · `.../messaging-pulsar-experimental/.../PulsarMessagingTransport.java`(`rejectedLocally`)
|
||
- evidence: 없음 — 두 헬퍼의 고정 범주와 호출자 목록 대조로 판정했다
|
||
- classification: 두 실험 어댑터가 같은 형태로 로컬 거절 헬퍼를 갖고, 그 헬퍼가 `FailureCategory.PERMANENT_BUSINESS` 를 고정으로 붙인다. 호출자가 둘씩이다 — 적재물이 상한을 넘은 경우와 전송이 종료 중인 경우. 첫째는 영구 업무 실패가 맞다. 둘째는 아니다. 종료 중이라는 것은 이 세대의 사정이고, 다음 세대나 다른 인스턴스에서는 같은 메시지가 정상 발행된다. 영구 업무 실패로 분류되면 재시도 엔진이 재시도하지 않고 DLQ 라우터가 그것을 최종 실패로 처리한다. 같은 파일의 실패 분류기는 범주를 신중히 나눈다 — 사전 거절은 `CONFIGURATION`, 모호는 `TRANSIENT_INFRASTRUCTURE`. 종료만 그 규율 밖에 있고, 두 리프가 같은 형태를 공유하므로 수정도 함께 해야 한다.
|
||
- missing-verification: 종료 중 발행을 실행해 재시도 엔진의 판정을 관측하지 않았다
|
||
- relations: `reference:a-category-is-a-contract-between-retry-dlq-and-dashboard` · `reference:this-generations-circumstance-is-not-a-permanent-failure` · `reference:retryability-needs-both-idempotency-and-category`
|
||
|
||
### REFERENCE — 실패 범주는 재시도·DLQ·대시보드 사이의 계약이다
|
||
- slug: `a-category-is-a-contract-between-retry-dlq-and-dashboard`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md` §4.12 · `analysis/messaging/messaging-security.md` §17
|
||
- classification: 실패 범주는 로그 문자열이 아니라 세 소비자가 각자 다른 행동을 고르는 입력이다. 재시도 엔진은 재시도 여부를, DLQ 라우터는 최종 처리를, 대시보드는 어느 담당자에게 보일지를 그 값으로 정한다. 그러므로 범주를 고르는 것은 "이 실패를 뭐라고 부를까" 가 아니라 "이 세 소비자가 각각 무엇을 해야 하는가" 를 동시에 정하는 결정이다. 판정은 범주를 붙이는 자리마다 세 질문에 답해 보는 것이다.
|
||
- scope: 어댑터가 만드는 모든 실패 서술자. 범주가 헬퍼 안에 고정되어 있으면 그 헬퍼의 호출자 전부에 대해 같은 답이 성립하는지 확인해야 한다 — 호출자가 둘 이상이면 대개 성립하지 않는다.
|
||
- exceptions: 분류할 수 없는 실패는 억지로 좁히지 말고 가장 보수적인 범주로 둔다. 이 저장소의 기본값이 일시적 인프라 실패인 것이 그 선택이다.
|
||
- relations: `case:an-authorization-denial-recorded-as-a-configuration-error` · `case:a-closing-transport-reported-as-a-permanent-business-failure` · `concept:transport-failure-stage-and-category` · `reference:retryability-needs-both-idempotency-and-category`
|
||
|
||
### REFERENCE — 이 세대의 사정은 업무의 영구 실패가 아니다
|
||
- slug: `this-generations-circumstance-is-not-a-permanent-failure`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md` §17.2 · `analysis/messaging/messaging-pulsar-experimental.md` §17.2
|
||
- classification: 종료 중 · 회전 중 · 재연결 중처럼 프로세스나 연결 세대의 상태 때문에 거절된 요청은 그 세대 밖에서 성립한다. 영구 실패는 요청 자체의 성질이어야 한다 — 적재물이 상한을 넘거나, 스키마가 맞지 않거나, 업무 규칙이 거절하는 경우다. 판정은 "다음 세대에서 같은 요청이 성공하는가" 이고, 성공한다면 그것은 일시적 실패다.
|
||
- scope: 로컬에서 만들어지는 모든 거절 — 전송 종료, 승인 거부, 자격증명 회전 중 거절. 거절 헬퍼가 범주를 고정으로 들고 있으면 호출자마다 이 질문을 다시 해야 한다.
|
||
- exceptions: 전송되지 않았다는 증거는 세대와 무관하게 유효하다. 범주가 틀렸다고 해서 전송 증거까지 바꾸면 안 된다 — 두 축은 독립이다.
|
||
- relations: `case:a-closing-transport-reported-as-a-permanent-business-failure` · `reference:a-category-is-a-contract-between-retry-dlq-and-dashboard` · `decision:three-axes-of-evidence`
|
||
|
||
---
|
||
|
||
## TOPIC 23 — contract-domain-and-bounds
|
||
|
||
> 값의 도메인과 경계 — 상한·보존·용량이 타입과 포트에서 어떻게 표현되는가. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — "실환경 증거" 가 두 리프에 반씩 있고 서로 만나지 않는다
|
||
- slug: `grpc-advanced-diagnostics-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-diagnostics.md#L218`
|
||
- owning-module: `grpc-advanced-diagnostics` · priority: `P3`
|
||
- classification: 이 리프가 능력별로 무엇이 실환경인지 정의한다. 그리고 `grpc-advanced-bootstrap` 이 승격 증거로 그것을 요구한다. `GrpcAdvancedPromotionGate.evaluate` 는 그 불리언이 거짓이면 "xds has no real environment test" 를 차단 사유로 낸다. 그 불리언을 무엇으로 채워야 하는지는 그쪽에서 답하지 않고, 답하는 코드가 이 리프에 있는데 두 쪽이 서로를 부르지 않는다. 결과: `GrpcAdvancedPromotionEvidence.complete(XDS, 7일)` 은 `realEnvironmentTest = true` 를 그냥 넣는다. xDS 통제 평면이 실제로 있었는지와 무관하다. 이 리프의 javadoc 이 경계한 상태 — "a suite that runs without the infrastructure passes and establishes nothing" — 를 승격 게이트가 그대로 통과시킬 수 있다. 두 리프 모두 배선되지 않았고 승격은 사람이 수행한다. 다만 이 두 조각이 존재하는 이유가 "그 판단을 코드로 적어 두는 것" 이므로, 판단의 절반이 다른 절반을 부르지 않는 것은 그 목적에 어긋난다. `grpc-advanced-edition` §17.2 가 같은 가족에서 같은 모양을 기록했다 — 두 승격 게이트가 서로를 부르지 않는다. `GrpcAdvancedPromotionEvidence.realEnvironmentTest` 를 불리언 대신 `Set<Infrastructure> availableInfrastructure` 로 바꾸고, 게이트가 `missingInfrastructure(capability, available)` 를 불러 그 결과를 차단 사유에 합친다. 그러면 "실환경 테스트를 했다" 가 선언이 아니라 능력별 목록에 대한 대조가 된다. 의존 방향도 맞는다 — 이 리프가 이미 bootstrap 을 의존하므로, 게이트가 이쪽을 부르려면 방향을 뒤집거나 `Infrastructu…
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 대체 선택기는 사용자 정의 선택기가 받는 보호를 받지 않는다
|
||
- slug: `grpc-advanced-resilience-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-resilience.md#L155`
|
||
- owning-module: `grpc-advanced-resilience` · priority: `P3`
|
||
- classification: `fallback.pick(selectable)` 은 감싸이지 않는다. 대체가 던지면 예외가 그대로 올라가고, 널이나 목록 밖 엔드포인트를 돌려주면 그대로 결정이 된다. 기본 생성자는 플랫폼의 라운드 로빈을 대체로 쓰므로 지금은 안전하다. 그러나 두 인자 생성자가 임의의 선택기를 대체로 받고, 그 인자에는 아무 제약이 없다. 이 클래스의 존재 이유가 "선택기 버그가 가용성이 아니라 균형을 저하시키게 하는 것" 인데, 대체 선택기의 버그는 가용성을 저하시킨다. 수정은 대체 호출도 같은 검사를 지나게 하거나(그 결과가 널이거나 목록 밖이면 플랫폼 라운드 로빈으로 한 번 더 떨어진다), 두 인자 생성자를 없애 대체를 플랫폼 것으로 고정하는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 클래스가 비판한 무제한 증가를 형제 맵이 그대로 한다
|
||
- slug: `grpc-advanced-streaming-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-streaming.md#L125`
|
||
- owning-module: `grpc-advanced-streaming` · priority: `P3`
|
||
- classification: 클래스 javadoc 이 집합 방식을 거부한 이유가 무제한 증가다 — "A set grows without bound for the life of a session". 체크포인트는 그 비판을 지킨다. 세션당 항목 하나이고 순번만 앞으로 간다. 형제 맵은 지키지 않는다. 제거는 `endSession` 뿐이고, 그때 그 세션의 접두를 가진 키를 전부 지운다. 그러므로 결과 참조를 기록하는 세션에서는 적용된 메시지 수만큼 항목이 쌓인다. 상한도 만료도 없다. 클래스 javadoc 은 다르게 말한다. 작은 창이 코드에 없다. 체크포인트가 앞으로 가도 그 이전 결과들은 남는다. 그리고 실제로 필요한 창은 좁다 — 판정이 `alreadyApplied(sequence)` 로 재생을 결정하고, 재생 응답에 쓰이는 것은 그 순번의 결과 하나다. 체크포인트보다 한참 뒤처진 순번의 결과가 필요할 상황은 재개 직후의 좁은 구간뿐이다. 수정은 창을 실제로 만드는 것이다 — 세션당 최근 N개만 유지하거나, 체크포인트가 앞으로 갈 때 그보다 오래된 항목을 지운다. 후자가 자바독의 서술과 정확히 같다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 클라이언트 스트림 정책의 네 상한 중 둘은 읽는 코드가 없다
|
||
- slug: `grpc-advanced-streaming-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-streaming.md#L159`
|
||
- owning-module: `grpc-advanced-streaming` · priority: `P3`
|
||
- classification: `GrpcClientStreamPolicy` javadoc 이 네 상한을 모두 든다. 저장소 전체에서 접근자 호출을 세면 둘이 0 이다. Advanced 가족이 미배선이라는 사실과는 별개다 — 이 리프 안에도 그 값을 쓰는 코드가 없다. 수요 상한을 강제하는 `GrpcDemandController` 는 `GrpcManualFlowControlPolicy` 를 쓰고, 이 정책을 보지 않는다. `wholeStreamRetryAllowed()` 는 항상 거짓을 돌려주는 형태이므로 그 자체가 문서화 장치다. 나머지 둘은 강제 지점이 필요하다. 수정은 상한을 강제하는 지점을 만들거나(수신 경로에 속도·미확인 수 검사), 강제되지 않는 값이 강제되는 것처럼 읽히지 않도록 자바독을 낮추는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 메타데이터 예산의 두 성분 중 하나는 강제되지 않고, 나머지 하나는 바이트가 아니라 문자를 센다
|
||
- slug: `grpc-core-api-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-core-api.md#L219`
|
||
- owning-module: `grpc-core-api` · priority: `P3`
|
||
- classification: `GrpcMetadataBudget` 은 세 성분을 갖는다 — `maxTotalBytes`·`maxUserDefinedBytes`·`maxEntries`. `check(...)` 가 보는 것은 뒤의 둘뿐이다. 저장소 전체에서 이 접근자를 부르는 곳은 compact constructor 의 순서 가드와 테스트 단언 하나뿐이다. 자바독은 그 이유를 설명한다 — 하드 총계를 넘기는 것은 프레임워크가 던지는 전송 거절이고, 여기서 함께 검사하면 "고칠 수 있는 쪽" 과 "고칠 수 없는 쪽" 이 한 자리에서 발견된다는 것. 판단은 옳다. 다만 그 결과로 이 record 는 자기가 쓰지 않는 수를 성분으로 들고 있고, 이름은 그것이 강제된다고 읽힌다. 성분 이름은 `...Bytes` 인데 세는 것은 `String.length()`, 즉 UTF-16 코드 단위다. 키는 `[a-z0-9._-]` 로 제한되어 ASCII 지만 값에는 문자 집합 제약이 없다. 다중 바이트 문자를 담은 값은 실제 프레임보다 적게 계산된다. gRPC 의 ASCII 메타데이터 값은 프로토콜 상 인쇄 가능 ASCII 여야 하므로 실무에서는 대개 일치한다. 다만 그 제약을 이 클래스가 검사하지 않으므로, 일치는 보장이 아니라 관행이다. 수정은 둘 다 작다 — `value.getBytes(StandardCharsets.US_ASCII).length` 로 세거나 값의 문자 집합을 `GrpcMetadataKey.Kind.ASCII` 에 맞춰 검증하고, `maxTotalBytes` 는 성분에서 빼고 javadoc 의 서술로 남긴다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 프로파일이 스트림 재접속 예산을 선언하는데 그것이 함의하는 DNS 갱신 주기를 정하지 않는다
|
||
- slug: `grpc-discovery-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-discovery.md#L152`
|
||
- owning-module: `grpc-discovery` · priority: `P3`
|
||
- classification: `GrpcKubernetesProfile` 은 세 시간 값을 다룬다. 검증기는 앞의 둘만 비교한다 — 배수 유예가 재접속 예산보다 짧으면 위반. 셋째는 비교 대상에 없다. 그래서 `headlessStreaming()`(재접속 예산 5초, 배수 유예 30초)에서 갱신 주기는 여전히 30초다. 롤아웃으로 스트림이 끊긴 클라이언트가 5초 예산 안에 재접속하려 할 때, 그 클라이언트의 DNS 캐시는 최대 30초 동안 사라진 파드 주소를 들고 있을 수 있다. 그 실패가 `GrpcResolverProfile` 자신의 javadoc 이 서술한 것이다 — "A channel that resolved once at startup keeps sending to addresses that stopped existing an hour ago; the calls fail with `UNAVAILABLE` and the deployment looks unhealthy long after it finished." 수정은 갱신 주기를 재접속 예산에서 파생시키거나(예: 예산 이하), 검증기에 세 값의 순서 규칙을 추가하는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 낙관적 잠금 컬럼이 없어 전이 가드가 메모리 안에만 있다
|
||
- slug: `grpc-operation-ledger-jpa-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-operation-ledger-jpa.md#L184`
|
||
- owning-module: `grpc-operation-ledger-jpa` · priority: `P3`
|
||
- classification: `requireInProgress()` 가 두 번째 종결 전이를 막는다. 그 가드는 한 영속성 컨텍스트 안의 인스턴스 상태에만 적용된다. 엔티티에 `@Version` 이 없으므로 두 트랜잭션이 같은 행을 읽어 각각 전이하면 나중 쓰기가 앞의 것을 덮는다. DB 의 세 CHECK 제약은 행의 모양을 지키지 지 전이 순서를 지키지 않는다. `COMMITTED` 행이 다른 결과 참조로 갱신되는 것을 막는 제약이 없다. 청구가 배타적이라는 설계 전제 아래서는 도달성이 낮다. 다만 §17.1 을 고치면 이 전제가 실제로 성립하는지가 함께 확인되어야 한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — 보존 sweep이 없다
|
||
- slug: `messaging-claim-check-f05`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L543`
|
||
- owning-module: `messaging-claim-check` · priority: `P3`
|
||
- classification: `ClaimCheckStore.delete`가 선언돼 있고 이 leaf에서 호출되지 않는다. `ClaimCheckPublisher` javadoc이 "the retention sweep reclaims it"이라고 그 존재를 전제한다. 실패한 발행이 남긴 객체를 회수할 주체가 없다. 저장소 자체의 lifecycle 정책(예: S3 object expiration)이 대신할 수 있으나 `ClaimCheckPolicy.retention`이 그것과 연결되지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `WireSafeText`의 규칙이 leaf 경계에서 멈춘다
|
||
- slug: `messaging-core-api-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L880`
|
||
- owning-module: `messaging-core-api` · priority: `P3`
|
||
- classification: `WireSafeText`의 leaf 밖 참조 0. 제어문자·인코딩 경계를 각자 검사하는 곳이 저장소에 최소 15개. javadoc이 "Each copy of this check ... was one more place for the rule to drift"라고 적었고 그 통합을 leaf 안에서만 했다. 저장소 수준에서는 같은 drift가 그대로 남아 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 컬럼 폭은 애플리케이션 검증과 짝을 이룬다
|
||
- slug: `messaging-inbox-jdbc-postgresql-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L684`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- rule: 컬럼 폭은 애플리케이션 검증과 짝을 이룬다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `consumer_id` 길이 제약이 애플리케이션 층에 없다
|
||
- scope: 멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다
|
||
- slug: `messaging-inbox-jdbc-postgresql-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L693`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- rule: 같은 안전 규칙은 한 공식과 한 강제 시점을 갖는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 보존 규칙이 세 곳에 있고 공식이 다르다
|
||
- scope: 멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — `extract`가 손상된 추적 헤더에 분류되지 않은 예외를 던진다
|
||
- slug: `messaging-observability-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L726`
|
||
- owning-module: `messaging-observability` · priority: `P3`
|
||
- classification: `MessagingTracer.extract`가 `new TraceContext(...)`를 부르고, 그 생성자는 W3C 문법·바이트 상한·all-zero를 검사해 `IllegalArgumentException`을 던진다. `extract`는 잡지 않는다. 다른 시스템이 보낸 메시지의 헤더는 신뢰할 수 없는 입력이다. 손상된 `traceparent` 하나가 `MessagingException`이 아닌 예외로 소비 경로를 끊는다 — 추적이 없어야 할 자리에서 메시지 처리가 실패한다. `messaging-cloudevents`의 id 파싱과 같은 형태다(그쪽 §17).
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — inbox 보존 규칙이 문서로만 있다
|
||
- slug: `messaging-reliability-api-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L725`
|
||
- owning-module: `messaging-reliability-api` · priority: `P3`
|
||
- classification: `InboxRepository.purgeProcessedBefore` javadoc이 "Retention must outlive the broker's maximum redelivery window, otherwise a late redelivery arrives after its inbox row was pruned and is processed a second time"라고 한다. 그 비교를 하는 코드가 이 leaf에도 `messaging-policy`의 프로파일 검증기에도 없다. 위반의 결과가 **부작용의 이중 실행**이다 — Inbox가 존재하는 이유 그 자체가 무효화된다. 그리고 위반이 조용하다: 짧은 보존은 정상 동작처럼 보이고 늦은 재전달이 올 때만 드러난다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 포트가 bounded/unbounded purge 두 오버로드를 나란히 노출하고, 호출자가 무제한 쪽을 고른다
|
||
- slug: `messaging-reliability-api-f08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L752`
|
||
- owning-module: `messaging-reliability-api` · priority: `P3`
|
||
- classification: `InboxRepository`와 `OutboxRepository`가 각각 `purge*Before(Instant)`와 `purge*Before(Instant, int)`를 선언한다. 후자에 호출 지점이 0이고 두 cleanup job이 전자를 부른다. §12.1(a)의 두 세대 전이와 같은 형태다 — **한 인터페이스가 안전한 형태와 그렇지 않은 형태를 나란히 두고, `@Deprecated`도 이름 차이도 없으며, 호출자가 짧은 쪽을 골랐다.** 두 경우 모두 포트의 형태가 오용을 가능하게 했다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 24 — runtime-reachability-and-composition
|
||
|
||
> 만들어진 것이 실행 경로에 닿는가 — 배선·호출자·소비자의 유무. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — 통합 다리의 메타데이터 조립이 메타데이터 예산을 검사하지 않는다
|
||
- slug: `grpc-advanced-compat-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-compat.md#L120`
|
||
- owning-module: `grpc-advanced-compat` · priority: `P3`
|
||
- classification: 허용 목록으로 키를 거르지만 값의 크기도, 합계도 보지 않는다. 클래스 javadoc 자신이 예산을 이 정책의 이유 중 하나로 든다 — 흐름의 내부 배관을 네트워크로 보내면 "it counts against the metadata budget." 그리고 같은 저장소의 다른 메타데이터 경로는 예산을 검사한다. `String.valueOf(value)` 이므로 헤더 값이 임의의 객체일 때 그 문자열 표현이 그대로 실린다. Spring Integration 헤더에는 컬렉션이나 도메인 객체가 흔히 들어가므로 값 하나가 클 수 있다. 수정은 이 record 에 `GrpcMetadataBudget` 를 성분으로 추가하고 `metadataFrom` 끝에서 검사하는 것이다. 형태가 이미 옆 리프에 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 정책 목록의 가장 강한 성질을 이 저장소에서는 쓸 수 없다
|
||
- slug: `grpc-core-api-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-core-api.md#L140`
|
||
- owning-module: `grpc-core-api` · priority: `P3`
|
||
- classification: `withDescriptorMethods` 를 부르는 곳은 이 리프의 테스트 두 줄뿐이다. `GrpcMethodPolicyCatalog.builder()` 를 부르는 곳은 저장소 전체에서 전부 테스트다. 그리고 그중 어느 것도 서술자 집합을 선언하지 않는다(위 두 줄 제외). 자바독이 그 상태를 미리 서술한다 — 서술자가 없으면 "the catalog is materially weaker … there is nothing to compare a policy's method name against." 그리고 서술자가 없는 이유는 옆 리프에 있다. `grpc-codegen` 이 서술자 산출물을 정의하지만 저장소에 protobuf 플러그인이 없어 `protoc` 이 돌지 않는다. 즉 이름 변경을 잡는 성질은 코드 생성 레인이 켜지기 전까지 성립할 수 없다. 기록하는 이유는 이것이 이 클래스가 존재하는 첫 번째 이유로 적혀 있기 때문이다. 수정은 코드 생성 레인이 생길 때 그 서술자를 목록 조립에 연결하는 것이고, 그때까지는 자바독이 그 조건을 명시하는 편이 낫다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `queueHighWatermark` 는 요구되고 검증되지만 아무도 읽지 않는다
|
||
- slug: `grpc-observability-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-observability.md#L197`
|
||
- owning-module: `grpc-observability` · priority: `P3`
|
||
- classification: `GrpcStreamObservation` 의 7성분 중 `queueHighWatermark` 만 소비자가 없다. `tags()` 에 없고, `GrpcObservationConvention.record(GrpcStreamObservation)` 이 등록하는 세 meter(`STREAM_LIFETIME`·`STREAM_MESSAGES`·`STREAM_FLOW_CONTROL_STALLS`) 어디에도 들어가지 않는다. 테스트도 `250L` 을 넘기고 그 값에 대해 아무것도 단언하지 않는다. 클래스 javadoc 이 "what is recorded instead is …" 로 세 가지를 열거하는데 그 목록에도 없다. 즉 서술과 구현은 일치하고, 어긋난 것은 **필수 생성자 인자**라는 점이다. 호출자는 측정해서 넘겨야 하고 그 값은 버려진다. 수정은 둘 중 하나다 — `STREAM_QUEUE_HIGH_WATERMARK` gauge/counter 를 추가하거나, 성분에서 뺀다. 큐 최고 수위는 소비자 지연의 직접 지표이므로 전자가 이 클래스의 목적에 맞는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `clearAfterTask` 는 합법 값이 하나뿐인 성분이고, 아무도 읽지 않는다
|
||
- slug: `grpc-policy-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L324`
|
||
- owning-module: `grpc-policy` · priority: `P3`
|
||
- classification: `false` 를 무조건 거부하므로 이 성분이 가질 수 있는 값은 `true` 하나다. 그리고 저장소 전체에서 `clearAfterTask()` 를 읽는 production 코드가 없다 — 호출처는 이 생성자의 가드와 테스트의 단언 한 줄뿐이다. 읽지 않아도 되는 이유는 `GrpcContextBinder` 가 옳게 쓰였기 때문이다. `runWith`·`callWith`·`wrap` 이 전부 `finally` 에서 detach 한다. 불변식이 이미 구조로 지켜진다. 그래서 이 성분은 설정처럼 보이지만 설정이 아니다. 읽는 사람은 정책으로 끌 수 있는 것이라고 읽고, 테스트는 그 가드를 시험한다. 수정은 성분을 지우고 javadoc 에 "always cleared" 를 남기는 것이다. 그러면 `backgroundWork()`·`stable()` 이 인자 하나가 되고, 불변식은 검증이 아니라 구조가 된다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 빌더 경로에서 순서 규칙 넷 중 셋이 발화할 수 없다
|
||
- slug: `grpc-server-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-server.md#L196`
|
||
- owning-module: `grpc-server` · priority: `P3`
|
||
- classification: `byStage` 는 `EnumMap` 이므로 `keySet()` 은 언제나 열거형 선언 순서다. 그리고 `stage(...)` 가 같은 단계의 두 번째 등록을 이미 거부한다. 따라서 빌더가 만드는 목록에서는 중복도, 역순도, 예외 경계가 최외곽이 아닌 경우도 발생할 수 없다. 발화 가능한 규칙은 필수 단계 누락 하나다. 결함은 아니다 — 나머지 셋은 `violations(List)` 를 직접 부르는 외부 호출자를 위한 것이고, 테스트가 그 경로로 셋을 모두 확인한다. 기록하는 이유는 빌더를 쓰는 조립 코드가 그 셋의 보호를 받는다고 읽기 쉽기 때문이다. 실제 보호는 자료구조가 준다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 시작 검증기가 시작 시 실행되지 않는다
|
||
- slug: `grpc-spring-boot-starter-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-spring-boot-starter.md#L183`
|
||
- owning-module: `grpc-spring-boot-starter` · priority: `P2`
|
||
- classification: `GrpcPlatformStartupValidator` 를 이름으로 부르는 파일은 둘뿐이다 — 자기 자신과 자기 테스트. `GrpcPlatformAutoConfiguration` 은 빈 9개를 만들고 `requireValid` 를 부르지 않는다. 초기화 콜백도, `@PostConstruct` 도, `ApplicationRunner` 도 없다. 그래서 클래스 javadoc 이 약속한 성질이 성립하지 않는다 — "Refuses to start on a configuration that would be wrong in a way nobody would notice." 지금은 그 설정으로 그냥 시작한다. 검증기가 유일한 소비자인 설정 키가 넷이다. `transport` — production 이 아닌 전송을 거부할 곳이 없다. 게다가 자동 설정은 이 값을 보지 않고 `GrpcServerProfile.stableNetty(...)` 를 하드코딩한다(§17.2). `tls-enabled` · `trust-all-certificates` — 배포 환경의 TLS 바닥을 강제할 곳이 없다. `operation-ledger-enabled` — 멱등 키 필수 메서드가 원장 없이 열리는 것을 막을 곳이 없다. 같은 저장소가 이 형태를 두 번 기록했다 — `WebPlatformStartupValidator` 가 시작 시 실행되지 않고, `BrokerAclManifest` 의 시작 자기점검이 없다. 반대로 messaging 의 `StartupProfileValidation` 은 `InitializingBean.afterPropertiesSet` 으로 돌려 그 문제를 이미 한 번 해결했고, fileserver 는 `attestMapping()` 을 app-bootstrap 의 `@Bean` 으로 연결했다. 정본이 저장소 안에 둘 있다. `violations` 는 넷을 받는다. 넷 중 셋에 생산자가 없다. 특히 마지막은 "스타터가 해석한 모듈 id 집합" 인데, 그것을 실행 중에 산출하는…
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `default-unary-deadline` 은 읽는 코드가 저장소에 없다
|
||
- slug: `grpc-spring-boot-starter-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-spring-boot-starter.md#L236`
|
||
- owning-module: `grpc-spring-boot-starter` · priority: `P3`
|
||
- classification: `getDefaultUnaryDeadline()` 의 호출자가 0 이다. 검증기도 이 값을 쓰지 않는다 — 검증기가 보는 것은 정책 목록의 `policy.deadline().usable()` 이고 그 값이 0 이면 위반을 낸다. 즉 자바독이 말하는 "선언하지 않은 메서드에 적용되는 기본 마감" 을 적용하는 코드가 없다. `ignoreUnknownFields = false` 라서 이 키를 설정하는 것은 성공하고 아무 효과가 없다. 수정은 그 기본값을 실제로 적용하는 지점을 만들거나(정책 목록 조립 시), 필드를 제거하는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 계약 스위트 둘이 결과를 만드는 코드를 갖지 않는다
|
||
- slug: `grpc-testkit-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-testkit.md#L245`
|
||
- owning-module: `grpc-testkit` · priority: `P3`
|
||
- classification: `GrpcUnaryReliabilityContract` 와 `GrpcServerStreamingContract` 는 순수 평가기다 — `List<Result>` 를 받아 위반을 돌려준다. 시나리오 정의(단항 3 · 스트리밍 5)와 그 정합성 검사는 훌륭하다. 스위트가 자기 커버리지를 열거하고, 돌지 않은 시나리오를 침묵이 아니라 위반으로 만든다. 빠진 것은 그 시나리오를 **돌리는** 쪽이다. `GrpcUnaryContractResult`·`GrpcStreamingContractResult` 를 만드는 코드는 저장소 전체에서 두 테스트뿐이고, 둘 다 리터럴로 만든다. 그래서 "이 플랫폼은 비멱등 변경을 재시도하지 않는다" 를 뒷받침하는 것은, 그 문장을 리터럴로 적은 뒤 평가기가 그것을 읽고 위반이 없다고 답하는 절차다. 평가기의 산술은 옳고, 대상이 관측이 아니다. in-process 픽스처(§1)는 이 시나리오들을 돌릴 재료를 이미 갖고 있다 — 인터셉터를 끼운 서버, 상태 매핑, 스트리밍 핸들러. 수정은 픽스처 위에서 세 시나리오를 실행해 `attempts`·`businessInvocations` 를 세는 러너를 두는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `TopologyManagementMode` 가 어디에도 연결되어 있지 않다
|
||
- slug: `messaging-admin-api-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L952`
|
||
- owning-module: `messaging-admin-api` · priority: `P3`
|
||
- classification: 자기 선언과 테스트 4건이 전부다. 이 enum 을 읽는 프로덕션 코드도, 이것으로 매핑되는 설정 프로퍼티도 없다(`EVD-302`). 두 선택지가 있다: 실제로 배선하거나(선언된 토폴로지 관리 모드를 설정에서 읽고 `requireSafeFor(isProduction)` 를 기동 시 호출), 제거한다. 지금 상태는 "규칙이 코드에 있다" 는 인상만 준다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 운영자용 표면 전체에 프로덕션 소비자가 없다
|
||
- slug: `messaging-admin-api-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L956`
|
||
- owning-module: `messaging-admin-api` · priority: `P3`
|
||
- classification: `ReplayPlan.describeImpact`, `RedrivePlan.describeImpact`, `ReplayResult.fellShortOfTheEstimate`, `RedriveResult.isFullyAccounted`, `AdminOperationLease.isResumption` — 다섯 개가 전부 테스트에서만 호출된다(`EVD-302`). 이것들은 잉여 코드가 아니라 **아직 소비자가 없는 잘 설계된 표면**이다. `describeImpact` 의 javadoc 이 "operator-facing" 이라고 쓰고 `ApprovedPlanExecutionTest.aReplayIntoTheLiveGroupSaysSoInCapitals` 가 대문자 `LIVE` 까지 검증한다. 문제는 그 문자열이 도달할 화면이 없다는 것이다. admin API·CLI 계층을 만들 때 이 다섯이 그 계층의 명세라는 점을 문서에 남겨 두는 것이 낫다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `messaging-policy` 의존이 import 0건이다
|
||
- slug: `messaging-admin-api-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L968`
|
||
- owning-module: `messaging-admin-api` · priority: `P3`
|
||
- classification: 선언만 남아 있다. 제거 후보.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 오케스트레이터가 어디에서도 실행되지 않는다
|
||
- slug: `messaging-admin-runtime-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L918`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P2`
|
||
- classification: `DefaultMessagingAdminService` 257줄과 `ReplayService` 99줄이 프로덕션에서도 테스트에서도 인스턴스화되지 않는다(`EVD-307`). 검사 순서·저널 시퀀스·실패 시 재던짐·실패 코드 정제가 전부 미검증이다. `DefaultMessagingAdminService` 의 생성자는 10개 인자를 받고 그중 8개가 SPI 또는 `Supplier` 이므로, 대역으로 조립하는 테스트를 쓰는 비용은 낮다. §12.1(a)의 회귀 테스트도 이 층에서 쓰는 것이 자연스럽다 — 저널·리스·리드라이브 루프가 함께 도는 것이 결함이 나타나는 조건이기 때문이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — public 인터페이스를 패키지 밖에서 구현할 수 없다
|
||
- slug: `messaging-admin-runtime-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L924`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P3`
|
||
- classification: `RedriveEstimator`(public)의 반환 타입 `RedriveEstimate` 가 package-private 이다(`EVD-308`). `DefaultMessagingAdminService` 의 public 생성자가 그 인터페이스를 요구하므로, 외부 조립이 불가능하다. `RedriveEstimate` 를 public 으로 올리는 것이 최소 수정이다. 더 나은 방향은 `DefaultMessagingAdminService` 밖의 최상위 record 로 꺼내는 것 — 지금은 오케스트레이터의 내부 타입이 SPI 계약의 일부가 되어 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 선언된 의존 6개 중 3개가 import 0건
|
||
- slug: `messaging-admin-runtime-f09`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L957`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P3`
|
||
- classification: `messaging-policy`, `messaging-transport-spi`, `messaging-security`. 제거 후보.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 배포 아티팩트가 싣지만 아무도 부르지 않고, 다른 곳의 에러 메시지가 이 경로를 권한다
|
||
- slug: `messaging-claim-check-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L507`
|
||
- owning-module: `messaging-claim-check` · priority: `P2`
|
||
- classification: 여섯 타입 전부 leaf 밖 참조 0, `ClaimCheckStore` 구현이 테스트 fake뿐, 조립 0건. 그런데 `runtime_memberships`가 `["app-bootstrap"]`이고 starter의 `allowed_dependencies`에 포함된다. 그리고 `messaging-policy`의 `PayloadLimitGuard`가 상한 초과 payload를 거절하며 `"payload of %d bytes exceeds the %d byte limit for %s; use claim check"`라고 안내한다. 운영자가 상한 초과 오류를 보고 안내대로 claim check를 켜려 해도 켤 것이 없다 — 저장소 구현도, bean도, 오프로드를 부르는 발행 경로도 없다. 그리고 `DestinationProfile`이 `claimCheckThresholdBytes`를 선언하고 검증까지 하므로 **설정 표면은 존재한다.** 설정할 수 있고 아무 효과가 없는 값이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다
|
||
- slug: `messaging-claim-check-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L534`
|
||
- owning-module: `messaging-claim-check`
|
||
- rule: leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `ClaimCheckPublisher`가 이 leaf의 테스트에 등장하지 않는다
|
||
- scope: 멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 배포 아티팩트가 싣지만 아무도 부르지 않는다
|
||
- slug: `messaging-cloudevents-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L529`
|
||
- owning-module: `messaging-cloudevents` · priority: `P2`
|
||
- classification: 세 타입의 leaf 밖 참조가 0인데 `runtime_memberships`가 `["app-bootstrap"]`이다. `messaging-spring-boot-starter`의 의존 목록에 있어 `cloudevents-api`·`cloudevents-core` 두 jar가 런타임 classpath에 오른다. starter에 `CloudEventMapper`를 만드는 `@Bean`이 없다. 형제 Avro·Protobuf는 소비자 0과 membership `[]`이 일치하는 정합적 incubating 상태다. 이 leaf만 어긋난다. 오늘 실행되는 코드가 없으므로 사고는 아니지만, 아티팩트 크기와 "이 의존성이 왜 있지"의 조사 비용이 남는다. 그리고 `support-matrix.md:23`이 "모든 messaging leaf가 unwired"라고 적고 있어 문서에서도 이 사실을 알 수 없다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — 선언된 핸들러 계약이 배선된 것과 다르다
|
||
- slug: `messaging-core-api-f01`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-core-api.md#L835`
|
||
- owning-module: `messaging-core-api` · priority: `P2`
|
||
- classification: `MessageHandler<T>`(`delivery/MessageHandler.java:14`)의 저장소 전체 참조가 0이다. 핸들러 결과를 정산으로 바꾸는 유일한 지점 `DefaultDeliveryProcessor`는 `Function<MessageEnvelope<EncodedMessage>, HandleResult>`를 받는다. `MessageDelivery`가 빠지면서 `deliveryAttempt`·`redelivered`·`handlerDeadline`·`shutdownRequested`가 핸들러에 도달할 수 없다. `DeliveryContext`의 javadoc이 설명하는 graceful drain 협력은 현재 배선으로는 성립하지 않는다. 그리고 새 소비자를 붙이는 사람은 공개 API에서 `MessageHandler`를 먼저 보게 되는데, 그것을 구현해도 아무 데도 꽂히지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — 12개 예외가 선언만 되어 있다
|
||
- slug: `messaging-core-api-f03`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-core-api.md#L862`
|
||
- owning-module: `messaging-core-api` · priority: `P3`
|
||
- classification: 23개 구체 예외 중 12개가 leaf 밖 참조 0이다(§6.2 표). 지금 당장 깨지는 것은 없다. 다만 `MessagePublishAmbiguousException`처럼 설계의 중심 개념에 이름을 준 타입이 던져지지 않으면, 그 개념이 실제로 어떤 경로로 표현되는지(결과 record)를 읽는 사람이 스스로 알아내야 한다. 그리고 `src/messaging/CLAUDE.md:44` — "새 public 타입은 그 모듈의 계약이다. 삭제·시그니처 변경은 breaking change로 취급한다" — 때문에 나중에 정리하는 비용이 계속 커진다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — bounded purge가 구현돼 있고 호출되지 않아, cleanup이 스스로 막겠다고 한 장애를 일으킨다
|
||
- slug: `messaging-inbox-jdbc-postgresql-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L647`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql` · priority: `P1`
|
||
- classification: `InboxRepository`·`OutboxRepository` 둘 다 `purge*Before(Instant, int)` 오버로드를 선언하고, `JdbcInboxRepository:141`·`JdbcOutboxRepository:486`이 `LIMIT` + `FOR UPDATE SKIP LOCKED`로 구현한다. 저장소 전체에서 그 시그니처가 등장하는 9곳은 **선언 2 + 구현 2 + 테스트 fake override 5**이고 **호출 지점이 0**이다. `InboxCleanupJob:56`과 `OutboxCleanupJob:50`이 무제한 오버로드를 부른다. `InboxCleanupJob.DEFAULT_BATCH_SIZE = 1_000`은 자기 선언 한 줄만 존재한다. `InboxCleanupJob`의 javadoc이 스스로 적는다 — *"A single unbounded DELETE over a table that has been accumulating for weeks holds locks long enough to block the very reservations the inbox exists to serve, so the cleanup would cause the outage it is meant to prevent."* 실행되는 코드가 정확히 그 문장이 서술하는 동작이다. `OutboxRepository`의 bounded 오버로드 javadoc은 한 발 더 나간다 — *"The cleanup jobs describe themselves as bounded by batch size; **this is the parameter that makes that true**."* 그 파라미터를 아무도 넘기지 않는다. 그리고 두 leaf가 **동일한 형태로** 그렇다. **왜 P1인가.** 두 leaf 다 `runtime_memberships: ["app-bootstrap"]`이고 두 cleanup job이 starter에서 bean으로 만들어진다(`Mes…
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다
|
||
- slug: `messaging-kafka-share-experimental-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L476`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- rule: 허용 의존 목록은 상한이므로 미사용을 잡지 않는다 — 그것을 잡으려면 별도 검사가 필요하다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 선언된 의존 셋이 사용되지 않는다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### OPEN QUESTION — 형제 어댑터 넷이 구현하는 SPI를 이 leaf만 구현하지 않는다
|
||
- slug: `messaging-kafka-share-experimental-f03`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L485`
|
||
- owning-module: `messaging-kafka-share-experimental` · priority: `P3`
|
||
- classification: `KafkaMessagingTransport`·`RabbitMessagingTransport`·`PulsarMessagingTransport`·`NatsJetStreamTransport`가 전부 `MessagingTransport`를 구현한다. 이 leaf는 `TransportConsumerRegistration`만 부분 구현한다. `KafkaShareWorkQueueCapability`가 존재하는 이유("shared validators refuse … before a message is ever produced")가 실현되려면 `MessagingTransport.capabilities(DestinationName)`를 통해 값이 전달돼야 한다. 그 인터페이스를 구현하지 않으므로 capability는 아무도 읽지 않는 상수다. 두 experimental 형제(pulsar, nats)는 구현하므로 "experimental이라서"가 이유가 되지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 관측 구현이 조립되지 않고, 그 재료 둘만 bean으로 존재한다
|
||
- slug: `messaging-observability-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L682`
|
||
- owning-module: `messaging-observability` · priority: `P2`
|
||
- classification: `MessagingMetrics`는 `MessagingObservation`의 유일한 구현이고 저장소 전체에서 자기 테스트에서만 생성된다. starter는 그 생성자 인자 둘(`MessagingRedactor:253`, `CardinalityGuard:264`)을 bean으로 만들고 `MessagingMetrics` bean은 만들지 않는다. `DefaultMessagePublisher`는 `NO_OBSERVATION`을 쓰는 6인자 생성자로 조립된다. 재료·구현·seam·호출부가 전부 있고 조립 한 줄이 없다. 그리고 두 재료 bean은 주입처가 0이므로 컨텍스트에 앉아만 있다 — bean 존재 검사는 통과한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 정리 작업이 무제한 DELETE 를 쏘고, 그것을 막는 오버로드는 호출되지 않는다
|
||
- slug: `messaging-outbox-jdbc-postgresql-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L876`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql` · priority: `P1`
|
||
- classification: `OutboxCleanupJob:50` 과 `InboxCleanupJob:56` 이 무제한 오버로드를 부른다. bounded 오버로드(`purgePublishedBefore(Instant, int)` / `purgeProcessedBefore(Instant, int)`)는 두 포트에 선언되고 두 구현에 구현되어 있으며 호출부가 **0건**이다(`EVD-294`, `EVD-311`). 두 잡 모두 starter 빈이지만 스케줄러는 등록되지 않으며, 그것은 의도된 설계다(`EVD-316`). 즉 기본 배포에서는 아무 일도 일어나지 않고, 애플리케이션이 문서 지시대로 잡을 스케줄하는 순간 무제한 DELETE 가 발동한다. 잠재 결함이지 상시 결함이 아니다. bounded 구현의 주석이 결과를 명시한다: *"An unbounded DELETE holds locks and writes WAL in proportion to the whole backlog, which stalls the relay and the business writes behind retention."* 3일치 백로그가 쌓인 테이블에서 이것은 릴레이 정지와 비즈니스 쓰기 정체를 뜻한다. 두 리프 모두 `runtime_memberships: ["app-bootstrap"]` 이고 두 잡 모두 starter 빈이다. 수정은 한 줄이다 — `purgePublishedBefore(cutoff, batchLimit)`. `maxBatches` 가 그제서야 의미를 갖는다. 배치 크기는 새 파라미터가 필요하고, `OutboxProperties.batchSize`(100)를 재사용하거나 별도 값을 둔다. 그리고 **회귀 테스트가 성립하려면 `RecordingRepository` 를 고쳐야 한다.** 현재 대역의 bounded 구현은 `Math.min(unbounded(), limit)` 로, 전부 지우고 숫자만 깎는다. 실제 저장소를 흉내 내려면 보유 행 목록을 갖고 `limit` 만큼만 제거해야 한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 두 릴레이 상호배제가 기동에서 강제되지 않는다
|
||
- slug: `messaging-outbox-jdbc-postgresql-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L909`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql` · priority: `P2`
|
||
- classification: `DebeziumOutboxProfile.requireExactlyOneRelay(...)` 는 프로덕션 호출부가 0건이다. 클래스 javadoc 은 "the incompatibility is therefore enforced at startup instead of documented" 라고 쓴다. properties 파일도 같은 경고를 반복한다("Enable this OR the in-process polling relay, never both"). 같은 리프에 정확히 이 형태를 고친 선례가 있다 — `OutboxRelayWorker` 가 "nothing ever called `runOnce`" 를 고치고 `MessagingOutboxRelayLifecycle` 로 배선까지 마쳤다. 같은 방식으로 `MessagingReliabilityAutoConfiguration` 에 프로필 빈과 `InitializingBean` 검사를 두면 된다. 배선하려면 CDC 모드를 선택할 프로퍼티도 필요하다 — 지금은 `DebeziumOutboxProfile` 을 만드는 설정 경로 자체가 없다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — 출하 컨텍스트가 발행은 하고 소비는 하지 못한다
|
||
- slug: `messaging-policy-f02`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-policy.md#L790`
|
||
- owning-module: `messaging-policy` · priority: `P2`
|
||
- classification: `KafkaConsumerRegistrar`·`RabbitConsumerRegistrar`·`KafkaBatchConsumerRegistrar`·`RabbitBatchConsumerRegistrar`·`DefaultDeliveryProcessor`·`KafkaRetryExecutor`·`KafkaDeadLetterPublisher`·`RabbitDeadLetterPublisher`가 전부 `src/main` 생성 0이다. 대조군인 발행 경로(`DefaultMessagePublisher`·`TransportMessagingRuntime`)는 `MessagingCoreAutoConfiguration:446,476`에서 생성된다. `messaging-policy`의 두 축이 미배선인 근본 원인이고, `analysis/messaging/messaging-core-api.md` §12.1이 관측한 `MessageHandler<T>` 참조 0의 조립 쪽 설명이다. 그리고 `docs/messaging/support-matrix.md`의 브로커 등급표가 소비 측 보장(순서·정산·재시도)을 서술하는데, 그 보장을 수행할 코드가 조립되지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 능력 상수의 `delayedDelivery` 가 무조건 참이고, 그 지연을 제공할 토폴로지는 조립되지 않는다
|
||
- slug: `messaging-rabbit-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-rabbit.md#L307`
|
||
- owning-module: `messaging-rabbit` · priority: `P3`
|
||
- classification: 그런데 지연을 실제로 만드는 것은 `RabbitRetryQueueTopology` 이고, 그 클래스는 자기 파일과 시험 하나 밖에서 참조되지 않는다. 어떤 production 코드도 그 큐를 선언하지 않는다. 그리고 그 클래스의 javadoc 이 이 지연의 성질을 정확히 적는다. 즉 제공되는 것은 "메시지별 지연" 이 아니라 "재시도 큐 하나당 TTL 하나" 다. 능력 모델에는 그 구분을 표현하는 자리가 없고, 상수는 프로파일과 무관하게 참을 답한다. Kafka 는 같은 칸을 `false` 로 둔다. 그래서 이 플래그의 두 값이 "지연 있음/없음" 이 아니라 "지연을 흉내낼 토폴로지를 선언할 수 있음/없음" 을 뜻하게 된다. 수정은 능력을 전송 상수가 아니라 목적지의 재시도 큐 선언에서 파생시키는 것이다. 이 리프가 조립되지 않는 동안에는 P3 이고, `RabbitChannelPublisher` 구현이 생기는 날 함께 봐야 한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — fencing token 경로가 실제 데이터베이스에 대해 실행되지 않는다
|
||
- slug: `messaging-reliability-api-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L698`
|
||
- owning-module: `messaging-reliability-api` · priority: `P2`
|
||
- classification: `OutboxRelay`는 `claimBatch`/lease 기반 전이만 쓴다. `OutboxPostgresIT`는 `leaseBatch`/`MessageId` 기반 전이만 쓴다. 신세대를 쓰는 다른 테스트는 `InMemoryOutboxRepository`와 `RecordingRepository` — SQL이 없는 fake다. fencing의 정확성은 구현의 조건부 UPDATE가 영향 행 수를 정확히 세는지에 달려 있다. `OutboxTransitionResult.STALE_LEASE`는 "its update matches zero rows"에서 나오고, 그것은 SQL의 성질이지 Java의 성질이 아니다. in-memory fake는 그 SQL을 실행하지 않는다. 즉 **이중 발행을 막는 장치가 그것을 검증할 수 있는 유일한 환경에서 실행되지 않는다.**
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — dual-write의 답이라고 선언한 진입점에 구현이 없다
|
||
- slug: `messaging-reliability-api-f03`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L707`
|
||
- owning-module: `messaging-reliability-api` · priority: `P2`
|
||
- classification: `ReliableMessagePublisher`가 구현 0, 참조 0이다. javadoc은 "This is the answer to the dual-write problem"이라고 한다. `OutboxRepository.append`가 있으므로 outbox에 행을 넣을 방법이 없는 것은 아니다. 그러나 그 포트는 저장소 계약이고, `ReliableMessagePublisher`는 애플리케이션이 저장소를 직접 만지지 않게 하려고 존재한다. 그리고 **애플리케이션은 ArchUnit 규칙 때문에 이 leaf를 참조할 수 없으므로** 브리지 어댑터가 필요한데 그것이 없다. 즉 이 leaf의 Outbox 절반은 "릴레이가 읽는 쪽"만 배선돼 있고 "애플리케이션이 쓰는 쪽"이 비어 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 관측이 구현·호출부·주입 자리를 모두 갖추고도 출하에서 no-op이다
|
||
- slug: `messaging-runtime-core-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L710`
|
||
- owning-module: `messaging-runtime-core` · priority: `P2`
|
||
- classification: `DefaultMessagePublisher`가 모든 발행 결과를 `observation.recordPublish(...)`로 기록하고, 관측을 "constructor argument rather than an optional decorator"로 받는다. `MessagingMetrics`가 `MessagingObservation`을 구현한다. 그런데 출하 조립(`MessagingCoreAutoConfiguration:446`)은 **6인자 생성자**를 써서 `NO_OBSERVATION`을 넣고, `MessagingMetrics`는 저장소 전체에서 자기 테스트에서만 생성된다. starter는 `MessagingMetrics`의 협력자 둘(`MessagingRedactor:253`, `CardinalityGuard:264`)을 bean으로 만든다. 이 필드의 javadoc이 정확히 이 상황을 막으려고 쓰였다 — "an unobserved publish path is how 'the dashboards were empty during the incident' happens". 그리고 같은 javadoc이 **이전 결함**을 "bean은 있고 호출 경로가 없었다"로 기록한다. 지금은 반대다 — 호출 경로가 있고 bean이 없다. 관측 결과는 같다. **고침이 간극을 닫은 게 아니라 반대편으로 옮겼다.** "decorator가 아니라 생성자 인자"라는 선택도 막지 못했는데, 인자를 기본값으로 채우는 짧은 생성자가 함께 있기 때문이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 소비 오케스트레이터가 조립되지 않는다
|
||
- slug: `messaging-runtime-core-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L719`
|
||
- owning-module: `messaging-runtime-core` · priority: `P2`
|
||
- classification: `DefaultDeliveryProcessor`는 leaf 밖 참조 0, `src/main` 생성 0, `src/test` 생성 1이다. 이 클래스가 고친 문제("각 어댑터가 retry/dead-letter의 뜻을 각자 결정")가 배선 없이는 그대로 남는다. 그리고 `DeclaredDestinationAccess`가 consume 권한을 빈 집합으로 두는 것과 정합적이다 — 기본 구성은 소비를 상정하지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 만들어 두고 흘리지 않는 진단값은 진단이 아니다
|
||
- slug: `messaging-runtime-core-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L763`
|
||
- owning-module: `messaging-runtime-core`
|
||
- rule: 만들어 두고 흘리지 않는 진단값은 진단이 아니다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `missingResult()`가 아무 데도 쓰이지 않는다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 포맷 독립 진화 규칙이 호출되지 않고, 그것이 막으려던 중복이 실제로 생겼다
|
||
- slug: `messaging-schema-api-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L494`
|
||
- owning-module: `messaging-schema-api` · priority: `P2`
|
||
- classification: `SchemaCompatibilityValidator`의 저장소 전체 참조가 자기 선언과 자기 테스트뿐이다. 동시에 `AvroCompatibilityGate`가 `isTransitive`를 글자 그대로 복사했고 방향 판정 둘은 허용목록/거부목록으로 형태가 반대다. 오늘은 7개 모드 전부에서 두 구현의 결과가 같다(`NONE_EXPERIMENTAL`은 gate의 early return이 가린다). 그러나 enum에 값이 하나 추가되면 허용목록은 "검사 안 함", 거부목록은 "양방향 검사"로 **반대 방향** 기본값을 갖는다. 그리고 `requireProductionMode` — 검사 없는 스키마가 보존 로그를 뒷받침하는 것을 막는 게이트 — 는 호출되는 곳이 없다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — 빈 registry로 조립되면 모든 메시지가 거절된다
|
||
- slug: `messaging-schema-json-f03`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L481`
|
||
- owning-module: `messaging-schema-json` · priority: `P3`
|
||
- classification: `contracts.getIfAvailable(MessageContracts::none)`이 기본값이므로 `MessageContracts` bean이 없으면 빈 registry로 codec이 만들어진다. 그 codec은 시작에 성공하고 첫 publish에서 `UNKNOWN_MESSAGE_TYPE`으로 실패한다. `messaging-core-api` 계열의 다른 leaf에서 관측된 것과 같은 형태다 — "시작은 하고 첫 쓰기에서 실패한다."
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 종료 시 자격증명 소거가 호출되지 않는다
|
||
- slug: `messaging-security-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L661`
|
||
- owning-module: `messaging-security` · priority: `P3`
|
||
- classification: `CredentialRuntimeRegistry.clearAll()`의 javadoc이 "Clears every held credential, for shutdown"이라고 하고, 호출자가 저장소에 없다. 이 leaf 전체가 "비밀이 힙에 남지 않게 한다"를 목적으로 하고(`char[]`, `clear()`, 회전 시 즉시 소거), 종료 경로에서 그 마지막 단계가 빠져 있다. 프로세스가 끝나면 힙도 사라지지만, 종료가 느리거나 힙 덤프가 뜨는 경우가 정확히 이 통제가 노리는 상황이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 배선된 게이트는 자기 leaf 레인에서 검증한다
|
||
- slug: `messaging-security-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L688`
|
||
- owning-module: `messaging-security`
|
||
- rule: 배선된 게이트는 자기 leaf 레인에서 검증한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 다섯 타입이 이 leaf의 테스트에 등장하지 않는다
|
||
- scope: 보안·승인 표면. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 같은 자동 설정 안에서 검증기 하나만 감싸이지 않는다
|
||
- slug: `messaging-spring-boot-starter-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L281`
|
||
- owning-module: `messaging-spring-boot-starter` · priority: `P2`
|
||
- classification: `KafkaMessagingAutoConfiguration` 은 검증기 셋을 만든다. `KafkaTransactionProfileValidator` 에는 대응하는 `StartupProfileValidation` 이 없다. 즉 컨텍스트가 그 검증기를 발행하고 아무도 주입하지 않는다 — `StartupProfileValidation` 의 javadoc 이 서술한 이전 상태와 정확히 같은 형태다. `RabbitMessagingAutoConfiguration` 은 검증기 하나이고 그것을 감싼다. 그러므로 이 가족에서 감싸이지 않은 검증기는 이 하나다. 트랜잭션 프로파일 검증이 무엇을 막는지는 그 클래스가 안다 — 비트랜잭션 생산자 위의 정확히 한 번 주장 같은 조합이다. 그 검증이 지금 돌지 않는다. 수정은 한 블록이다. 같은 파일의 `kafkaProfileStartupValidation` 형태를 복사해 세 번째 검증기를 감싼다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 허용 의존 목록은 상한이므로 미사용을 잡지 않는다
|
||
- slug: `messaging-spring-cloud-stream-bridge-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L552`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- rule: 허용 의존 목록은 상한이므로 미사용을 잡지 않는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 선언된 의존 둘이 사용되지 않는다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 등록을 받는 컴포넌트는 해제도 제공한다
|
||
- slug: `messaging-spring-cloud-stream-bridge-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L588`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- rule: 등록을 받는 컴포넌트는 해제도 제공한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 등록 해제 경로가 없다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — `FaultController` 의 5개 중 2개가 구현만 3벌 있고 호출부가 0건이다
|
||
- slug: `messaging-testkit-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L940`
|
||
- owning-module: `messaging-testkit` · priority: `P2`
|
||
- classification: `rejectPublish()` 와 `reset()` 은 인터페이스에 선언되어 세 하니스가 전부 구현하지만, 계약 스위트를 포함해 어디에서도 호출되지 않는다(`EVD-299`). `rejectPublish` 는 심지어 세 하니스의 `publish()` 경로에 완전히 배선되어 있다(`KafkaContractHarness:119`, `RabbitContractHarness:85`, `InMemoryMessagingHarness:66`) — 켜는 스위치만 아무도 누르지 않는다. 이것이 단순한 미사용 코드가 아닌 이유: 미사용 경로가 **틀린 값을 인코딩하고 있다**. `InMemoryMessagingHarness` 에서 `rejectPublish` 는 `rejected("BROKER_REJECTED", …)` 를 돌려주고, 그 헬퍼는 `PublishEvidence.notTransmitted()` 를 쓴다(`:184-193`). `TransmissionEvidence.NOT_TRANSMITTED` 의 javadoc 은 "Nothing was written to the broker connection." 이다. 그런데 `FaultController.rejectPublish` 의 javadoc 은 "refused outright by **the broker**" 다 — 브로커가 거절하려면 바이트가 나갔어야 하므로 `TRANSMITTED` 여야 한다. 이 플랫폼은 전송 증거를 세 값으로 구분하는 것을 핵심 가치로 삼는데, 유일하게 실행되지 않는 경로에 그 구분의 오류가 들어 있다. `connection-refused` 시나리오(유일하게 증거가 없는 시나리오, `Expectation.REJECTED`)와 이 미사용 결함이 같은 빈칸을 가리킨다. 둘 중 하나를 택해야 한다: 계약에 `rejectsWhenBrokerRefusesBeforeTransmission` 를 추가하고 전송 증거를 바로잡거나, `rejectPublish` 를 인터페이스에서 제거해 세 하니스의 구현 부담을 …
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 1 MiB 한도가 `PayloadPolicy` 를 두고 리터럴로 재선언된다
|
||
- slug: `messaging-testkit-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L966`
|
||
- owning-module: `messaging-testkit` · priority: `P3`
|
||
- classification: `PayloadPolicy.DEFAULT_MAX_BYTES = 1_048_576` 이 정본인데 같은 값이 최소 8곳에 다시 있고(§12.3(b)), 그중 둘이 이 리프다(`InMemoryMessagingHarness:31`, `ContractMessage:50`). `messaging-testkit` 은 `api project(':messaging:messaging-policy')` 를 이미 선언하고 있으므로 import 한 줄이면 된다. 지금은 `messaging-policy` 의존이 import 0건이라 선언만 남아 있는데(§12.4(b)), 이 자리가 그 의존이 실제로 쓰여야 할 곳이다. `ContractMessage.oversized()` 의 `1_048_577` 은 `PayloadPolicy.DEFAULT_MAX_BYTES + 1` 로 쓰면 "한도 바로 위 한 바이트" 라는 의도가 코드에 드러난다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `messaging-transport-spi` 의존이 import 0건이다
|
||
- slug: `messaging-testkit-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L972`
|
||
- owning-module: `messaging-testkit` · priority: `P3`
|
||
- classification: policy 와 달리 transport-spi 는 쓸 자리가 보이지 않는다. 제거 후보.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `BrokerFailureMatrix.adapters()` 는 호출부가 0건이다
|
||
- slug: `messaging-testkit-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L976`
|
||
- owning-module: `messaging-testkit` · priority: `P3`
|
||
- classification: public 메서드이나 아무도 쓰지 않는다. 이 리프의 다른 public 표면은 전부 소비자가 있다. 제거하거나, 진단용이라면 그렇게 적는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 25 — verification-path-coverage
|
||
|
||
> 검증이 무엇을 실제로 붙드는가 — 대역·단언·레인이 증명하는 것과 이름이 주장하는 것. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — `rejectNewAdmission()` 이 단계만 기록하고 아무것도 거절하지 않는다
|
||
- slug: `grpc-admin-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-admin.md#L131`
|
||
- owning-module: `grpc-admin` · priority: `P2`
|
||
- classification: javadoc 은 "Starts refusing new calls" 라고 적는다. 실제로 하는 일은 단계 목록에 표식을 넣는 것뿐이다. 조정자는 `GrpcAdmissionController` 를 협력자로 들고 있는데, 그것을 쓰는 곳은 `inFlightAdmitted()` 의 조회 하나다. 그리고 승인 제어기의 공개 표면에 승인을 멈추는 메서드가 없다. `close`·`drain`·`refuseNew` 에 해당하는 것이 없다. 그러므로 배수가 시작된 뒤에도 `tryAdmit()` 은 용량이 남아 있는 한 계속 승인한다. `admittingNewCalls()` 은 그 사실과 무관하게 거짓을 돌려준다 — 표식을 읽기 때문이다. 운영자나 상위 코드가 이 값을 보고 "더 이상 받지 않는다" 고 읽으면 틀린 답을 얻는다. 배수 테스트가 단언하는 것은 `admittingNewCalls()` 의 값이고, 단계 이후에 `tryAdmit()` 이 거절되는지는 어느 테스트도 묻지 않는다. 승인 제어기에 승인 중단 상태를 두고(`stopAdmitting()` 과 그것을 보는 `tryAdmit`), 조정자의 `rejectNewAdmission` 이 그것을 부르게 한다. 지금 형태에서는 배수 순서를 지키는 장치가 순서 표식만 갖고 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 등급 재정의에 하한이 없어 "켤 수 없다" 는 등급이 켜질 수 있다
|
||
- slug: `grpc-advanced-bootstrap-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md#L153`
|
||
- owning-module: `grpc-advanced-bootstrap` · priority: `P3`
|
||
- classification: `GrpcCapabilityGrade` 의 javadoc 이 두 등급을 단정한다. 그런데 등급은 런타임에 갈아끼울 수 있다. `withGrade(EDITION_2026, ADVANCED_STABLE).enable(EDITION_2026)` 이면 가드의 두 번째 조건이 통과한다. 등급 올리기 자체는 의도된 기능이다 — 테스트 `a deployment may raise a capability's grade on its own evidence` 가 `HEDGING`(EXPERIMENTAL)을 `ADVANCED_STABLE` 로 올린다. 문제는 그 재정의에 하한이 없다는 것이다. `EXPERIMENTAL` 을 올리는 것은 "실패 양식이 충분히 규명되지 않은 것을 감수한다" 는 판단이고 배포가 자기 증거로 내릴 수 있다. `WATCH` 를 올리는 것은 다르다. 그 등급의 뜻이 "추적할 뿐 구현되지 않았다" 이므로 배포가 가질 자기 증거가 없다. 그리고 승격 게이트는 `WATCH` 가 `EXPERIMENTAL` 을 먼저 거쳐야 한다는 규칙을 갖는데, 런타임 재정의는 그 게이트를 지나지 않는다. 같은 리프 안에 문이 둘이고 증거 규칙은 한쪽에만 있다. 수정은 `withGrade` 가 현재 등급이 `startable()` 인 능력에만 적용되게 하거나, `WATCH`·`DISABLED` 에서 올리는 재정의를 거부하는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `capabilitiesDraggedAlong` 은 독립성을 증명하지 않는다. 상수를 상수와 비교한다
|
||
- slug: `grpc-advanced-bootstrap-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md#L286`
|
||
- owning-module: `grpc-advanced-bootstrap` · priority: `P3`
|
||
- classification: javadoc 이 스스로 밝히듯 본문은 무조건 빈 목록이다. 그것을 단언하는 테스트는 리터럴이 리터럴임을 확인한다 — 증거를 능력마다 따로 기록했다는 §4 의 설계 속성과는 아무 연결이 없다. 설계가 무너져 `apply` 가 다른 능력의 등급을 바꾸게 되어도 이 메서드는 여전히 빈 목록을 돌려준다. **진짜 증거는 같은 테스트의 다른 줄에 있다.** 승격을 실제로 적용하고 다른 능력의 등급이 그대로임을 확인한다. 이쪽은 설계가 무너지면 깨진다. 앞선 판에서 이 메서드를 "주석이 주장하는 대신 테스트가 붙든다"는 확인된 설계로 분류했다. 다시 읽으니 붙드는 것은 옆줄이고, 이 메서드는 그 옆줄이 있다는 사실을 가린다. 메서드를 지우고 단언을 매트릭스 비교 쪽으로 남긴다. 남겨 둔다면 실제로 매트릭스를 훑어 등급이 바뀐 다른 능력을 돌려주게 만든다 — 그때 비로소 이름이 하는 말과 본문이 맞는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 반응형 표면 두 타입은 테스트조차 없다
|
||
- slug: `grpc-advanced-compat-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-compat.md#L149`
|
||
- owning-module: `grpc-advanced-compat` · priority: `P3`
|
||
- classification: 이 가족의 다른 미참조 Advanced 타입은 전부 테스트가 하나씩 있다 — 같은 패키지의 `GrpcReactorCancellationBridge` 는 2개 파일, `GrpcReactorContextBridge` 는 4개 파일에 등장한다. 두 타입은 채택자가 부를 표면이므로 production 참조 0 이 설계와 모순되지는 않는다. 어긋나는 것은 검증이다. 채택자용 표면이면 그 계약이 무엇인지를 테스트가 붙들어야 하고, 이 가족은 다른 곳에서 정확히 그렇게 한다. `ReactiveGrpcClient` 의 javadoc 이 "Exposes a unary call as a `Mono` and a server stream as a `Flux`" 라고 적는데, 그 사상이 취소와 배압에서 어떻게 동작하는지는 어디에서도 확인되지 않는다. 같은 리프의 `GrpcReactorCancellationBridge` 가 취소 전파를 다루므로 둘을 함께 검증할 자리가 이미 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 저장소가 참조 프록시 설정을 갖고 있는데, 그것을 판정할 코드에 넣지 않는다
|
||
- slug: `grpc-advanced-compat-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-compat.md#L162`
|
||
- owning-module: `grpc-advanced-compat` · priority: `P3`
|
||
- classification: 이 리프에는 두 가지가 함께 있다. `GrpcWebProxyContract.violations(profile, exposedHeaders, allowedOrigins)` — 프록시 설정이 브라우저 클라이언트에게 통할지 판정하는 코드. `src/main/resources/envoy/envoy.yaml` — 그 설정의 참조 구현. 그리고 설정 파일 자신이 그 관계를 주장한다. `GrpcWebProxyContract` 는 이 파일에 대해 아무것도 단언하지 않는다. 판정기는 시험에서 리터럴 집합을 받고, 참조 설정은 시험에서 문자열 포함으로만 확인된다. 그래서 참조 설정이 `grpc-status` 를 노출하는지는 문자열이 확인하고, 그 노출이 **충분한지** 는 `requiredExposedHeaders()` 가 정의하는데, 둘을 잇는 코드가 없다. 필수 트레일러 목록이 늘어나면 판정기는 새 항목을 요구하고 참조 설정은 옛 문자열로 계속 통과한다. 이 리프의 다른 판정기들과 다른 점은 재료가 이미 저장소에 있다는 것이다 — grpc-testkit §17.5·grpc-server §17.1 은 스캔할 대상 자체를 만들어야 하지만, 여기서는 파일 하나를 파싱하면 된다. 수정은 시험이 `envoy.yaml` 의 `expose_headers` 와 `allow_origin`(`exact:`)을 뽑아 `GrpcWebProxyContract.violations` 에 넣고 비어 있음을 단언하는 것이다. 그러면 참조 설정과 계약이 한 곳에서 함께 움직인다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — Buf 수명주기 태스크 목록이 빌드와 대조되지 않는다. 테스트는 목록을 자기 자신과 비교한다
|
||
- slug: `grpc-codegen-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-codegen.md#L190`
|
||
- owning-module: `grpc-codegen` · priority: `P3`
|
||
- classification: 정책이 네 태스크 이름을 담고, javadoc 이 그 이유를 적는다. 그런데 그 네 이름은 저장소의 어떤 빌드 파일에도 없다. 그리고 테스트가 비교하는 대상이 실제 등록 태스크 집합이 아니다. 첫 단언은 목록을 리터럴과, 셋째는 목록을 자기 자신과 비교한다. 어느 것도 빌드가 그 단계를 등록했는지 묻지 않는다. Buf CLI 가 이 툴체인에 없다는 것은 build.gradle 이 이미 밝힌 사실이므로 태스크가 없는 것 자체는 놀랍지 않다. 어긋난 것은 javadoc 의 주장이다 — 지금 형태에서 단계가 사라져도 테스트는 초록이다. 수정은 `missingTasks` 에 Gradle 이 실제로 등록한 태스크 이름 집합을 넣는 검사를 만들거나(다른 가족의 레인 등록 검사와 같은 형태), CLI 가 없는 동안에는 그 문장을 "CI 환경이 채울 계약" 으로 낮추는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `sha256:` 검사가 길이 15자 이상만 요구한다. 저장소 자신의 테스트가 32자 해시를 통과시킨다
|
||
- slug: `grpc-codegen-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-codegen.md#L287`
|
||
- owning-module: `grpc-codegen` · priority: `P3`
|
||
- classification: 같은 검사가 두 곳에 손으로 복사돼 있다. `"sha256:"` 이 7자이므로 뒤에 8자만 있으면 통과한다. sha256 digest 는 hex 64자다. 그리고 이 헐거움이 테스트에 이미 드러나 있다. 32자 — sha256 이 아니다. 여기서는 "다른 해시" 역할이라 결과가 바뀌지 않지만, 형식 검사가 이런 값을 유효한 해시로 받는다는 사실 자체가 이 값 객체의 주장("the hashes that prove which bytes it was built from")을 약하게 만든다. `sha256:` 뒤 64자 hex 를 정규식으로 요구하고, 검사를 한 곳에 둔다 — 두 record 가 같은 규칙을 각자 적고 있는 지금 형태에서는 한쪽만 조여도 다른 쪽이 남는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 모듈 목록 테스트가 레지스트리와 목록을 붙들지 않는다
|
||
- slug: `grpc-core-api-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-core-api.md#L157`
|
||
- owning-module: `grpc-core-api` · priority: `P3`
|
||
- classification: 클래스 javadoc 이 두 SSOT 의 관계를 적는다. 그 테스트는 레지스트리를 읽지 않는다. 다섯 테스트가 하는 일은 목록을 리터럴과 대조하고, 두 집합의 서로소를 확인하고, 누출 판정을 확인하는 것이다. `modules.json` 을 읽는 줄도, 파일 경로도 없다. 두 목록은 오늘 일치한다 — 레지스트리의 grpc 계열 리프가 18 개이고 목록이 12 + 6 이다. 어긋난 것은 그 일치를 무엇이 지키는가다. 같은 저장소가 이 형태를 messaging 가족에서 이미 기록했다 — 정확한 목록은 레지스트리가 소유하므로 산문에서 세지 않는다, 세는 순간 다시 표류한다. 수정은 테스트가 `modules.json` 을 읽어 grpc 계열 리프 집합과 두 상수 집합의 합집합을 대조하는 것이다. 그 테스트가 있으면 새 리프가 어느 쪽에도 들어가지 않은 채 추가되는 것을 잡는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 두 아키텍처 규칙이 저장소 소스에 적용되지 않는다
|
||
- slug: `grpc-server-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-server.md#L140`
|
||
- owning-module: `grpc-server` · priority: `P2`
|
||
- classification: `GrpcApplicationBoundaryRules` javadoc: 세 적용처 중 저장소에 존재하는 것이 없다. 그리고 이 리프의 테스트는 저장소 파일을 훑지 않는다. 인라인 소스 문자열을 넣는다. 즉 규칙의 판정 로직은 검증되지만, 저장소의 어떤 파일도 그 판정을 받지 않는다. `GrpcServiceAdapterMarker` 는 규칙이 어댑터를 런타임에 열거할 수 있도록 만든 애너테이션인데, 그것을 붙인 타입도 그것을 읽는 코드도 없다. 이 리프의 테스트에 저장소 소스를 훑는 검사를 추가한다 — `src/**/*.java` 를 읽어 `GrpcRawApiImportRule.violations` 를 돌리고 비어 있음을 단언하는 형태다. 규칙이 이미 파일 이름과 소스 텍스트를 받는 서명이므로 재료는 갖춰져 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 네 레인이 `check` 에 붙지 않고, 이 가족을 이름으로 부르는 워크플로가 없다
|
||
- slug: `grpc-testkit-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-testkit.md#L153`
|
||
- owning-module: `grpc-testkit` · priority: `P2`
|
||
- classification: `ca.strict-test-lane.gradle` 은 레인을 `verification` 그룹의 `Test` 태스크로 **등록만** 한다. `check` 에 연결하는 줄이 없다. 그리고 CI 워크플로에서 이 가족을 이름으로 부르는 것이 없다. `ci-quality-gates.yml` 이 `./gradlew check` 를 돌리므로 각 리프의 기본 `test` 는 돈다(이번에 확인: classes=71 tests=579 failures=0). 네 증거 레인은 그 밖에 있다. 결과적으로 이 플랫폼의 CONTRACT·TRANSPORT·FAULT 등급을 뒷받침하는 것은 25개 테스트이고, 그 25개는 누군가 명령을 직접 입력할 때만 돈다. build.gradle 자신이 그 위험을 적는다 — "A lane that discovers nothing fails, and **none of them can serve an up-to-date result**". 첫 성질은 레인 규약이 지킨다. 둘째 성질은 아무도 돌리지 않으면 무의미하다. 같은 저장소가 이 형태를 두 번 기록했다 — 모듈 18 의 "붉은 게이트는 마지막으로 돌린 사람이 본 것을 보고한다" 와 mongo 가족의 릴리스 게이트 지형. 차이는 이쪽 레인이 오늘 초록이라는 것이고, 그것을 확인한 방법이 이번 분석에서 직접 돌린 것이라는 점이다. 수정은 세 레인(성능 제외)을 `check` 에 붙이거나, messaging 가족처럼 전용 워크플로를 두는 것이다. 성능 레인을 빼는 판단은 이미 근거와 함께 코드에 있으므로 그대로 두면 된다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 고장 레인의 유일한 실소켓 시험이 자기가 관측한 것을 버리고 리터럴로 증거를 만든다
|
||
- slug: `grpc-testkit-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-testkit.md#L187`
|
||
- owning-module: `grpc-testkit` · priority: `P2`
|
||
- classification: `GrpcTransportEvidenceClassifierTest.aRealConnectionLossAfterAppStartIsCompletionUnknown` 은 이 리프에서 유일하게 실제 연결을 작업 중에 끊는다. 서버 핸들러가 래치로 멈춰 있는 동안 `server.close()` 를 부른다. 거기까지는 진짜 고장이다. 그런데 그 고장이 만들어 낸 관측이 어디에도 남지 않는다. 주석이 "the exception is the observation" 이라고 말하는데 그 예외는 `catch` 안에서 사라지고, 변수는 `null` 로 고정되고, 단언은 자기 대입을 확인한다. 그리고 `GrpcFaultResult` 에 들어가는 증거는 방금 일어난 호출에서 오지 않는다. 즉 소켓은 실제로 죽었고, 그 죽음에서 읽어 낸 값은 하나도 쓰이지 않는다. 이 시험이 실제로 증명하는 것은 `applicationStarted == true` 하나다. 나머지는 분류기의 산술이고, 그것은 같은 파일의 다른 일곱 시험이 이미 소켓 없이 증명한다. 이 형태를 이 리프 자신이 이름 붙여 두었다. 여기서는 소켓이 열렸다. 그런데 등급을 뒷받침해야 할 증거가 여전히 손으로 쓴 값이다. 한 단계 아래의 같은 치환이다. `callUnary` 를 부른 스레드가 잡은 예외와 그 시점의 진행 상태를 밖으로 넘겨(`AtomicReference`), 그것으로 `ClientObservation` 을 구성한다. 그러면 `sendCompleted`·`responseHeadersReceived` 가 관측값이 되고, 이 시험이 FAULT 등급을 실제로 뒷받침한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 호환성 표의 레인 이름과 빌드의 레인 이름이 서로 다른 집합이다
|
||
- slug: `grpc-testkit-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-testkit.md#L233`
|
||
- owning-module: `grpc-testkit` · priority: `P3`
|
||
- classification: `GrpcStableReleaseGate.evaluate` 의 둘째 인자는 `Map<String, Boolean> laneResults` 이고, `GrpcCompatibilityMatrix.missingResults` 가 그 키를 자기 목록과 대조한다. 그 목록은 배포 조합의 이름이다 — `boot-managed-platform`, `netty-shaded`, `upstream-grpc-java-override`, `protobuf-edition-2024` … 빌드가 등록하는 레인의 이름은 증거 종류다 — `grpcInProcessContractTest`, `grpcNettyContractTest`, `grpcFaultTest`, `grpcPerformanceTest`. 두 집합의 교집합이 비어 있다. 그래서 §17.1 을 고쳐 네 Gradle 레인을 `check` 에 붙이더라도, 그 결과가 이 게이트의 `laneResults` 를 채우지는 못한다 — 이름이 다른 축을 가리키기 때문이다. 게이트가 요구하는 것은 "Boot 관리 플랫폼 조합에서 돌았는가" 이고, 레인이 답할 수 있는 것은 "전송 증거를 냈는가" 다. 두 축이 다 필요하다는 것 자체는 옳다. 기록하는 이유는 §17.1·§17.2 의 수정이 이것까지 함께 다루지 않으면 게이트가 여전히 손으로 만든 값을 먹는다는 점이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `DestructiveOperationGuard` 의 두 분기가 문서에도 없고 테스트에도 없다
|
||
- slug: `messaging-admin-api-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L915`
|
||
- owning-module: `messaging-admin-api` · priority: `P2`
|
||
- classification: operation 불일치(`:72-83`)와 source 불일치(`:84-93`)는 클래스 javadoc 의 "Four conditions" 에 포함되지 않고, 두 에러 코드를 단언하는 테스트도 저장소 전체에 없다(`EVD-303`). 이 둘은 사소한 검사가 아니다 — 5번 분기의 인라인 주석이 정확히 말한다: "A guard that only checks presence and window lets a verified redrive approval authorise a destination deletion." 즉 **검증된 승인으로 목적지 삭제를 인가하는 것**을 막는 검사다. 혼동을 키우는 정황이 하나 더 있다. `anApplicationRuntimeCannotRedrive` 는 `REDRIVE` 요청에 `REPLAY` 승인을 넘기지만 `adminCredentialPresent=false` 라 분기 2에서 먼저 걸린다. 불일치 조합이 테스트에 등장하지만 그 분기는 실행되지 않는다. 수정: javadoc 을 여섯으로 고치고, `new DestructiveOperationGuard(true)` 위에서 operation 불일치·source 불일치 각각 1건씩 테스트를 추가한다. 이 리프에는 이미 `verified(operation, source, from, until)` 헬퍼가 있어 두 줄이면 된다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 상호운용을 위한 매퍼가 명세 준수 이벤트를 분류되지 않은 예외로 거절한다
|
||
- slug: `messaging-cloudevents-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L518`
|
||
- owning-module: `messaging-cloudevents` · priority: `P2`
|
||
- classification: `fromCloudEvent`가 `new MessageId(UUID.fromString(event.getId()))`로 id를 파싱한다. CloudEvents 1.0.2는 `id`를 비어 있지 않은 문자열로만 제약한다. 런타임 probe 결과: 명세 예시 id `A234-1234-1234` → `java.lang.IllegalArgumentException: Invalid UUID string`, UUIDv4 → `java.lang.IllegalArgumentException: a message identity is UUIDv7`. **둘 다 `MessagingException`이 아니다.** **(1) 범위.** v4 거절은 의도이고 테스트 주석이 그렇게 적는다. 그러나 **비UUID 거절은 어디에도 언급되지 않았고** 그것은 다른 판단이다 — v4 거절은 "우리 정책", 비UUID 거절은 "CloudEvents 상호운용 포기"다. 이 leaf의 존재 이유가 상호운용인데 명세 예시조차 받지 못한다. **(2) 실패 어휘.** 같은 메서드의 다른 검증 실패 넷은 전부 `MessageValidationException`이고 안정 코드(`CLOUDEVENT_TIME_REQUIRED` 등)를 갖는다. id 실패만 raw `IllegalArgumentException`이라 `FailureDescriptor`가 없다 — 카테고리도, 코드도, retryable 판정도 없다. DLQ 라우팅과 대시보드가 이 실패를 분류하지 못한다. 같은 문제가 `causationid`·`type`·`correlationid`·`tenantcontext`·`producer`·`datacontenttype`·`schemaversion` 값 범위에도 있다(§6의 "코드 없음" 여덟 행).
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다
|
||
- slug: `messaging-cloudevents-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L538`
|
||
- owning-module: `messaging-cloudevents`
|
||
- rule: 왕복이라 부르는 테스트는 무엇을 보존하지 않는지도 적는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 왕복이 다섯 필드를 버리고, 테스트가 그 필드를 비교하지 않는다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 속성을 이름으로 주장하는 테스트가 그 속성을 보일 수 없는 fake 위에서 통과한다
|
||
- slug: `messaging-inbox-jdbc-postgresql-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L657`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql` · priority: `P2`
|
||
- classification: `InboxOperationsTest.cleanupDeletesInBoundedBatches`가 `InMemoryInbox(List.of(1000, 500))`에 대해 `removed == 1500`과 `cutoffs.hasSize(3)`을 단언한다. 그 fake의 무제한 메서드는 미리 준 목록을 순서대로 반환하는 **대본**이고 아무것도 삭제하거나 제한하지 않는다. bounded 오버로드는 fake에도 있지만 job이 부르지 않아 실행되지 않는다. 이 테스트가 통과로 증명하는 것은 "0을 받을 때까지 루프를 돈다"이고 이름이 주장하는 "배치로 제한된다"가 아니다. 1000·500은 배치처럼 보이는 숫자다. **P1이 이 테스트를 통과한 채로 존재할 수 있었던 이유**다. 그리고 컨테이너 레인(`InboxPostgresIT.retentionRemovesOldRows`)도 무제한 오버로드를 한 행에 대해 부르므로 실 DB에서도 드러나지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다
|
||
- slug: `messaging-kafka-share-experimental-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L503`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- rule: leaf의 각 public 클래스는 자기 레인에 테스트를 갖는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 네 타입 중 하나만 테스트된다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — `deduplicatedPublish` 를 무조건 참으로 선언하는데 실제 중복 제거는 프로파일에 창이 있을 때만 일어난다
|
||
- slug: `messaging-nats-experimental-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md#L146`
|
||
- owning-module: `messaging-nats-experimental` · priority: `P2`
|
||
- classification: 검증기의 `capabilities()` 도 같은 값을 돌려준다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어진다. `NatsJetStreamProfile.deduplicationWindow` 는 `Optional<Duration>` 이고, 비어 있는 것이 정상 상태다 — 프로파일 생성자도 검증기도 창을 요구하지 않는다. 창이 없으면 `Nats-Msg-Id` 가 실리지 않고 서버는 중복을 제거하지 않는다. 즉 능력 선언이 프로파일과 무관하게 참이다. 이 저장소에서 능력 열두 개 중 부재가 예외를 만드는 유일한 것이 `deduplicatedPublish` 다(`DefaultMessagePublisher:250`). 나머지는 읽히지 않거나 분기에 쓰인다. 그러므로 이 플래그의 과대 선언은 다른 어느 플래그의 과대 선언보다 직접적이다 — 창 없는 목적지가 그 가드를 통과한다. 클래스 javadoc: "when the profile enables one" 이 정확히 능력이 담지 않은 조건이다. 창이 없는 목적지에서 모호를 재시도하면 스트림에 같은 메시지가 두 번 들어간다. `MessagingCapabilities` 의 클래스 javadoc 이 이 상황을 미리 서술한다 — "a silently weakened guarantee is indistinguishable from a working one until the incident." `NatsAdapterContractTest` 안에서, 같은 빈 창 프로파일(`confirming(Optional.empty())`)에 대해: 둘 다 통과한다. 모순이 우연히 남은 것이 아니라 **테스트로 고정되어** 있다는 뜻이고, 수정할 때 함께 고쳐야 할 지점이 어디인지도 이 두 개가 알려 준다. `NatsJetStreamProfile.durable(...)` 는 창을 2분으로 채워 주지만 호출자가 없고, 정규 생성자는 빈 창을 정상값으로 받는다(§4). 능력을 프로파일에서 파생시킨다. 또는 검증기가 최소 한 번 배달 …
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 경과 시간 회귀를 막으려는 어셈블이 항상 참이다
|
||
- slug: `messaging-nats-experimental-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md#L238`
|
||
- owning-module: `messaging-nats-experimental` · priority: `P3`
|
||
- classification: `as(...)` 가 막으려는 회귀는 "모든 결과가 `Duration.ZERO` 를 보고하던 것"이다. 그런데 어셈블은 `>= Duration.ZERO` 다. `Duration.ZERO` 는 이 조건을 통과한다. 경과 시간은 시작 시점에서 잰 값이라 음수가 될 수 없으므로, 이 어셈블은 **구현이 무엇을 하든 통과한다.** 이름과 `as` 메시지가 정확히 짚은 회귀를, 어셈블만 못 잡는다. 그래서 이 테스트는 회귀 방지가 아니라 회귀 방지의 표시다. `isGreaterThan(Duration.ZERO)` 로 바꾼다. 시간 분해능이 불안하면 전송 람다에 관측 가능한 지연을 넣고 그 하한과 비교한다 — 같은 클래스의 `aPublishThatNeverCompletesIsBoundedByTheCallersTimeout` 가 이미 50밀리초 마감으로 그 방식을 쓴다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 배포되는 Debezium 설정이 수정 이전 버전이다
|
||
- slug: `messaging-outbox-jdbc-postgresql-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L888`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql` · priority: `P2`
|
||
- classification: `src/main/resources/debezium/outbox-event-router.properties` 가 `event.key=destination` 을 유지하고 있다. 같은 저장소의 Java(`DebeziumOutboxEventRouter`), V4 마이그레이션 주석, 그리고 전용 테스트(`theRoutedKeyIsNotTheTopicName`)가 모두 그것이 결함이라고 말한다 — "keying by destination puts every message on a topic onto one partition". 추가로 헤더 매핑이 15개 중 4개뿐이라, 이 파일로 배포한 CDC 는 tenant·correlation·causation·producer·trace·partition/ordering key 를 **전부 잃는다**. V4 가 존재하는 이유가 그 유실을 막는 것이다. 1. properties 를 Java 설정에서 생성하거나, 최소한 **둘을 대조하는 테스트**를 둔다. `DebeziumOutboxEventRouter.connectorConfiguration("")` 의 항목이 파일에 모두 있는지 확인하는 테스트면 충분하다. 지금은 두 표현을 잇는 코드가 한 줄도 없다. 2. `aggregateIdAsPartitionKey` 를 `connectorConfiguration` 에 전달하거나, 전달할 수 없다면 `DebeziumOutboxRecordMapper` 에서 그 분기를 제거한다. 지금은 모델이 커넥터가 하지 않을 일을 예측한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 역슬래시로 끝나는 헤더 값이 헤더 맵을 깨뜨린다
|
||
- slug: `messaging-outbox-jdbc-postgresql-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L899`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql` · priority: `P2`
|
||
- classification: `findClosingQuote`(`:657-664`)가 이스케이프된 역슬래시를 고려하지 않는다. 값이 역슬래시로 끝나면 파서가 종료 지점을 놓치고, 뒤에 헤더가 더 있으면 맵 전체가 붕괴한다(`EVD-314`, 런타임 재현). `HeaderValue` 는 제어문자만 금지하므로 이 입력은 플랫폼 검증을 통과한다. 헤더 주입으로 이어지지는 않는다 — 예약 이름은 키가 아니라 값이 되고, 쓰기 경로가 예약 이름을 이미 거절한다. 수정: 종료 판정을 "앞의 연속된 역슬래시 개수가 짝수" 로 바꾸거나, 인덱스를 앞에서부터 스캔하며 이스케이프 상태를 추적한다. 후자가 `unescape` 와 대칭이라 낫다. 테스트는 `OutboxPostgresIT.aHeaderValueWithControlCharactersRoundTrips` 옆에 역슬래시 종결 케이스를 추가하면 된다 — 실 DB 왕복까지 확인할 수 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 한 인터페이스가 같은 전이의 두 세대를 갖고, 안전하지 않은 쪽에 `@Deprecated`가 없다
|
||
- slug: `messaging-reliability-api-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L689`
|
||
- owning-module: `messaging-reliability-api` · priority: `P2`
|
||
- classification: `OutboxRepository`가 다섯 전이 각각에 대해 `MessageId` 기반(반환 `void`)과 `OutboxLease` 기반(반환 `OutboxTransitionResult`) 두 형태를 선언한다. javadoc이 전자를 "deprecated for the relay's use"라고 부르지만 `@Deprecated` 애노테이션이 이 leaf 전체에 **0건**이다. 전자에는 fencing이 없다 — `OutboxLease` javadoc이 그 부재가 만든 이중 발행 사고를 기록한다. 컴파일러가 경고하지 않으므로 새 호출자가 그것을 고를 수 있고, **실제로 PostgreSQL 컨테이너 테스트가 그렇게 했다**(§12.1c). 그리고 새 구현자는 17개 메서드를 전부 구현해야 하며 그중 다섯은 안전하지 않은 형태다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다
|
||
- slug: `messaging-reliability-api-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L716`
|
||
- owning-module: `messaging-reliability-api`
|
||
- rule: 계약만 담는 leaf도 계약의 거절 조건은 자기 레인에서 검증한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 이 leaf에 테스트가 없다
|
||
- scope: 멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — record의 `equals`를 좁히면 이유를 적는다
|
||
- slug: `messaging-reliability-api-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L743`
|
||
- owning-module: `messaging-reliability-api`
|
||
- rule: record의 `equals`를 좁히면 이유를 적는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `OutboxRecord.equals`가 다섯 필드만 비교하고 이유가 없다
|
||
- scope: 멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다
|
||
- slug: `messaging-runtime-core-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L754`
|
||
- owning-module: `messaging-runtime-core`
|
||
- rule: 증가한다고 문서화한 값이 리터럴이면 그 사실을 적는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `generation`이 항상 1이다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다
|
||
- slug: `messaging-schema-avro-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L551`
|
||
- owning-module: `messaging-schema-avro`
|
||
- rule: 모드 enum을 분기 조건으로 쓰면 각 분기에 테스트를 둔다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — transitive 분기가 테스트되지 않는다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다
|
||
- slug: `messaging-schema-protobuf-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L527`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- rule: 검증되지 않는 스키마 파일은 문서임을 파일 안에 적는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `.proto` fixture와 테스트 descriptor의 일치를 아무도 강제하지 않는다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다
|
||
- slug: `messaging-schema-protobuf-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L536`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- rule: 신뢰할 수 없는 입력 쪽 경계를 먼저 테스트한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 디코딩 상한 분기가 테스트되지 않는다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다
|
||
- slug: `messaging-security-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L670`
|
||
- owning-module: `messaging-security`
|
||
- rule: 같은 술어가 두 타입에 있으면 하나가 다른 하나를 부른다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 회전 술어가 두 번 구현돼 있고, 쓰이지 않는 쪽이 테스트된다
|
||
- scope: 보안·승인 표면. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 배치 발행자가 `CompletionStage` 를 돌려주면서 동기 예외를 던진다
|
||
- slug: `messaging-spring-boot-starter-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L367`
|
||
- owning-module: `messaging-spring-boot-starter` · priority: `P3`
|
||
- classification: 같은 클래스가 자기 의존 대상에 대해서는 정확히 이 형태를 방어한다. 즉 "게으르게 검증하고 실패한 스테이지를 돌려준다" 가 이 클래스가 아는 계약인데, 자기 호출자에게는 그것을 지키지 않는다. 비동기 파이프라인으로 배치를 부르는 코드는 `.exceptionally(...)` 로 잡히지 않는 예외를 만난다. 등급이 P3 인 이유는 이것이 프로그래밍 오류(배치 크기 초과)이고 결과가 손실이 아니라 예외 형태의 불일치이기 때문이다. 전용 테스트(`aBatchLargerThanItsLimitIsRefusedBeforeAnythingIsPublished`)가 `assertThatThrownBy` 로 현재 동작을 고정하고 있으므로, 고치려면 그 테스트도 함께 바꾼다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 항등식을 단언하는 테스트가 하나 있다
|
||
- slug: `messaging-testkit-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L980`
|
||
- owning-module: `messaging-testkit` · priority: `P3`
|
||
- classification: `CompatibilityMatrixTest.aCertificationClaimCannotBeMadeWithoutEvidence`(`:48-60`)의 좌변과 우변이 같은 식이다(§12.3(c)). 이름이 약속하는 것을 검사하지 않는다. 실질 검사는 같은 파일의 다른 두 테스트가 하고 있으므로 커버리지 손실은 없다. 이 테스트를 지우거나, "증거를 비우면 Stable 주장이 무너진다" 를 실제로 검사하도록 바꾼다 — 후자가 이름에 맞는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 드레인 마감 30초가 세 곳에서 독립적으로 결정된다
|
||
- slug: `messaging-transport-spi-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L659`
|
||
- owning-module: `messaging-transport-spi` · priority: `P3`
|
||
- classification: `MessagingLifecycle.DEFAULT_DRAIN_DEADLINE`(public), `DefaultMessagingRuntimeRegistry.DEFAULT_DRAIN_DEADLINE`(private), `MessagingShutdownLifecycle`의 생성자 인자. public 상수가 같은 leaf 안에 있는데 다른 클래스가 자기 private 복사본을 쓴다. `MessagingLifecycleTest.theDefaultDrainDeadlineMatchesTheDesign`이 public 쪽만 고정하므로 private 쪽이 바뀌어도 통과한다. 그리고 §17 첫 항목대로 public 상수가 있는 인터페이스는 구현체가 없다 — 즉 살아 있는 값(private)이 죽은 인터페이스의 값(public)을 참조하지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 26 — state-ownership-and-concurrency
|
||
|
||
> 상태의 소유와 전이 — 원자성·순서·회수. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — 배수 조정자가 가변이고 동기화가 없다
|
||
- slug: `grpc-admin-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-admin.md#L181`
|
||
- owning-module: `grpc-admin` · priority: `P3`
|
||
- classification: `phasesRun`(`ArrayList`), `startedAt`, `completedUnaryCalls`, `signalledStreams` 가 평범한 필드다. `synchronized`·`volatile`·동시 자료구조가 없다. 같은 리프의 건강 레지스트리는 정반대다 — `ConcurrentHashMap` 둘과 `volatile boolean draining`. 즉 이 리프는 동시성을 인지하고 있고 한 클래스에만 적용했다. 조정자의 javadoc 이 대기를 호출자에게 맡긴다고 적으므로 단일 호출자 전제로 읽을 수 있다. 다만 그 전제가 자바독에 적혀 있지 않고, `unaryDrainComplete` 는 반복 호출을 전제한 형태라 종료 훅과 상태 조회가 다른 스레드에서 닿기 쉽다. 수정은 단일 스레드 전제를 자바독에 적거나, 형제 클래스와 같은 수준으로 맞추는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 깃발 홀더가 가변이고 동기화가 없다
|
||
- slug: `grpc-advanced-bootstrap-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md#L209`
|
||
- owning-module: `grpc-advanced-bootstrap` · priority: `P3`
|
||
- classification: `GrpcAdvancedFeatureFlags` 는 두 `EnumMap` 을 `enable`·`withGrade` 로 갱신하고, `available`·`active` 가 같은 맵을 읽는다. `synchronized`·`volatile`·동시 자료구조가 없다. 시작 시 전부 설정하고 그 뒤로 읽기만 한다면 안전 공개 문제만 남는다. 다만 두 메서드가 `this` 를 돌려주는 유창한 형태라 런타임 중 갱신을 권하는 모양이고, `active()` 는 순회 중 갱신에 노출된다. 같은 저장소가 이 형태를 다른 리프에서 결함으로 기록했다(`GrpcCompletionReconciler` 의 동기화 없는 `ArrayList`). 여기서는 등급과 깃발이 요청 경로에서 읽히므로 같은 노출이 생길 수 있다. 수정은 홀더를 불변으로 만들고 `enable`·`withGrade` 가 새 인스턴스를 돌려주게 하는 것이다. 이 저장소가 다른 곳에서 쓰는 형태다(`GrpcProtoStyleManifest.allowingWellKnownTypes` 등).
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 리졸버의 개정 가드가 비교 후 교체가 아니다
|
||
- slug: `grpc-advanced-resilience-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-resilience.md#L176`
|
||
- owning-module: `grpc-advanced-resilience` · priority: `P2`
|
||
- classification: `GrpcCustomResolver` 의 javadoc 이 지키겠다고 하는 것은 명확하다. 빈 집합은 `GrpcEndpointSnapshot` 의 생성자가 지키므로 성립한다. 개정 가드는 그렇지 않다. `AtomicReference` 를 쓰면서 읽기와 쓰기 사이에 원자성이 없다. 개정 5 와 6 을 든 두 스레드가 같은 `applied`(개정 4)를 읽으면 둘 다 `supersedes` 를 통과하고, 나중에 `set` 하는 쪽이 이긴다. 6 이 먼저 쓰이고 5 가 덮으면 **채널이 옛 엔드포인트로 되돌아간다** — 개정 번호가 존재하는 이유가 정확히 그것을 막는 것이다. `listener.accept(update)` 도 `set` 밖에 있으므로, `applied` 의 최종 값이 옳더라도 리스너(=채널)가 받는 순서는 뒤집힐 수 있다. 채널은 마지막으로 받은 것을 믿는다. 같은 형태가 이 가족에 셋이다. 정본이 같은 리프 안에 있다는 점이 §12.2 의 대조와 같다 — 이 리프는 예산에서는 CAS 를 쓰고 리졸버에서는 쓰지 않는다. `a stale revision is dropped rather than applied` 는 단일 스레드에서 개정 2 를 적용한 뒤 개정 1 을 제시한다. 순차적으로는 가드가 정확히 작동한다. 이 리프가 배선되지 않으므로 P2. 리졸버는 정의상 외부 발견 소스가 밀어 넣는 것이고, 그 소스가 한 스레드만 쓴다는 보장은 이 클래스가 하지 않는다. `applied.updateAndGet` 안에서 판정과 교체를 함께 하거나, `compareAndSet(observed, snapshot)` 이 실패하면 다시 읽어 판정한다. 리스너 통지는 성공한 CAS 뒤에 그 CAS 가 이긴 순서로 해야 한다 — 예산 쪽의 `tryConsume` 루프가 같은 리프 안의 본보기다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 체크포인트 전진이 `ConcurrentMap` 위의 확인 후 쓰기다
|
||
- slug: `grpc-advanced-streaming-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-streaming.md#L180`
|
||
- owning-module: `grpc-advanced-streaming` · priority: `P3`
|
||
- classification: `GrpcClientStreamCheckpoint.advancedTo` 가 뒤로 가는 것을 거부하고, 그 메시지가 원인을 정확히 짚는다 — "two writers are checkpointing one session". 그 가드가 보는 것은 **호출한 스레드가 읽은 값** 이다. 두 스레드가 순번 5 와 6 을 적용하며 같은 체크포인트(4)를 읽으면 둘 다 `advancedTo` 를 통과한다. 5 를 든 쪽이 나중에 `put` 하면 체크포인트는 6 에서 5 로 **뒤로 간다** — `advancedTo` 가 막겠다고 한 바로 그 상태이고, 이번에는 예외 없이 조용히 일어난다. 그러면 순번 6 의 메시지가 다시 `APPLY` 로 판정되어 두 번 적용된다. 이 클래스가 존재하는 이유가 정확히 그것을 막는 것이다. `ConcurrentHashMap` 에는 이 형태를 위한 연산이 있다. `compute` 안에서는 읽기와 쓰기가 원자적이므로, 뒤처진 쪽이 `advancedTo` 의 예외를 실제로 받는다 — 가드가 설계대로 발화한다. 같은 리프의 `GrpcDemandController` 는 모든 공개 메서드가 `synchronized` 이고, `GrpcBidiSequenceTracker` 도 그렇다(§12.2 가 그것을 이 가족의 모범으로 든다). 중복 제거기만 `ConcurrentMap` 의 원자 연산을 쓰지 않는다. 중복 제거기 시험 아홉 개가 전부 단일 스레드다. 순차적으로는 `advancedTo` 가 정확히 작동하고, 전용 시험(`aCheckpointRecordsWhatWasApplied`)이 그것을 확인한다 — 확인하는 것은 record 의 메서드이지 맵에 쓰는 경로가 아니다. 미배선이므로 P3. 다만 이 클래스의 javadoc 이 "The application effect and this checkpoint belong in one transaction" 이라고 적어 둔 것과 함께 보면, 이 자리는 배선되는 날 트랜잭션 경계와 함께 다시 설계될 곳이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `rotate` 가 비교 후 교체가 아니라 덮어쓰기다
|
||
- slug: `grpc-client-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-client.md#L120`
|
||
- owning-module: `grpc-client` · priority: `P2`
|
||
- classification: `install` 은 정확하다. `rotate` 는 그렇지 않다. 두 회전이 동시에 들어오면 둘 다 같은 `previous` 를 읽고, 둘 다 대체본을 만들고, 나중 `set` 이 앞의 대체본을 덮는다. 덮인 대체본은 어디에도 등록되지 않는다 — `draining` 목록에 들어가는 것은 `previous` 뿐이다. 그러므로 그 세대는 배수도 회수도 되지 않고, 그 위에서 시작된 호출은 아무도 세지 않는다. 클래스가 이 문제를 인지하고 있다는 증거가 같은 파일에 있다 — `install` 의 비교 후 교체와 `AtomicReference` 선택이다. 회전 쪽만 그 규율에서 벗어나 있다. 수정은 `holder.compareAndSet(previous, replacement)` 로 바꾸고 실패 시 다시 읽어 판정하거나 던지는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 비원자적 감소가 세대를 영구히 회수 불가로 만든다
|
||
- slug: `grpc-client-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-client.md#L149`
|
||
- owning-module: `grpc-client` · priority: `P2`
|
||
- classification: 카운터가 1 일 때 두 스레드가 동시에 끝나면 둘 다 조건을 통과해 둘 다 감소시켜 −1 이 된다. 그 결과가 이 리프에서는 구체적이다. 정확히 0 을 요구한다. 음수가 되면 조용해짐 판정이 영원히 거짓이고, `retireQuiescent` 가 그 세대를 결코 제거하지 않는다. 회전이 반복될수록 `draining` 목록이 자란다. 같은 형태가 이 가족의 다른 두 곳에도 있다(`GrpcAdmissionController.release`, `GrpcStreamAdmission.release`). 그쪽은 경계가 느슨해지는 결과였고, 이쪽은 자원이 회수되지 않는 결과다. 수정은 `updateAndGet(v -> Math.max(0, v - 1))` 이나 `decrementAndGet()` 후 하한 보정이다. 같은 가족의 `GrpcRetryBudget` 이 정확한 비교 후 교체 루프를 이미 쓴다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 배수 목록의 순회가 동기화 밖에서 일어난다
|
||
- slug: `grpc-client-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-client.md#L176`
|
||
- owning-module: `grpc-client` · priority: `P3`
|
||
- classification: `Collections.synchronizedList` 는 개별 연산만 동기화한다. 순회는 호출자가 그 목록을 잠그고 해야 한다는 것이 그 API 의 계약이다. `List.copyOf(...)` 와 `stream()` 둘 다 순회다. 회전이 동시에 `add` 하면 동시 변경 예외가 가능하다. 그리고 읽고 지우는 두 단계가 원자적이지 않으므로, 그 사이에 조용해진 세대가 추가되면 이번 회수에서 빠진다. 후자는 다음 호출에서 회수되므로 무해하다. 수정은 `CopyOnWriteArrayList` 로 바꾸는 것이다. 배수 목록은 쓰기가 드물고 읽기가 잦아 그 자료구조의 전형적 용례다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 릴리스 버전 불변성이 프로세스 안에서만 성립한다
|
||
- slug: `grpc-codegen-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-codegen.md#L220`
|
||
- owning-module: `grpc-codegen` · priority: `P3`
|
||
- classification: 발행 이력이 발행자 인스턴스의 필드다. 새 프로세스는 아무것도 기억하지 못하므로 같은 버전을 다른 해시로 다시 발행하려는 시도가 통과한다. 이 클래스가 존재하는 이유가 그 규칙이다 — "refuses to let a released version change underneath its consumers." 그 규칙이 지켜지는 범위가 한 발행자 인스턴스의 수명이다. 빌드마다 새 프로세스가 도는 것이 정상 형태이므로, 실제로 이 검사가 무언가를 막으려면 이력이 산출물 저장소나 파일에서 와야 한다. `GrpcSchemaBaseline` 이 이미 릴리스된 해시를 들고 있으므로 그 방향의 재료는 있다. 덧붙여 이 맵은 동기화되지 않는다. 발행자를 공유해 병렬로 평가하면 경합한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 목록으로 보고하는 검증기가 주소 수 0 에서 던진다
|
||
- slug: `grpc-discovery-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-discovery.md#L187`
|
||
- owning-module: `grpc-discovery` · priority: `P3`
|
||
- classification: `profile.resolverProfile(n)` 이 `new GrpcResolverProfile(DNS, …, n)` 을 만들고, 그 정규 생성자가 거부한다. 그래서 `violations(profile, 0)` 은 빈 목록도 위반 목록도 아닌 `IllegalArgumentException` 이다. 같은 메서드가 `profile == null` 에는 명시적으로 던지고 나머지는 목록으로 답하므로, 호출자는 이 API 를 "던지지 않고 보고한다" 로 읽는다. `expectedAddressCount` 는 이 리프가 계산하지 않고 입력으로 받는 값이고(§16), 그 출처는 헤드리스 레코드의 DNS 조회 결과다. 롤아웃 중 파드가 모두 교체되는 순간이나 셀렉터가 어긋난 서비스에서 그 답은 0 이다. 그것은 이 리프가 다루는 문제 영역 안의 상태이지 프로그래밍 오류가 아니다 — 그리고 운영자가 가장 보고받고 싶어 할 상태다. `GrpcResolverProfile` 쪽 거부 자체는 옳다. 값 객체가 "주소 0 개인 목표"를 표현하지 않는 것은 §4 의 두 겹 분담과 일치한다. 어긋난 것은 그 위에 얹힌 검증기가 그 예외를 그대로 통과시킨다는 점이다. `violations` 가 `expectedAddressCount < 1` 을 먼저 보고 위반 문자열로 보고한 뒤 나머지 검사를 건너뛴다. 그러면 이 리프가 답할 수 있는 가장 중요한 배포 상태 하나가 예외가 아니라 목록의 한 줄이 된다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `markCommitted` 는 던지고 `markFailed` 는 조용히 넘어간다
|
||
- slug: `grpc-operation-ledger-jpa-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-operation-ledger-jpa.md#L192`
|
||
- owning-module: `grpc-operation-ledger-jpa` · priority: `P3`
|
||
- classification: 커밋 쪽의 근거는 자바독에 있다 — 청구 없이 커밋하면 변경은 내구적이고 보호받지 못한다. 실패 쪽에는 근거가 없다. 청구가 사라진 뒤 도착한 종결 실패가 아무 흔적도 남기지 않는다. 회수가 청구를 지운 뒤 원래 소유자가 실패를 기록하려는 경우가 그 형태다. 의도라면 그 이유를 자바독에 적어야 하고, 아니라면 커밋 쪽과 같게 다뤄야 한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 완료 조정자가 요청 경로에서 동기화 없는 가변 리스트를 변경한다
|
||
- slug: `grpc-policy-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L271`
|
||
- owning-module: `grpc-policy` · priority: `P2`
|
||
- classification: `synchronized`·`Concurrent*`·`volatile`·`Lock` 전부 0 이고 단일 스레드 전용 표기도 없다. 같은 리프의 `GrpcSerializedStreamWriter` 는 아홉 마커로 제대로 닫혀 있어, 이 리프가 동시성을 인지하고 있음을 보여 준다. `reconcile` 은 완료 결과가 불확실한 호출마다 불린다 — 장애 상황에서 동시에 몰리는 경로다. 그리고 `pending` 이 담는 것은 사람이 조정해야 하는 연산 목록이므로, 유실은 조정되지 않은 채 잊히는 연산이 된다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 스트림 수명 조정자의 배수 신호가 스레드를 건너면서 `volatile` 이 아니다
|
||
- slug: `grpc-policy-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L285`
|
||
- owning-module: `grpc-policy` · priority: `P2`
|
||
- classification: 두 메서드의 호출자가 다른 스레드다. `signalDrain()` 은 서버가 내려갈 때 종료 훅이 부르고, `terminationDue(...)` 는 스트림 자신의 틱에서 불린다 — 클래스 javadoc 이 검사 순서를 "then drain, because a server that has been told to stop should stop before its own timers fire" 로 규정한 그 틱이다. 평범한 `boolean` 이고 `volatile`·`synchronized`·`AtomicBoolean` 어느 것도 없다. 자바 메모리 모델 아래서 틱 스레드가 이 쓰기를 관측할 보장이 없다. 관측하지 못하면 스트림은 배수 명령을 받고도 계속 돌고, 최대 수명(기본 1시간)이 차야 끝난다. 같은 저장소가 같은 뜻의 플래그를 두 번은 `volatile` 로 적었다(§12.3). 세 번째만 빠졌다. 수정은 `volatile boolean` 한 단어다. `heartbeat` 의 `lastActivity` 는 같은 문제가 아니다 — 스트림 틱 스레드만 만진다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 릴리스 게이트의 입력이 전부 호출자가 손으로 만드는 값이다
|
||
- slug: `grpc-testkit-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-testkit.md#L172`
|
||
- owning-module: `grpc-testkit` · priority: `P3`
|
||
- classification: 세 입력 중 어느 것도 실제 레인 결과나 실제 산출물에서 오지 않는다. 게이트를 부르는 곳은 자기 테스트 하나뿐이고, 그 테스트가 세 값을 리터럴로 만든다. 이 형태 자체는 이 저장소의 다른 게이트와 다르다. mongo 가족의 증거 검증기는 테스트 결과 XML 을 읽고 파일의 수정 시각까지 본다. 이쪽 게이트는 그런 산출물 판독기를 갖지 않는다. 지금은 무해하다 — 릴리스 절차가 이 게이트를 부르지 않기 때문이다. 기록하는 이유는 §17.1 을 고쳐 레인을 자동으로 돌리게 되면, 그 결과를 이 게이트에 넣어 주는 코드가 함께 필요하다는 점이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 저널의 `itemsCompleted` 단조성이 인터페이스 계약에 없다
|
||
- slug: `messaging-admin-runtime-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L936`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P3`
|
||
- classification: `AdminOperationJournal` javadoc 은 구현 의무 셋을 명시하면서 이것을 빠뜨렸고, `fail` 의 `@param` 은 오히려 문자 그대로 저장하라고 읽힌다. 유일한 호출자는 낡은 값을 넘긴다. 두 구현이 각각 clamp 해서 무사한 상태다(`EVD-308`). 두 가지 중 하나가 필요하다. 인터페이스 javadoc 에 "`itemsCompleted` 는 단조 증가해야 하며 구현은 기존 값보다 작은 값을 무시한다" 를 명시하거나, 호출자가 실제 체크포인트 값을 넘기도록 고친다. 후자가 더 정직하다 — 지금 `journal.fail(lease, lease.resumeFrom(), …)` 은 "이번 시도가 아무것도 못 했다" 고 주장하는 것이고, 그것은 대개 사실이 아니다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 리플레이가 리스를 받지만 재개하지 않는다
|
||
- slug: `messaging-admin-runtime-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L942`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P3`
|
||
- classification: `executeReplay` 는 `journal.begin(...)` 으로 리스를 받고 `lease.resumeFrom()` 을 쓰지 않는다. `ReplayService.replay(...)` 시그니처에 재개 지점이 없고 체크포인트 콜백도 없다. 클래스 javadoc 의 "a retry continues the same operation" 은 리드라이브에만 해당한다. 리플레이가 재개 불필요하다면(같은 구간을 다시 읽는 것이 멱등이므로) 그 근거를 적고, 저널 사용을 "중복 실행 방지" 로만 한정하는 것이 낫다. 재개가 필요하다면 리드라이브와 같은 형태로 맞춘다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다
|
||
- slug: `messaging-claim-check-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L516`
|
||
- owning-module: `messaging-claim-check`
|
||
- rule: 같은 튜닝 값이 두 계층에 있으면 어느 쪽이 이기는지 정한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — claim check 문턱이 두 곳에서 독립적으로 정해진다
|
||
- scope: 멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 반환을 순번에 맞추는 조각이 production 에 없고, 시험이 그 자리를 스스로 메운다
|
||
- slug: `messaging-rabbit-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-rabbit.md#L238`
|
||
- owning-module: `messaging-rabbit` · priority: `P2`
|
||
- classification: 이 어댑터의 핵심 보장(§1)은 반환과 확인을 **같은 발행** 에 묶는 데 달려 있다. 묶는 열쇠는 순번이다. 그런데 AMQP 의 `basic.return` 콜백은 순번을 주지 않는다. 교환기·라우팅 키·속성·본문만 온다. 그래서 발행자가 순번을 메시지에 실어 보내고 반환에서 되읽어야 한다. `RabbitHeaderMapper.toProperties` 전문에 그런 헤더가 없다. 쓰는 것은 `msg.*` 예약 헤더들과 AMQP 의 `messageId`·`correlationId`·`timestamp`·`deliveryMode` 뿐이다. 그 조각이 존재하는 곳은 시험 하나다. 그 메서드의 javadoc 이 문제를 정확히 서술한다. "the adapter has to" 인데 어댑터는 하지 않는다. `RabbitChannelPublisher` 의 javadoc 은 **등록 경합**(확인이 `basicPublish` 반환보다 먼저 올 수 있다)만 설명하고 이 상관 문제는 언급하지 않는다. 결과는 이렇다. 언젠가 `RabbitChannelPublisher` 를 구현하는 사람은 이 헤더 규약을 다시 발명해야 하고, 발명하지 않으면 `onReturn` 이 호출되지 않아 unroutable 발행이 **`CONFIRMED` 로 보고된다** — 이 어댑터가 존재하는 이유로 든 바로 그 실패다. 수정은 순번 헤더를 `RabbitHeaderMapper` 나 `RabbitPublishMapper` 로 올려 production 계약으로 만들고, 그 이름을 `RabbitChannelPublisher` javadoc 에 적는 것이다. 지금은 그 규약이 시험 파일 20줄에만 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다
|
||
- slug: `messaging-security-f08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L697`
|
||
- owning-module: `messaging-security`
|
||
- rule: 가변 필드로 상태 전이를 표현하면 가시성을 함께 정한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `CredentialRuntime.material`이 동기화되지 않는다
|
||
- scope: 보안·승인 표면. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 함께 읽히는 두 맵은 한 값으로 묶는다
|
||
- slug: `messaging-spring-cloud-stream-bridge-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L579`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- rule: 함께 읽히는 두 맵은 한 값으로 묶는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 두 맵 갱신이 원자적이지 않다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다
|
||
- slug: `messaging-spring-cloud-stream-bridge-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L597`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- rule: 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 활성화 프로퍼티 키가 에러 메시지에만 존재한다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
---
|
||
|
||
## TOPIC 27 — schema-and-data-contracts
|
||
|
||
> 스키마와 데이터 계약 — 진화·직렬화·레지스트리. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — 예외가 들고 있는 능력이 `transient` 라 역직렬화 뒤 사라진다
|
||
- slug: `grpc-advanced-bootstrap-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md#L312`
|
||
- owning-module: `grpc-advanced-bootstrap` · priority: `P3`
|
||
- classification: `transient` 는 보통 직렬화 가능하지 않은 필드를 담은 `Serializable` 클래스에 대한 정적 분석 경고를 끄려고 붙인다. 그런데 열거형은 언제나 직렬화 가능하다 — 여기서 `transient` 가 막을 문제가 애초에 없다. 대가는 있다. 예외가 직렬화를 거쳐 오면 `capability()` 가 `null` 이다. 메시지 문자열은 살아남으므로 사람이 읽는 데는 지장이 없고, 그래서 눈에 띄지 않는다. 이 예외를 던지는 `require` 자체가 리프 밖에서 불리지 않으므로(§12.1) 오늘 도달하지 않는다. `transient` 를 지우는 것이 수정 전부다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 비교 픽스처에 비교 대상이 없다
|
||
- slug: `grpc-advanced-edition-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-edition.md#L180`
|
||
- owning-module: `grpc-advanced-edition` · priority: `P2`
|
||
- classification: `compatibility.proto` 의 주석이 존재 이유를 적는다. 그 쌍둥이가 저장소에 없다. 한 곳뿐이다. 같은 필드와 번호를 proto3 로 선언한 파일이 없으므로 비교가 성립하지 않는다. 그리고 두 번째 전제도 없다. 이 저장소에는 protobuf 플러그인이 어디에도 없다 — `grpc-proto-contract` 와 `adapter-inbound-grpc` 의 build.gradle 이 그 사실을 주석으로 명시한다. 그러므로 편집 파일도 proto3 파일도 컴파일되지 않고, 유선 바이트와 JSON 을 비교할 산출물 자체가 만들어지지 않는다. 결과적으로 `GrpcEditionCompatibilityReport` 는 사람이 손으로 채우는 기록이 된다. 승격 게이트가 그것을 읽어 판정하므로, 게이트의 입력이 측정이 아니라 선언이다. Advanced 가족이라 오늘의 배포에는 영향이 없다. 기록하는 이유는 이 리프의 목적이 "공개 서비스가 옮겨 가기 전에 그 실패를 찾는 것" 이고, 그 실패를 찾을 장치가 픽스처 하나만 있고 짝이 없다는 점이다. `compatibility_proto3.proto` 를 같은 디렉터리에 두어 필드·번호·JSON 이름을 맞추고, 두 파일을 컴파일해 산출물을 비교하는 레인을 만든다. 그 레인이 생기기 전까지는 `GrpcEditionCompatibilityReport` 가 측정이 아니라 선언이라는 것을 자바독에 적는 편이 낫다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 픽스처의 메서드 경로가 서비스 × 메서드 교차곱이다
|
||
- slug: `grpc-codegen-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-codegen.md#L239`
|
||
- owning-module: `grpc-codegen` · priority: `P3`
|
||
- classification: `stub.<name>(` 호출 하나가 그 파일이 import 한 **모든** 서비스에 대해 메서드 경로를 만든다. javadoc 의 규칙 서술은 단수형이다 — "a method is a `stub.<name>(` call, mapped to `<service>/<UpperCamelName>`". 서비스가 둘 이상일 때 어느 서비스인지는 소스 텍스트만으로 알 수 없고, 코드는 전부에 붙이는 쪽을 골랐다. 결과는 존재하지 않는 메서드 경로를 요구하는 픽스처다. 서비스 둘과 메서드 셋이면 요구 경로가 여섯 개가 되고, 그중 셋은 어떤 후보 스키마에도 없으므로 `breaksAgainst` 가 항상 `METHOD_PATH` 파괴를 보고한다. 그러면 `GrpcSchemaArtifactPublisher.evaluate` 가 모든 발행을 거부한다. 커밋된 픽스처는 서비스가 하나(`DocumentServiceGrpc`)라 지금은 정확하다. 두 번째 소비자 픽스처를 추가하는 순간 성립한다. 수정은 호출자 변수의 선언 타입을 함께 읽어 메서드를 서비스에 귀속시키거나, 서비스가 둘 이상인 픽스처를 거부하는 것이다. 후자는 지금 형태의 근사를 명시적으로 만든다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 직렬화 가능하다고 선언한 예외가 자기 내용을 직렬화하지 않는다
|
||
- slug: `grpc-core-api-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-core-api.md#L241`
|
||
- owning-module: `grpc-core-api` · priority: `P3`
|
||
- classification: `serialVersionUID` 는 이 타입이 직렬화된다는 선언이고, `transient` 는 유일한 필드가 그 직렬화에서 빠진다는 선언이다. 둘이 함께 있으면 역직렬화된 예외는 `context == null` 이고, 공개 메서드 둘 중 하나(`requiresReconciliation()`)가 NPE 를 던진다. `transient` 자체는 강제된 선택이다 — `GrpcFailureContext` 가 `Serializable` 을 구현하지 않으므로 필드를 남기면 예외가 직렬화되지 않는다. 기록하는 이유는 이 리프의 서술 규율과 대비되기 때문이다. 다른 자리에서는 부재마다 이유가 붙어 있다("There is no factory that takes raw metadata, and that absence is the design"). 여기에는 `transient` 의 이유도, 역직렬화 뒤의 계약도 적혀 있지 않다. 도달성은 낮다. gRPC 예외가 자바 직렬화를 지나는 경로는 이 저장소에 없다. 수정은 셋 중 하나다 — `GrpcFailureContext` 와 그 구성 요소를 `Serializable` 로 만들거나, `serialVersionUID` 를 지워 직렬화를 지원하지 않음을 명시하거나, `context()` 와 `requiresReconciliation()` 이 null 문맥을 다루도록 하고 그 이유를 적는 것.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 직렬 스트림 기록기의 가장 오래된 것 버리기가 잘못된 메시지의 바이트를 뺀다
|
||
- slug: `grpc-policy-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L249`
|
||
- owning-module: `grpc-policy` · priority: `P2`
|
||
- classification: 버려지는 것은 꺼낸 봉투인데 빼는 값은 새 메시지의 크기다. 봉투는 크기를 성분으로 담지 않으므로 이 지점에서 버려지는 크기를 알 방법이 없다. 계산을 따라가면 이렇다. 한 번의 DROP_OLDEST 마다 `queuedBytes` 는 `nextBytes` 만큼 빠졌다가 `enqueue` 에서 같은 값만큼 다시 더해진다 — **순변화 0**. 그런데 큐의 실제 내용은 `nextBytes - droppedBytes` 만큼 바뀐다. 그 차이가 매 낙차마다 쌓인다. 방향은 둘 다 틀렸다. 들어오는 메시지가 버려지는 것보다 크면 추적값이 실제보다 **낮아져** 바이트 경계가 늦게 발화한다(메모리). 반대면 실제보다 **높아져** 경계가 이르게 발화한다(불필요한 종료·낙차). 누적 바이트는 흐름 제어 정책의 판정 입력이고, 바이트 경계의 존재 이유가 javadoc 에 있다 — 개수 경계만 있으면 메모리 한도를 가장 큰 메시지가 정한다. `flush()` 가 큐를 비우면서 `queuedBytes = 0L` 로 되돌리므로 오차가 flush 를 건너 누적되지는 않는다. 그래서 이것은 영구 드리프트가 아니라 한 flush 주기 안의 폭주 구간에서 바이트 경계를 잘못 판정하는 결함이다. 낙차가 일어나는 상황이 곧 소비자가 못 따라가는 상황이고, 그때 flush 간격이 가장 길어진다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `reserved 2 to 5;` 범위가 개별 숫자로만 수집되어 `RESERVED_HISTORY` 오탐이 된다
|
||
- slug: `grpc-proto-contract-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-proto-contract.md#L175`
|
||
- owning-module: `grpc-proto-contract` · priority: `P3`
|
||
- classification: `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` 는 상한 상수로 다루거나 그 메시지에 대해 검사를 통과시킨다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 커밋 스키마 게이트가 파일 목록을 하드코딩한다
|
||
- slug: `grpc-proto-contract-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-proto-contract.md#L203`
|
||
- owning-module: `grpc-proto-contract` · priority: `P3`
|
||
- classification: 리소스 디렉터리를 훑지 않는다. 이 리프에 세 번째 `.proto` 를 추가하면 이 테스트를 함께 고치기 전까지 판정되지 않고, 빌드는 초록으로 남는다. 같은 저장소가 다른 곳에서 이 형태를 이미 경계했다 — 빠뜨림이 통과가 되는 게이트다. 수정은 `proto/**` 아래 `.proto` 를 전부 열거해 돌리는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 열거형 안의 `reserved` 는 수집되지 않는다
|
||
- slug: `grpc-proto-contract-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-proto-contract.md#L265`
|
||
- owning-module: `grpc-proto-contract` · priority: `P3`
|
||
- classification: `scan` 은 스코프 종류로 갈라진다. `reserved` 수집은 `scanMessageMember` 안에만 있다. proto3 는 열거형에도 `reserved 2, 15;` 와 `reserved "OLD_VALUE";` 를 허용하고, 열거형 값을 지울 때 번호를 예약하는 것은 필드와 같은 이유로 필요하다 — 예약하지 않고 재사용하면 옛 클라이언트가 보낸 정수가 다른 뜻으로 해석된다. 지금 `SchemaHistory` 에 열거형 이름으로 삭제 이력을 넣으면, 스키마가 정확히 예약했더라도 `scan.reservedNumbers` 에 그 이름이 없으므로 `RESERVED_HISTORY` 오탐이 난다. §17.1 의 범위 문법 문제와 같은 방향(fail-closed)이고 같은 자리에서 고칠 수 있다. `reserved` 수집을 스코프 종류와 무관하게 먼저 시도한 뒤 나머지 판정을 갈래로 보낸다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — `dataschema`가 채워질 경로가 없다
|
||
- slug: `messaging-cloudevents-f04`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L547`
|
||
- owning-module: `messaging-cloudevents` · priority: `P3`
|
||
- classification: `toCloudEvent`가 `encoded.schemaReference().flatMap(SchemaReference::schemaUri).ifPresent(builder::withDataSchema)`로 `dataschema`를 채운다. 그런데 세 codec(JSON·Avro·Protobuf) 모두 `SchemaReference.of(subject, version)`로 만들고, 그 factory는 `schemaUri`를 `Optional.empty()`로 둔다. `dataschema`는 CloudEvents 소비자가 페이로드를 해석하는 데 쓰는 표준 속성이다. 항상 비어 있으므로 이 프로파일이 만드는 CloudEvent는 스키마 위치를 알리지 않는다. `schemaversion` 확장이 그 자리를 대신하지만 그것은 비표준 확장이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 감사 sink 인터페이스가 사용처에서 다시 선언된다
|
||
- slug: `messaging-observability-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L699`
|
||
- owning-module: `messaging-observability` · priority: `P3`
|
||
- classification: `MessagingAuditSink.record(MessagingAuditEvent)`와 같은 시그니처를 `RedriveService:208`이 자기 중첩 인터페이스로 선언한다. `messaging-admin-runtime`은 `messaging-observability`에 의존할 수 있다(registry 확인). `MessagingAuditSink.inMemory()`가 제공하는 구현을 admin-runtime이 쓸 수 없다. 그리고 감사 sink의 계약(분리된 보존·접근·무결성 요구)이 문서화된 곳과 실제로 구현되는 곳이 다르다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 선언된 content type과 실제 인코딩이 조용히 갈라질 수 있다
|
||
- slug: `messaging-runtime-core-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L727`
|
||
- owning-module: `messaging-runtime-core` · priority: `P3`
|
||
- classification: `encode`가 `codecs.find(message.contentType()).orElseGet(codecs::defaultCodec)`으로 폴백한다. 출하 registry에는 JSON codec 하나만 등록된다. 봉투가 `application/avro`를 선언해도 JSON으로 인코딩되고, `EncodedMessage`의 content type은 codec이 정하므로 `application/json`이 된다. 실패하지 않고 **다른 포맷으로 성공**한다. 소비 측이 봉투의 원래 선언을 믿고 디코더를 고르면 어긋난다. `DestinationProfile.schema().codec()`이 목적지의 codec을 선언하는데 그 값과 대조하는 코드가 이 경로에 없다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 진화 판단이 두 곳에 있고 형태가 반대다
|
||
- slug: `messaging-schema-avro-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L533`
|
||
- owning-module: `messaging-schema-avro` · priority: `P2`
|
||
- classification: `isTransitive`는 `SchemaCompatibilityValidator`(public static)와 이 leaf(private static)에 글자까지 같은 복사본이 있다. 방향 판정은 전자가 허용목록, 후자가 거부목록이다. 오늘 7개 모드에서 결과는 같지만 형태가 반대이므로 `SchemaCompatibility`에 값이 추가되는 순간 갈라진다 — 허용목록은 "검사 안 함", 거부목록은 "양방향 검사". 그리고 이 중복은 schema-api의 javadoc이 명시적으로 막으려던 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다
|
||
- slug: `messaging-schema-avro-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L542`
|
||
- owning-module: `messaging-schema-avro`
|
||
- rule: 컬렉션 순서가 계약이면 양쪽에서 같은 방향으로 적는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `history` 순서 계약이 port와 게이트에서 반대다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다
|
||
- slug: `messaging-schema-avro-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L560`
|
||
- owning-module: `messaging-schema-avro`
|
||
- rule: 안정 코드는 판단 단위로 정하고 구현 단위로 정하지 않는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 에러 코드 어휘가 형제 codec과 갈라진다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 포맷 중립 payload 정책이, 자기 상수를 두고 JSON codec의 상수를 참조한다
|
||
- slug: `messaging-schema-json-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L463`
|
||
- owning-module: `messaging-schema-json` · priority: `P2`
|
||
- classification: `MessagingCoreAutoConfiguration:410-413`이 `new PayloadPolicy(JacksonMessageCodec.DEFAULT_MAX_BYTES, JacksonMessageCodec.DEFAULT_MAX_BYTES / 2)`를 만든다. 그런데 `PayloadPolicy` 자신이 같은 값의 public 상수 `PayloadPolicy.DEFAULT_MAX_BYTES`(`messaging-policy/PayloadPolicy.java:17`)를 갖고 있다. `MessagingAdmissionController`는 목적지의 codec이 무엇이든 지나는 관문이다. 그 상한이 **한 포맷 클래스**의 상수에서 나오면 두 가지가 깨진다. (1) `@ConditionalOnMissingBean`이 허용하는 대로 애플리케이션이 자기 `MessageCodecRegistry`를 내놓아 JSON codec을 대체해도, 정책은 여전히 JSON codec의 값을 읽는다. (2) 다섯 곳의 리터럴 중 하나만 바뀌면 조용히 갈라지고, `RawBytesMessageCodec` javadoc이 이미 "shared with the Stable codecs"라고 사실과 다르게 부르고 있다. 정책 소유자가 이미 존재하는데 배선이 그것을 지나쳤다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — protobuf-java 버전이 저장소에 셋이고 전역 정책이 없다
|
||
- slug: `messaging-schema-protobuf-f03`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L545`
|
||
- owning-module: `messaging-schema-protobuf` · priority: `P3`
|
||
- classification: `ext.protobufVersion = 3.25.5`(grpc 모듈 범위로 한정), 이 leaf `4.29.3`, websocket `4.33.2`. lockfile들이 세 값을 모두 고정한다. 오늘은 사고가 아니다 — 이 leaf의 `runtime_memberships`가 `[]`이라 세 버전이 한 classpath를 공유하지 않는다. **채택 시점의 부채다.** 이 leaf를 런타임에 편입시키는 순간 버전 판정이 필요해지고, 그때 참조할 전역 정책이 없다. 그리고 `src/build.gradle`의 "the single SSOT"라는 표현이 전역 정책의 존재를 시사하는데 실제 범위는 그 문장 안에서 grpc 모듈로 한정된다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — registry 조회 로직이 세 codec에 복제돼 있다
|
||
- slug: `messaging-schema-protobuf-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L554`
|
||
- owning-module: `messaging-schema-protobuf` · priority: `P3`
|
||
- classification: `requireRegistered`(JSON/Protobuf)와 `schemaFor`(Avro)가 같은 3단 판단을 각자 구현한다. JSON과 Protobuf는 `registeredVersions` 헬퍼까지 사실상 동일하다. 판단은 `MessageContractKey`의 성질이지 포맷의 성질이 아니다. 그리고 실제로 갈라졌다 — Avro만 `AVRO_` 접두 코드를 쓰고 등록 버전 목록을 메시지에 넣지 않는다. `messaging-schema-api`가 흡수할 수 있는 형태다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 28 — transport-and-provider-semantics
|
||
|
||
> 브로커와 전송의 의미론 — 배달·정착·토폴로지. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — 자동 설정이 `transport` 를 읽지 않고 전송을 하드코딩한다
|
||
- slug: `grpc-spring-boot-starter-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-spring-boot-starter.md#L221`
|
||
- owning-module: `grpc-spring-boot-starter` · priority: `P3`
|
||
- classification: `GrpcPlatformProperties.transport` 는 `GrpcServerTransport` 열거형이고 기본값이 `NETTY_SHADED` 다. 그 값을 자동 설정이 보지 않으므로 다른 값을 설정해도 만들어지는 프로파일은 같다. 지금은 무해에 가깝다 — 기본값이 하드코딩된 것과 같고, 다른 값은 §17.1 때문에 거부되지도 않지만 반영되지도 않는다. 그러나 설정 키가 존재하고 문서화되어 있으므로 운영자는 그것이 전송을 고른다고 읽는다. 수정은 프로파일 팩토리를 `transport` 로 분기시키거나, 그 키를 검증 전용임을 자바독에 명시하는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 토폴로지 검증 스택이 두 벌이고 판정이 어긋난다
|
||
- slug: `messaging-admin-runtime-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L910`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P2`
|
||
- classification: Stack A 는 파티션 스케일업을 ADVISORY 로 두어 기동을 허용하고 그 근거를 명시한다. Stack B 는 같은 상황을 차이로 보고 기동을 거부한다. 둘 다 프로덕션 호출부가 0건이라 지금은 충돌하지 않지만, `analysis/messaging/messaging-admin-api.md` §17 첫 항목대로 토폴로지 검증을 기동에 배선하는 순간 **어느 스택을 배선하느냐가 스케일업한 배포의 기동 여부를 가른다**. Stack A 가 남아야 할 것으로 보인다 — severity 구분, `physicalName` 검사, 근거 주석이 있고 테스트도 13건으로 더 두껍다. Stack B(`TopologyValidationRuntime`, `TopologyReader`, `ObservedTopology`, 그리고 그것만 쓰는 `TopologyManifest.differencesFrom`)를 제거하는 편이 낫다. 같은 코드 문자열 `TOPOLOGY_MISMATCH` 를 두 예외 타입이 쓰는 것도 정리 대상이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 실패한 리드라이브 항목의 사유가 어디에도 남지 않는다
|
||
- slug: `messaging-admin-runtime-f10`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L961`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P3`
|
||
- classification: `attempt(...)` 는 예외와 미확인을 모두 `false` 로 접는다(`RedriveService:142-156`). 감사 이벤트는 `failed` 개수만 담는다(`:135`). 사건 복구 중에 "왜 이 메시지들이 안 갔는가" 를 물을 수 있어야 하는데 답이 없다. `RedriveReport` 에 실패 사유별 집계(코드 → 개수) 정도만 추가해도 크게 달라진다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 배치 metadata를 만들고 넘길 곳이 없다
|
||
- slug: `messaging-core-api-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L844`
|
||
- owning-module: `messaging-core-api` · priority: `P2`
|
||
- classification: `KafkaBatchConsumerRegistrar:104`와 `RabbitBatchConsumerRegistrar:139`가 `BatchDeliveryMetadata`를 만들고, 두 javadoc 다 "the batch-wide metadata handed to the handler"라고 적는다. `new BatchMessageDelivery`는 저장소 전체에서 0건이고 `BatchMessageHandler` 참조도 0건이다. 두 registrar는 브로커별로 다른 정확한 계산을 한다 — Kafka는 파티션 단위 커밋이라 `settlableAsBatch=true`, Rabbit은 multiple-ack이 in-flight까지 정산하므로 `false`. 이 판단이 계산되어 어디에도 전달되지 않는다. javadoc은 존재하지 않는 수신자를 가리킨다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — SQL 실패가 재시도 불가로 분류된다
|
||
- slug: `messaging-inbox-jdbc-postgresql-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L666`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql` · priority: `P2`
|
||
- classification: `INBOX_RESERVE_FAILED`·`INBOX_QUERY_FAILED`·`INBOX_PURGE_FAILED` 셋 다 `MessagingConfigurationException`이고, 그 예외의 카테고리는 `CONFIGURATION`, `retryable = false`다. `SQLException`의 원인 대부분은 구성 오류가 아니라 **일시적 인프라**다 — 연결 끊김, 데드락, 락 타임아웃, 커넥션 풀 고갈. `FailureCategory`는 "the stable classification a retry engine, DLQ router, and dashboard all agree on"이고 `retryable = false`는 재시도 엔진이 즉시 파킹한다는 뜻이다. 같은 leaf의 `INBOX_ACTION_FAILED`는 `TRANSIENT_INFRASTRUCTURE`/`retryable = true`로 정확히 분류된다 — 같은 파일 안에서 기준이 갈린다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 천장에 닿아 일시정지된 파티션을 재개하는 경로가 없다
|
||
- slug: `messaging-kafka-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka.md#L265`
|
||
- owning-module: `messaging-kafka` · priority: `P2`
|
||
- classification: `pollOnce` 의 파티션 루프는 세 경우에 그 파티션을 멈춘다. 이 세 경로 중 어느 것도 `retries.pauseUntil(...)` 을 부르지 않는다. 그런데 폴 루프가 파티션을 재개하는 곳은 하나뿐이다. `retries` 에 항목을 넣는 곳은 `QueuedSettlement.enqueueRequeue` 하나이고, 그것은 핸들러 실패·타임아웃·명시적 requeue 경로다. 천장·배수·풀 거부 경로는 등록하지 않는다. 따라서 천장 때문에 멈춘 파티션은 **폴 루프가 스스로 재개하지 않는다.** 재개할 수 있는 것은 외부에서 부른 `resume(scope)` 이나 재조정뿐이다. `maxInFlightPerOrderingUnit` 의 기본값은 1 이다(`DestinationSettings.Consumer`). 한 폴이 같은 파티션의 레코드를 둘 이상 돌려주는 순간 두 번째에서 `tryAcquire` 가 거짓이 되고, 그 파티션이 멈춘다. 그 뒤 작업자가 끝나 `coordinator.release` 로 슬롯이 비어도 `consumer` 는 여전히 일시정지 상태다. 같은 파일이 `coordinator.pause(...)` 와 `consumer.pause(...)` 를 구분해서 쓴다 — `applySettlements` 의 `PAUSE_AND_SEEK` 는 둘 다 부르고, 천장 경로는 `consumer` 쪽만 부른다. 그래서 조정자는 그 파티션을 멈춘 것으로 알지 못하고, 결과적으로 `tryAcquire` 는 계속 참을 답하는데 브로커에서 레코드가 오지 않는다. 천장 경로가 `retries.pauseUntil(partition, seekBackTo, Duration.ZERO, now)` 를 등록하면 다음 주기의 `applyDueResumes` 가 즉시 재개한다. 지연이 0 이므로 `dueForResume` 이 곧바로 돌려준다. 배수 경로는 재개하지 않는 것이 맞고, 풀 거부 경로는 천장과 같다. 소비 경로가 조립되지 않으므로(§12.1) P2. 배선하는 순간 …
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 오염된 재시도 헤더가 격리되지 않고 무한 pause-and-seek 을 만든다
|
||
- slug: `messaging-kafka-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka.md#L301`
|
||
- owning-module: `messaging-kafka` · priority: `P2`
|
||
- classification: `KafkaRetryMetadataMapper.attemptOf` 는 읽을 수 없는 `msg.retry.attempt` 에 대해 fail-closed 를 택하고, 그 이유를 정확하게 적는다. 메시지가 "quarantined" 라고 말한다. 소비자는 그렇게 하지 않는다. 격리 경로는 **디코딩 실패에만** 걸려 있다. `attemptOf` 는 디코딩이 끝난 뒤 두 번째 블록에서 던지고, `MessagingConfigurationException` 은 `MessagingException` 을 통해 `RuntimeException` 이므로 두 번째 `catch` 가 잡는다. 결과는 `requeueAfterFailure()` → `PAUSE_AND_SEEK` → 같은 오프셋 재읽기 → 같은 헤더 → 같은 예외다. 즉 fail-closed 가 막으려던 것(재시도 예산 무력화)보다 나쁜 것을 만든다 — 그 파티션이 영구히 그 레코드에서 멈춘다. 그리고 javadoc 이 지적한 대로 이 헤더는 호출자가 쓸 수 있는 값이므로, 숫자가 아닌 값 하나로 파티션 하나를 정지시킬 수 있다. `ReservedHeaderForgeryTest.aMalformedRetryAttemptIsQuarantined` 는 `attemptOf` 가 던지는 것만 단언한다. 이름은 "quarantined" 인데 격리를 확인하지 않는다. `attemptOf` 호출을 디코딩과 같은 블록으로 옮겨 격리 경로에 태우거나, 두 번째 `catch` 가 예외 종류를 나누게 한다 — `MessagingConfigurationException` 은 재시도로 회복되지 않는 종류이므로 격리 대상이고, 핸들러 실패는 재시도 대상이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 시계를 주입받는 클래스가 한 곳에서만 벽시계를 읽는다
|
||
- slug: `messaging-kafka-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka.md#L342`
|
||
- owning-module: `messaging-kafka` · priority: `P3`
|
||
- classification: `KafkaConsumerRegistrar` 의 설계 성질이 javadoc 에 적혀 있다. 주기마다 `Instant now` 를 받아 `applyDueResumes(now)` 로 넘긴다. 그런데 그 짝인 등록 쪽은 이렇다. 이 리프에서 `Instant.now()` 를 읽는 유일한 자리다. 그리고 그 호출은 작업자 스레드에서 일어나므로 폴 스레드의 `now` 와 다른 순간이다. 결과는 두 가지다. 지연 재시도(`requeue(Duration)`)의 재개 시점을 고정 시계로 시험할 수 없고, 시험이 `Duration.ZERO` 밖의 지연을 다루지 못한다 — 실제로 어떤 시험도 다루지 않는다. 수정은 생성자에 `Supplier<Instant>` 를 하나 더 받는 것이다. 같은 저장소의 `MessagingShutdownLifecycle` 이 정확히 그 형태로 두 생성자를 둔다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 결함으로 판정된 메서드가 남아 있고, 실브로커 증명이 그것 위에서 돈다
|
||
- slug: `messaging-kafka-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka.md#L362`
|
||
- owning-module: `messaging-kafka` · priority: `P3`
|
||
- classification: `KafkaTransactionalPublisher` 에 같은 일을 하는 메서드가 둘 있다. `inTransaction` 의 javadoc 이 둘째를 결함으로 지목한다. `sendInTransaction` 은 public 이고 production 호출자가 없다. 호출하는 것은 시험 다섯 자리뿐이다 — 그리고 그 다섯이 실브로커 트랜잭션 증명 전부다(`KafkaTransactionIT`·`KafkaTransactionFencingIT`·`KafkaReadCommittedIT`). 고쳐진 `inTransaction` 을 시험하는 것은 `KafkaTransactionOrderingTest` 하나이고 `MockProducer` 다. 즉 실브로커에서 커밋·중단·펜싱이 증명된 것은 옛 모양이고, 새 모양은 목 위에서만 증명됐다. 기능적 차이는 크지 않다(`body` 가 비어 있으면 두 메서드는 같은 호출열을 만든다). 그래도 두 가지가 남는다 — 결함으로 판정된 순서를 만드는 public 진입점이 여전히 열려 있다는 것, 그리고 실브로커 증거가 production 경로가 아닌 것 위에 있다는 것. 수정은 ITs 를 `inTransaction(..., () -> null)` 로 옮기고 `sendInTransaction` 을 지우는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — "등록"이 아무것도 등록하지 않고 성공을 반환한다
|
||
- slug: `messaging-kafka-share-experimental-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L467`
|
||
- owning-module: `messaging-kafka-share-experimental` · priority: `P2`
|
||
- classification: `KafkaShareGroupRegistrar.register(profile, spec)`이 `spec`을 `Objects.requireNonNull`로만 처리하고 버린다. `ShareRegistration`은 `profile`과 `AtomicBoolean` 둘만 갖는다. Kafka 소비자가 만들어지지 않고(`import org.apache.kafka` 0건), `spec.sink`가 저장되지 않으므로 어떤 전달도 일어나지 않는다. 반환된 registration은 `isActive() == true`를 보고한다. 같은 클래스의 javadoc이 pause를 조용히 무시하는 것을 거절한 이유로 "would let a retry policy that depends on pausing appear to work while doing nothing"을 든다. `register` 자체가 정확히 그 형태다 — 성공을 반환하고 아무것도 하지 않으며 `isActive()`가 true다. 오늘 호출자가 없으므로 사고는 아니지만, 이 leaf를 배선하는 사람이 가장 먼저 부를 메서드다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다
|
||
- slug: `messaging-kafka-share-experimental-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L512`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- rule: 에러 메시지가 지시하는 설정은 그 설정을 읽는 코드와 함께 존재해야 한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 활성화 프로퍼티 키가 에러 메시지에만 존재한다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 닫힌 전송의 거절이 영구 업무 실패로 분류된다
|
||
- slug: `messaging-nats-experimental-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md#L209`
|
||
- owning-module: `messaging-nats-experimental` · priority: `P3`
|
||
- classification: `rejectedLocally` 가 `FailureCategory.PERMANENT_BUSINESS` 를 고정으로 쓰고, 두 호출자 중 하나가 `NATS_TRANSPORT_CLOSED` 다. 자매 어댑터(Pulsar)와 같은 형태이고 같은 판단이다 — 종료 중이라는 것은 이 세대의 사정이지 업무의 영구 실패가 아니다. 같은 파일의 `classify` 는 범주를 신중히 나눈다. 두 리프가 같은 형태를 공유하므로 수정도 함께 하는 편이 낫다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `NatsJetStreamProfileValidator` 를 호출하는 곳이 저장소에 없다. javadoc 링크 하나가 유일한 흔적이다
|
||
- slug: `messaging-nats-experimental-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md#L217`
|
||
- owning-module: `messaging-nats-experimental` · priority: `P2`
|
||
- classification: 75줄짜리 검증기가 이 어댑터의 시작 시점 판단 넷을 들고 있다 — 실험 스위치가 꺼져 있으면 거부, 최소 한 번 배달에 코어 NATS 거부, 순서 있는 소비자와 경쟁 작업자 동시 사용 거부, 키 순서 목적지 거부. 저장소 전역에서 이 클래스 이름이 나오는 곳은 두 줄뿐이다. 하나는 선언이고 하나는 **javadoc 링크**다. 코드 호출자 0, 테스트 0. `validate` 는 `jetStreamEnabled` · `orderedConsumer` · `competingWorkers` · `enabled` 를 전부 인자로 받는다. 즉 스스로 아무것도 관찰하지 않고, 호출자가 이미 알고 있는 사실을 넘겨 줘야만 판단한다. 그런 호출자가 없으니 이 판단들은 한 번도 실행된 적이 없다. 전송의 클래스 javadoc 이 "그래서 검증기가 시작 시 그 조합을 거부한다"고 단언한다. 읽는 사람에게 이 어댑터는 코어 NATS 오설정으로부터 보호되는 것처럼 보이는데, 실제로는 아무 게이트도 걸려 있지 않다. 실험 등급이라 지금 당장 사고가 나지는 않지만, 이 어댑터를 실전에 붙이는 사람이 가장 먼저 신뢰할 문장이 지금 사실이 아니다. 같은 형태를 이 저장소에서 여러 번 봤다 — 채점기는 있는데 그 채점기에 값을 넣어 주는 생산자가 없는 구조(`GrpcRawApiImportRule` · `GrpcApplicationBoundaryRules` · `GrpcNettyParityContract` 등). 이쪽이 더 나쁜 쪽인 이유는 그 리프들에서는 최소한 테스트가 리터럴을 먹여 판단 자체는 실행해 보는데, 여기서는 그것조차 없다는 점이다. 어댑터 조립 지점에서 `validate` 를 부르거나, 그럴 지점이 아직 없다면 최소한 프로파일 생성 시점에 걸리도록 옮긴다(§4 의 압축 생성자가 이미 실행되는 유일한 게이트다). 어느 쪽도 못 하겠다면 전송 javadoc 의 "refuses ... at startup" 을 사실에 맞게 고친다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — 브로커 홉 추적기가 소비자를 갖지 않는다
|
||
- slug: `messaging-observability-f03`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-observability.md#L690`
|
||
- owning-module: `messaging-observability` · priority: `P3`
|
||
- classification: `MessagingTracer`의 leaf 밖 참조 0. 이 클래스가 존재하는 이유는 "the only way the two spans meet is if the context travels in the message headers"다. `messaging-core-api`의 `TraceContext`가 봉투 필드로 있고(그쪽 §4.11), 어댑터가 헤더를 매핑한다. 그런데 `traceparent`/`tracestate`/`baggage`를 헤더로 옮기는 **명시된 수단**을 아무도 쓰지 않는다. 어댑터가 각자 하고 있다면 `MessageHeaders.platform` 사용 여부와 빈 추적 처리가 어댑터마다 다를 수 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 재시도 엔진과 DLQ 조정자가 bean으로 만들어지고 주입되는 곳이 없다
|
||
- slug: `messaging-policy-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L781`
|
||
- owning-module: `messaging-policy` · priority: `P2`
|
||
- classification: `MessagingCoreAutoConfiguration`이 `RetryDecisionEngine`(:167)과 `DeadLetterOrchestrator`(:179)를 `@Bean @ConditionalOnMissingBean`으로 만든다. 두 타입을 받는 production 코드는 각각 `KafkaRetryExecutor`와 `KafkaDeadLetterPublisher`/`RabbitDeadLetterPublisher`뿐이고, **셋 다 저장소 어디에서도 생성되지 않는다.** 같은 설정의 51개 bean 중 두 타입을 인자로 받는 `@Bean` 메서드가 없다. 컨텍스트에 두 bean이 앉아 있고 `MessagingAutoConfigurationTest`류의 `hasSingleBean` 검사는 통과한다 — 즉 **bean 존재 검사가 배선을 증명하지 않는다.** 그리고 이 leaf가 가장 공들인 두 축(6개 재시도 모드·8단 판단 순서·full jitter·capability 인식, DLQ 발행-후-정산 불변식·예약 헤더 6개)이 실행되지 않는다. 42개 테스트 중 16개가 이 두 축을 검증한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 재시도와 DLQ 각각에 두 개의 구현이 있고 정본이 표시되지 않았다
|
||
- slug: `messaging-policy-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L799`
|
||
- owning-module: `messaging-policy` · priority: `P3`
|
||
- classification: 재시도: `DefaultRetryDecisionEngine`(6모드·백오프·순서 인식) vs `DefaultDeliveryProcessor`(고정 지연·시도 횟수 미확인). DLQ: `DeadLetterOrchestrator`(예약 헤더 6개 부착) vs `DefaultDeliveryProcessor.DeadLetterPublisher`(헤더 없음). 둘 다 조립되지 않았다. 오늘 경쟁하지 않지만, 소비 경로를 배선하는 사람이 둘 중 하나를 고르게 되고 코드가 어느 쪽이 정본인지 말하지 않는다. 두 javadoc이 각각 자기가 플랫폼 규칙의 구현이라고 서술한다. 그리고 선택 결과가 다르다 — `DefaultDeliveryProcessor` 경로로 DLQ된 메시지에는 실패 카테고리·코드·원본 목적지·시도 횟수가 붙지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — DLQ 메타데이터의 두 시각이 항상 같다
|
||
- slug: `messaging-policy-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L808`
|
||
- owning-module: `messaging-policy` · priority: `P3`
|
||
- classification: `DeadLetterMetadata`가 `firstFailureAt`과 `lastFailureAt`을 별도 필드로 선언하는데, 유일한 생산 지점인 `DeadLetterOrchestrator:89-97`이 둘 다 `delivery.metadata().receivedAt()`으로 채운다. 두 헤더(`msg.first-failure-at`, `msg.last-failure-at`)가 DLQ 메시지에 붙는데 항상 같은 값이다. 운영자가 "이 메시지가 얼마나 오래 실패해 왔는가"를 헤더에서 알 수 없다. `ReservedHeaders`가 두 이름을 따로 정의한 목적이 실현되지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 닫힌 전송의 거절이 영구 업무 실패로 분류된다
|
||
- slug: `messaging-pulsar-experimental-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-pulsar-experimental.md#L196`
|
||
- owning-module: `messaging-pulsar-experimental` · priority: `P3`
|
||
- classification: 두 호출자가 이 메서드를 쓴다. 첫째는 영구 업무 실패가 맞다. 둘째는 아니다. 종료 중이라는 것은 이 세대의 사정이고, 다음 세대나 다른 인스턴스에서는 같은 메시지가 발행된다. 같은 파일의 `classify` 가 분류를 신중히 나눈다 — 사전 거절은 `CONFIGURATION`, 모호는 `TRANSIENT_INFRASTRUCTURE`. 닫힘만 그 규율 밖에 있다. 전송되지 않았다는 증거(`notTransmitted`)는 옳다. 어긋난 것은 범주뿐이다. 수정은 닫힘에 `TRANSIENT_INFRASTRUCTURE` 를 주거나, 두 호출자가 범주를 인자로 받게 하는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 확인 등급이 요구에서 파생되고, 그 요구를 뒷받침하는 강제는 목적지 종류 하나에만 걸린다
|
||
- slug: `messaging-rabbit-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-rabbit.md#L210`
|
||
- owning-module: `messaging-rabbit` · priority: `P3`
|
||
- classification: 증거의 등급이 브로커가 무엇을 했는지가 아니라 프로파일이 무엇을 **요구했는지** 에서 나온다. 대부분의 경우 이 파생은 성립한다. 두 강제가 그것을 받쳐 준다. `RabbitHeaderMapper.toProperties` 가 배달 모드를 무조건 `PERSISTENT` 로 둔다. RabbitMQ 는 지속 메시지를 디스크에 쓴 뒤에 확인한다. `RabbitProfileValidator.validateDestination` 이 내구 작업 큐에 쿼럼 큐를 요구한다. 쿼럼 큐의 확인은 다수 복제 뒤에 온다. 빈틈은 둘째 강제의 범위다. 작업 큐가 아닌 목적지에는 쿼럼 요구가 없다. 교환기로 발행하는 목적지가 `REPLICATION_OR_PERSISTENCE_ACK` 를 요구하면, 그 교환기에 바인딩된 큐가 고전 큐여도 어댑터는 그 등급을 보고한다. 지속 모드 덕분에 디스크 기록은 보장되지만 복제는 보장되지 않는다. 이 저장소의 규율은 증거가 관측에서 나와야 한다는 것이다 — `MessagingCapabilities` 의 javadoc 이 "a silently weakened guarantee is indistinguishable from a working one until the incident" 라고 적는다. 수정은 쿼럼 요구를 목적지 종류가 아니라 **요구된 확인 등급** 에 걸거나, 작업 큐가 아닌 목적지에서는 등급을 `BROKER_ACK` 로 낮추는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — SCRAM 자격을 RabbitMQ 의 데모 기구로 조용히 매핑한다
|
||
- slug: `messaging-rabbit-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-rabbit.md#L274`
|
||
- owning-module: `messaging-rabbit` · priority: `P3`
|
||
- classification: `RABBIT-CR-DEMO` 는 RabbitMQ 의 시연용 challenge-response 인증 기구(`rabbit_auth_mechanism_cr_demo`)의 이름이고 기본 활성이 아니다. RabbitMQ 는 SCRAM-SHA 를 구현하지 않으므로 `SaslScram` 에 대응하는 AMQP 기구가 없다는 것 자체는 사실이다. 문제는 그 사실을 다루는 방식이 같은 파일 안에서 일관되지 않다는 것이다. 그리고 이웃 어댑터의 같은 클래스가 정확히 이 상황에 대한 규범을 적어 두었다. `SaslScram` 에도 그 규범이 적용되어야 한다. 플러그인이 없는 브로커에서는 handshake 가 알아보기 어려운 오류로 실패하고, 있는 브로커에서는 시연용 기구로 인증한다. 수정은 `Nkey` 와 같이 거부하거나, `PLAIN` 으로 매핑하고 그 이유를 주석으로 남기는 것이다. 어느 쪽이든 지금처럼 말없이 데모 기구를 고르는 것보다 낫다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `pause` 의 의미가 SPI 하나 뒤에서 두 브로커에 다르게 구현된다
|
||
- slug: `messaging-rabbit-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-rabbit.md#L335`
|
||
- owning-module: `messaging-rabbit` · priority: `P3`
|
||
- classification: 호출자가 이 단계를 기다리고 나면 "일시정지되었다" 고 읽는다. 실제로 일어난 것은 `onMessage` 가 이후 배달에 `false` 를 답하기 시작한 것뿐이고, 리스너 컨테이너는 계속 배달을 밀며 그 배달들은 미확인 상태로 재배달된다. 즉 정지가 아니라 거부-재배달 루프다. Kafka 쪽은 같은 SPI 를 정반대로 구현하고 그 이유를 적는다. AMQP 에는 대응하는 수단이 있다 — `basicCancel` 로 소비자를 취소하거나 컨테이너를 멈추는 것. 지금 구현이 그것을 하지 않는 이유는 어디에도 없다. 전용 시험(`aPausedQueueRefusesDeliveriesSoTheBrokerRedeliversThem`)의 이름이 이미 실제 동작을 정확히 말한다. 그러므로 수정은 둘 중 하나다 — 컨테이너를 실제로 멈추거나, SPI 의 javadoc 에 "브로커에 따라 정지가 거부-재배달일 수 있다" 를 명시하는 것.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다
|
||
- slug: `messaging-runtime-core-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L736`
|
||
- owning-module: `messaging-runtime-core`
|
||
- rule: 안정 코드는 운영자의 행동이 갈리는 지점마다 나눈다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 같은 실패 코드가 두 completion에 쓰인다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다
|
||
- slug: `messaging-schema-json-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L472`
|
||
- owning-module: `messaging-schema-json`
|
||
- rule: 실패 코드는 운영자의 다음 행동이 갈리는 지점마다 나눈다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 파서 방어 여섯 갈래가 하나의 실패 코드로 접힌다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### OPEN QUESTION — 브리지의 바인더 쪽 절반이 없다
|
||
- slug: `messaging-spring-cloud-stream-bridge-f02`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L561`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge` · priority: `P3`
|
||
- classification: `ChannelSend`·`BridgedHandler` 두 함수형 인터페이스가 바인더 접촉면이고, 그 구현이 저장소에 없다. `MessagingBindingBridge` javadoc은 "a service already has Stream bindings and needs to reach the same destinations without a rewrite"를 존재 이유로 든다. 정책·검증·정직성 세 층이 완성돼 있고 그것들을 실제 바인딩에 연결하는 코드가 없다. `runtime_memberships: []`와 정합하므로 오늘의 결함은 아니지만, 이 leaf의 이름이 약속하는 것("spring-cloud-stream-bridge")이 절반만 존재한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `Faults` 내부클래스 57줄이 3개 모듈에 바이트 단위로 복제되어 있다
|
||
- slug: `messaging-testkit-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L960`
|
||
- owning-module: `messaging-testkit` · priority: `P3`
|
||
- classification: `sha256` 이 세 곳 모두 `3028b459…` 로 동일하다(`EVD-299`). 총 171줄. `messaging-testkit/src/main` 에 `DefaultFaultController` (또는 `RecordingFaultController`) 하나를 두고 세 하니스가 그것을 필드로 갖게 하면 된다. `MessagingAdapterHarness.faults()` 의 반환 타입은 `FaultController` 그대로이므로 외부 API 변경이 없다. 이 복제가 위험한 이유는 P2 와 겹친다: `rejectPublish` 의 전송 증거를 고치려면 지금은 세 파일을 고쳐야 하고, 세 파일이 어긋나면 어댑터마다 다른 결함 의미를 갖게 된다 — 이 리프가 존재하는 이유 자체를 무너뜨린다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다
|
||
- slug: `messaging-transport-spi-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L668`
|
||
- owning-module: `messaging-transport-spi`
|
||
- rule: 멱등 종료를 보장하는 컴포넌트는 종료 이후의 등록도 정의한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 종료 중 `install`이 닫히지 않는 창
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 같은 개념의 sentinel은 계층을 넘어 하나로 정한다
|
||
- slug: `messaging-transport-spi-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L677`
|
||
- owning-module: `messaging-transport-spi`
|
||
- rule: 같은 개념의 sentinel은 계층을 넘어 하나로 정한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — pause scope sentinel이 두 인터페이스에서 다르다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
---
|
||
|
||
## TOPIC 29 — security-policy-enforcement
|
||
|
||
> 보안 정책이 실제로 강제되는 지점. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — 비밀 필드 검사가 스냅숏의 네 구획 중 하나에만 적용된다
|
||
- slug: `grpc-admin-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-admin.md#L162`
|
||
- owning-module: `grpc-admin` · priority: `P3`
|
||
- classification: 메시지는 "a platform snapshot must not carry …" 로 스냅숏 전체를 말한다. 검사 대상은 `channelProfileHashes` 하나다. 같은 채널 이름 공간을 쓰는 두 맵이 더 있다 — `resolverAndLoadBalancerByChannel`, `retryOwnerByChannel`. 그리고 `registeredServices` 목록과 `serviceHealth` 맵이 있다. 어느 것도 검사되지 않는다. 세 맵의 키 집합이 같아야 한다는 요구가 없으므로, 어떤 채널이 나머지 두 맵에만 있으면 그 이름은 검사를 지나지 않는다. `grpc-advanced-diagnostics` 의 스냅숏은 같은 형태의 자기 검사를 두 구획(주소 목록, 자원 판본 키)에 적용한다. 두 리프의 규율이 갈린다. 수정은 네 구획 전부를 같은 검사에 넣는 것이다. 값이 아니라 키를 보는 검사이므로 비용이 낮다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 마스킹이 IPv4 만 알고, 그 결과 "마스킹되지 않은 주소" 검사가 나머지 형태를 전부 통과시킨다
|
||
- slug: `grpc-advanced-diagnostics-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-diagnostics.md#L167`
|
||
- owning-module: `grpc-advanced-diagnostics` · priority: `P2`
|
||
- classification: IPv4 가 아닌 주소는 패턴에 맞지 않아 **입력 그대로 반환된다.** 그리고 스냅숏 생성자의 검사는 이렇게 되어 있다. 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. 그러므로 IPv4 가 아닌 주소는 전부 이 검사를 통과한다. 세 번째와 네 번째가 문제다. 이 플랫폼이 겨냥하는 배포 형태가 쿠버네티스이고(`grpc-discovery` 전체가 그 주제다), 헤드리스 레코드의 엔드포인트는 파드 DNS 이름이며 이중 스택 클러스터에서는 IPv6 주소다. 편집기가 막으려 한 것이 정확히 그것이다 — "a diagnostics endpoint that publishes peer addresses publishes every tenant's connection." `unix` 소켓 경로도 통과한다. 그것은 호스트 파일 시스템 경로다. 테스트의 주소 리터럴이 전부 IPv4 다 — `10.4.13.201:9090` · `10.9.13.201` · `10.4.x.x` · `10.5.x.x`. IPv6 도 호스트 이름도 없다. 마스킹을 형태별로 나눈다. IPv6 는 앞 두 그룹만 남기고 나머지를 `:x:x` 로, 호스트 이름은 최상위 라벨 몇 개만 남기고, 그 밖의 형태는 `unknown` 으로 접는다. 그리고 검사를 "결과가 입력과 같으면 통과" 가 아니라 "알려진 마스킹 형태와 일치해야 통과" 로 뒤집는다. 지금 형태는 마스킹이 모르는 입력을 전부 안전하다고 판정한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 금지 필드 검사가 키에만 적용되고 값에는 적용되지 않는다
|
||
- slug: `grpc-advanced-diagnostics-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-diagnostics.md#L206`
|
||
- owning-module: `grpc-advanced-diagnostics` · priority: `P3`
|
||
- classification: `redact(...)` 도 같다 — 금지 이름의 키를 버리고, 남은 값은 주소 필드일 때만 마스킹한다. 값 자체가 자격증명 형태인지는 보지 않는다. `grpc-observability` 의 태그 정책은 값도 본다(UUID·`sha256:`·`bearer ` 패턴). 같은 저장소의 두 관측 편집기가 값 검사에서 갈린다. xDS 자원 버전은 보통 짧은 숫자나 해시라 도달성이 낮다. 기록하는 이유는 두 편집기의 규율이 다르다는 점이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 정책의 자바독이 하지 않는 거부를 한다고 적고, 승격 승인이 두 곳에 따로 있다
|
||
- slug: `grpc-advanced-edition-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-edition.md#L216`
|
||
- owning-module: `grpc-advanced-edition` · priority: `P3`
|
||
- classification: **첫째, 서술과 코드가 어긋난다.** "refuses an approval nobody recorded" 에 해당하는 검사가 없다. `promotionApproved` 는 읽히지도 검증되지도 않고 그대로 저장된다. `new GrpcEdition2024Policy(Set.of(), Set.of(), true)` — 옵트인한 모듈도 공개 서비스도 없는데 승인만 참인 정책 — 이 아무 저항 없이 만들어지고, `serviceMayMove` 는 모든 서비스에 참을 답한다. **둘째, 같은 사실이 두 곳에 따로 있다.** 게이트는 정책을 인자로 받지도, 참조하지도 않는다. 그래서 "ADR 이 없다"고 판정한 게이트와 "승인되었다"고 답하는 정책이 동시에 성립할 수 있고, 둘을 맞추는 코드가 없다. §17.2 가 지적한 "두 게이트가 서로를 부르지 않는다" 와 같은 구조가 정책과 게이트 사이에도 있다. 정책도 게이트도 production 호출자가 없고(§12.1), 승격은 사람이 수행하는 절차다. 다만 이 리프가 존재하는 이유가 "그 절차를 코드로 적어 두는 것" 이므로, 적힌 절차 안에서 같은 사실이 둘로 갈라져 있는 것은 그 목적에 어긋난다. `promotionBlockers` 가 `GrpcEdition2024Policy` 를 받아 `promotionApproved` 를 `promotionAdr` 자리에 쓰고, 정책 생성자가 자바독대로 "승인이 참이면 그 근거(공개 서비스 집합이 비어 있지 않을 것 등)"를 요구한다. 어느 쪽도 하지 않겠다면 자바독의 그 문장을 지운다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `RESOURCE_EXHAUSTED` 매핑이 그 상태의 두 출처 중 하나만 가정한다
|
||
- slug: `grpc-core-api-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-core-api.md#L180`
|
||
- owning-module: `grpc-core-api` · priority: `P3`
|
||
- classification: 이 분기는 전송이 미시작을 증명하지 못한 뒤에 도달한다. 즉 "보냈는지 모르지만 이 상태 코드는 거절을 뜻한다" 는 판정이다. 목록의 나머지 일곱은 서버가 일을 시작하기 전에 답하는 상태다. `RESOURCE_EXHAUSTED` 는 두 출처를 갖는다. 이 플랫폼 자신의 승인 제어기가 부하를 흘려보낼 때 — 일을 쓰기 전이므로 거절이 맞다. 원격 서버가 작업 중 자원(할당량·디스크)을 소진했을 때 — 부분 커밋이 있을 수 있다. 이 클래스의 원칙은 보수적이다. 자바독이 두 기본값(`DEADLINE_EXCEEDED`·`UNAVAILABLE` 를 모호로)을 계획의 전역 제약이라 부르고, 그 이유는 "보냈는지 모르면 모호" 다. `RESOURCE_EXHAUSTED` 는 그 원칙에서 벗어난 유일한 항목이다. `ABORTED` 가 모호에 있는 것과 대비된다 — 트랜잭션 충돌은 서버가 일을 시작한 뒤의 상태이고, 그래서 모호다. 수정은 둘 중 하나다. `RESOURCE_EXHAUSTED` 를 모호로 옮기거나, 그 상태를 이 플랫폼이 발행한 것과 원격이 발행한 것으로 구분해 전자만 거절로 두는 것이다. 후자는 증거 축에 발신자 정보를 요구하므로 전자가 현실적이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 스트림 승인의 경계가 동시성 아래에서 새고, caller별 맵이 줄지 않는다
|
||
- slug: `grpc-policy-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L184`
|
||
- owning-module: `grpc-policy` · priority: `P2`
|
||
- classification: 읽고 비교한 뒤 별도로 증가한다. 경계에 있는 N 개 스레드가 모두 통과한다. 이 클래스의 javadoc 이 서술하는 실패 상황이 곧 고동시성이다 — "a client that reconnects on every error opens streams faster than the old ones close." 재접속 폭풍에서 경계가 가장 많이 샌다. `release` 도 같은 형태라 음수로 갈 수 있다. 그리고 `perCaller` 에서 항목이 제거되지 않는다. `computeIfAbsent` 가 호출자 지문마다 계수기를 만들고 `release` 는 값만 줄인다. 서로 다른 호출자 수만큼 맵이 자란다 — `grpc-observability` 의 `GrpcMetricCardinalityPolicy` 가 지표 태그에 대해 명시적으로 막는 것과 같은 종류의 증가이고, 여기에는 그 가드가 없다. 정본이 같은 리프에 있다 — `GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 자격증명 회전이 비교 후 교체가 아니고, 배수 완료가 진행 중인 회전을 되돌릴 수 있다
|
||
- slug: `grpc-policy-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L204`
|
||
- owning-module: `grpc-policy` · priority: `P2`
|
||
- classification: `AtomicReference` 를 쓰면서 두 메서드 모두 읽고 나서 조건 없이 쓴다. 두 회전이 같은 `observed` 를 읽으면 둘 다 승계 검사를 통과할 수 있고, 나중 `set` 이 앞의 것을 덮는다. 덮인 회전이 배수 대상으로 기록해 둔 세대가 상태에서 사라진다. 그 세대 위의 호출은 아무도 배수하지 않는다. javadoc 이 이 상황을 이미 알고 있다 — 승계 검사의 존재 이유로 "the usual reason for one is two rotators racing" 를 든다. 검사는 있고 원자성이 없다. `completeDrain()` 이 자기가 읽은 `observed.current()` 로 새 상태를 만든다. 읽기와 쓰기 사이에 회전이 일어나면, 그 회전이 활성화한 세대가 지워지고 **이전 세대가 다시 현재가 된다.** 즉 방금 교체된 자격증명이 되살아난다. 클래스의 존재 이유가 "in-flight 작업을 떨어뜨리지 않고 자격 자재를 교체하는 것" 인데, 이 경로는 교체 자체를 되돌린다. 수정은 두 메서드를 비교 후 교체로 바꾸는 것이다. `rotate` 는 `compareAndSet(observed, next)` 가 실패하면 다시 읽어 판정하고, `completeDrain` 은 `updateAndGet(s -> new State(s.current(), null, null))` 로 현재 값을 원자적으로 읽어 쓰면 된다. 후자는 한 줄이다. 같은 형태가 `grpc-client` 의 `GrpcChannelRuntimeRegistry.rotate` 에도 있다. 두 리프가 같은 자료구조를 같은 방식으로 잘못 쓴다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 승인 제어기의 세 메서드가 원자적이지 않고, 큐 계수기를 되돌리는 경로가 없다
|
||
- slug: `grpc-server-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-server.md#L211`
|
||
- owning-module: `grpc-server` · priority: `P2`
|
||
- classification: 이 리프가 SSOT 이므로 여기에 적는다. `grpc-policy` §17.1 이 이 클래스를 대조군으로 지목하는데, 지목된 쪽 문서에 판정이 없었다. **첫째, 읽고 나서 따로 증가시킨다.** 경계에 있는 N 개 스레드가 모두 통과한다. `AtomicInteger` 를 쓰면서 비교와 증가를 나눈 형태이고, 같은 가족의 정본이 `GrpcRetryBudget.tryConsume` 의 비교 후 교체 루프다. `release()`·`promoteFromQueue()` 도 같다 — `get() > 0` 을 확인한 뒤 별도로 감소시키므로, 두 스레드가 같은 마지막 하나를 보고 둘 다 감소시켜 음수가 될 수 있다. 클래스가 `Math.max(0, …)` 같은 하한도 두지 않는다. **둘째, 큐 계수기를 되돌리는 경로가 없다.** 큐에 들어간 호출도 `admitted=true` 를 받는다. 그런데 그 경로는 `queued` 만 올리고 `inFlight` 는 올리지 않는다. 그리고 끝난 호출을 반납하는 메서드는 하나뿐이다. 따라서 호출자가 `promoteFromQueue()` 를 정확히 한 번 끼워 넣지 않으면 계수기가 어긋난다 — 큐에서 실행된 호출이 끝나면 `queued` 는 그대로이고 `inFlight` 만 줄어든다. `releaseQueued()` 같은 메서드도, 그 짝짓기를 요구하는 서술도 없다. 두 시험 모두 단일 스레드이고, `releaseAndPromotionTrackCapacity` 는 `release()` 와 `promoteFromQueue()` 를 **짝지어** 부른다. 짝짓지 않는 경로는 시험되지 않는다. 오늘 호출자가 없으므로(§12.1) P2. 승인 단계를 배선하는 순간 P1 이다 — 부하 아래에서 경계가 새는 것과, 큐 계수기가 단조 증가해 `at capacity` 가 영구히 참이 되는 것이 함께 온다. 세 메서드를 비교 후 교체 루프로 바꾸고, 큐 경로에 대응하는 반납 메서드를 두거나 `promoteFromQueue` 를 `release` 안으로…
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 던져 버릴 비밀번호를 만들어 놓고 외부 프로세스의 명령줄에 싣는다
|
||
- slug: `grpc-testkit-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-testkit.md#L262`
|
||
- owning-module: `grpc-testkit` · priority: `P3`
|
||
- classification: `GrpcTlsTestMaterial` 이 상수 비밀번호를 피하는 이유를 세 줄로 적는다. 그리고 같은 클래스가 그 값을 `keytool` 인자로 넘긴다. 프로세스 명령줄은 같은 호스트의 다른 사용자가 `ps` 나 `/proc/<pid>/cmdline` 로 읽을 수 있다. 소스 리터럴보다 관측 가능성이 오히려 높다. 영향은 작다 — 값이 매번 새로 만들어지고, 키스토어는 임시 디렉터리에 있으며 `close()` 가 지운다. 기록하는 이유는 이 클래스가 정확히 그 위험 계층을 스스로 논증했다는 점이다. 완화와 노출이 같은 메서드 안에 있다. `keytool` 은 `-storepass:file` 과 `-keypass:file` 을 받는다. 임시 파일 하나면 명령줄에서 값이 사라진다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 계획 다이제스트가 승인 정규 형식과 다른 인코딩을 쓴다
|
||
- slug: `messaging-admin-api-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L944`
|
||
- owning-module: `messaging-admin-api` · priority: `P3`
|
||
- classification: `ApprovalGrant.canonicalForm()` 은 길이 접두를, `ReplayPlan.digest()`/`RedrivePlan.digest()` 는 `String.join("|", …)` 를 쓴다(§12.3(b), `EVD-304`). 현재는 충돌을 만들 수 없다 — 자유 형식 필드가 `topologyVersion` 하나뿐이기 때문이다. 그러나 그 조건은 코드 어디에도 적혀 있지 않고, 필드가 하나 추가되면 조용히 깨진다. `ApprovalGrant` 의 `appendField` 를 `PlanDigest` 쪽으로 옮겨 재사용하는 편이 낫다 — 규칙과 그 근거가 이미 같은 리프에 있다. 부수적으로 `topologyVersion` 에 형식 제약을 주는 것도 검토할 만하다. 지금은 `isBlank()` 만 본다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `VerifiedApproval` 의 위조 방지가 package-private 에만 의존한다
|
||
- slug: `messaging-admin-api-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L962`
|
||
- owning-module: `messaging-admin-api` · priority: `P3`
|
||
- classification: 이 저장소는 JPMS 를 쓰지 않는다(`module-info.java` 0개). 따라서 어떤 모듈이든 `package dev.caskeleton.messaging.admin;` 을 선언하면 `VerifiedApproval.of(grant)` 를 호출할 수 있다. 현재 그런 파일은 없지만, 이 타입의 존재 이유가 "아무도 만들 수 없다" 이므로 그 조건을 자동으로 지키는 검사가 있어야 한다. `ApprovalForgeryTest.aVerifiedApprovalCannotBeConstructedOutsideTheVerifier` 가 있으나, 그것은 같은 패키지 안에서 API 표면을 확인하는 테스트지 다른 모듈의 패키지 선언을 막지 못한다. ArchUnit 규칙 한 줄 — "`dev.caskeleton.messaging.admin` 패키지는 `messaging-admin-api` 소스 경로에만 존재한다" — 이면 된다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 같은 인가 실패 코드가 세 파일에 문자열 리터럴로 흩어져 있다
|
||
- slug: `messaging-admin-api-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L972`
|
||
- owning-module: `messaging-admin-api` · priority: `P3`
|
||
- classification: `APPROVAL_OPERATION_MISMATCH`, `APPROVAL_SOURCE_MISMATCH`, `APPROVAL_PLAN_MISMATCH`, `APPROVAL_EXPIRED` 가 `HmacApprovalVerifier`, `DestructiveOperationGuard`, `ApprovedReplayPlan`, `ApprovedRedrivePlan` 에 각각 리터럴로 존재하며 메시지 문구가 서로 다르다. 검사의 3중화 자체는 의도된 심층 방어지만(§12.2 대조군 2), 코드 문자열은 상수 하나로 모으는 편이 집계와 검색에 낫다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 파괴적 작업의 승인만 위조 가능한 형태로 남아 있다
|
||
- slug: `messaging-admin-runtime-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L883`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P2`
|
||
- classification: 생성자는 null·음수만 본다. `approval` 이 이 `operation` 을 인가하는지, 이 `destination` 을 인가하는지, `estimatedMessagesAffected` 가 승인 상한 이하인지 — 아무것도 검사하지 않는다. 계획 다이제스트 필드 자체가 없다. 이 형태가 정확히 `messaging-admin-api` 가 고쳤다고 기록한 것이다. 수정은 `REPLAY`·`REDRIVE`(복구 가능한 작업)에 적용되었고, `PURGE`·`DELETE_DESTINATION`·`OFFSET_RESET`(복구 불가능한 작업)에는 적용되지 않았다. 현재 구현체가 0건이라 실행되는 결함은 아니다(`EVD-307`). 그러나 이 인터페이스는 운영자 도구가 구현하라고 존재하는 것이고, 그 도구가 생기는 순간의 모양이 이것이다. `Approved` 를 `ApprovedReplayPlan` 과 같은 형태로 — `VerifiedApproval` + 생성자 검사 — 바꾸는 것이 맞다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 감사 싱크가 중복 선언되어 있고 레닥션 계약이 유실된다
|
||
- slug: `messaging-admin-runtime-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L930`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P3`
|
||
- classification: `RedriveService.AuditSink` 는 `MessagingAuditSink` 와 시그니처가 같다. admin-runtime 은 이미 `messaging-observability` 를 의존한다. 표준 싱크를 쓰면 세 가지가 함께 해결된다: `ReplayService` 가 형제의 중첩 타입에 의존하는 것, `InMemory` 구현 재작성, 그리고 무엇보다 **"모든 기록이 `MessagingRedactor` 를 통과했다" 는 계약**. 현재 `RedriveService:125-136` 은 목적지 이름과 details 를 그대로 넣는다. 목적지 이름은 `DestinationName` 이라 형식이 제한되어 있어 지금은 문제가 아니지만, 계약이 없는 자리에 값이 늘어나는 것을 막을 것이 없다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 격리 리플레이의 guard 우회가 `dryRun` 파라미터로 표현된다
|
||
- slug: `messaging-admin-runtime-f08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L948`
|
||
- owning-module: `messaging-admin-runtime` · priority: `P3`
|
||
- classification: 판단 자체는 근거가 있다. 다만 "승인이 필요 없다" 와 "실제로는 아무것도 하지 않는다" 가 guard 입장에서 구별되지 않는다. `DestructiveOperationGuard` 에 `skipAuthorization` 성격의 별도 경로를 두거나, 격리 리플레이는 애초에 guard 를 거치지 않는 편이 의도를 드러낸다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 자격증명 판정이 core-api보다 약하다
|
||
- slug: `messaging-observability-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L708`
|
||
- owning-module: `messaging-observability` · priority: `P3`
|
||
- classification: `MessagingRedactor.isDenied`는 27키 **정확 일치**다. `messaging-core-api`의 `MessageHeaders.carriesACredential`은 세그먼트 매칭 + 인접 결합으로 `x-api-key`·`auth-token`·`db_password`를 잡는다. redactor의 denylist에 `api_key`·`apikey`는 있으나 `x-api-key`는 없다. 두 표면이 다르지만 **더 자유로운 입력을 받는 쪽이 더 약하다.** 이 leaf 자신이 진단 맵을 "the one place where a caller can pass arbitrary keys"라고 부른다. 그리고 `recordDiagnostics`가 redaction을 첫 단계로 두는 이유가 바로 그것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 같은 TLS posture를 두 클래스가 다른 엄격도로 검사한다
|
||
- slug: `messaging-security-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L634`
|
||
- owning-module: `messaging-security` · priority: `P2`
|
||
- classification: `MessageSecurityValidator`는 hostname 검증을 `production && !hostnameVerification`일 때만 요구하고, `BrokerTlsPolicy`는 `tlsEnabled && !hostnameVerification`일 때 요구한다. 전자는 코드 없는 `IllegalArgumentException`, 후자는 안정 코드가 붙은 `MessagingConfigurationException`을 던진다. 둘 다 같은 `BrokerSecurityProfile`을 받고, 후자만 어댑터에서 실제로 호출된다. 비운영에서 TLS를 켜고 hostname 검증을 끈 구성을 두 검사가 다르게 판정한다. 그리고 이 leaf 자신의 javadoc이 그 구성을 "looks encrypted in every dashboard while accepting any certificate a man in the middle presents"라고 부른다 — 즉 더 느슨한 쪽이 그 위험을 통과시킨다. 실패 형태도 달라서 운영자가 두 어휘를 알아야 한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 권한 거부가 `AUTHORIZATION`이 아니라 `CONFIGURATION`으로 기록된다
|
||
- slug: `messaging-security-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L643`
|
||
- owning-module: `messaging-security` · priority: `P2`
|
||
- classification: `DestinationAccessValidator.requirePublish`는 `MessageAuthorizationException("DESTINATION_PUBLISH_DENIED")`을 던지고 그 카테고리는 `AUTHORIZATION`이다. 소비자가 0이다. 실제 발행 경로는 `access.mayPublish`를 직접 묻고 `rejected("PUBLISH_FORBIDDEN", ...)`을 반환하는데, `rejected(...)`는 `FailureCategory.CONFIGURATION`을 붙인다. `FailureCategory`는 "stable classification a retry engine, DLQ router, and dashboard all agree on"이다(`messaging-core-api` §4.12). 권한 거부가 구성 오류로 분류되면 보안 대시보드가 그것을 보지 못하고, 구성 오류 알림이 권한 거부로 오염된다. 그리고 `AUTHORIZATION` 카테고리를 쓰는 유일한 코드가 미사용 클래스에 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### OPEN QUESTION — ACL 매니페스트 전체가 쓰이지 않는다
|
||
- slug: `messaging-security-f03`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-security.md#L652`
|
||
- owning-module: `messaging-security` · priority: `P3`
|
||
- classification: `BrokerAclManifest`의 세 메서드(`requireApplicationRuntime`, `undeclared`, `missing`)와 두 enum이 소비자 0이다. javadoc은 "The manifest is what the platform checks itself against at startup"이라고 한다. "애플리케이션 런타임은 파괴적 권한을 갖지 않는다"는 이 leaf의 핵심 원칙 중 하나이고, `MessageSecurityValidator`가 admin **자격증명**의 부재만 검사한다. 브로커가 producer 자격증명에 `DELETE`를 준 경우는 아무도 보지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다
|
||
- slug: `messaging-security-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L679`
|
||
- owning-module: `messaging-security`
|
||
- rule: 맵 갱신 함수 안에서 I/O를 하면 그 지연이 락 범위가 된다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 자격증명 해석이 맵 bin 락 안에서 외부 I/O를 한다
|
||
- scope: 보안·승인 표면. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
---
|
||
|
||
## TOPIC 30 — declaration-and-document-drift
|
||
|
||
> 선언·문서·주석이 코드보다 앞서 있는 자리. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — 승격 게이트가 하향 전이도 승격 규칙으로 판정하고, javadoc 이 약속한 거부는 없다
|
||
- slug: `grpc-advanced-bootstrap-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md#L182`
|
||
- owning-module: `grpc-advanced-bootstrap` · priority: `P3`
|
||
- classification: 던지는 경우는 널과 `from == to` 둘뿐이다. "이 게이트가 다루는 전이가 아닐 때" 라는 조건에 해당하는 검사가 없다. 그래서 하향 전이가 승격 규칙으로 판정된다. 능력을 철회하려는 결정이 증거 부족을 이유로 막힌다. 방향이 뒤집혀 있다. 지금은 도달성이 낮다 — 이 게이트를 부르는 production 코드가 없고 테스트도 상향 전이만 넣는다. 기록하는 이유는 javadoc 이 그 거부를 이미 약속했다는 점이다. 수정은 `to.ordinal()` 이 아니라 등급의 서열을 명시한 뒤 상향 전이만 받고 나머지는 던지는 것이다. 철회는 별도 경로가 필요하다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 승격 차단 목록에 담금 기간과 실환경 항목이 없다
|
||
- slug: `grpc-advanced-edition-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-edition.md#L206`
|
||
- owning-module: `grpc-advanced-edition` · priority: `P3`
|
||
- classification: `GrpcEdition2024Gate.promotionBlockers` 가 보는 것은 셋이다 — 호환성 보고서의 문제들, 소비자 이관 계획, 승격 ADR. 같은 가족의 `GrpcAdvancedPromotionGate` 는 `EDITION_2024` 능력에 대해 일곱 증거 항목과 7일 담금을 요구한다. 두 게이트가 같은 능력의 승격을 서로 다른 기준으로 판정한다. 두 게이트가 각각 다른 것을 묻는다고 볼 수도 있다 — 하나는 편집 자체의 호환성, 하나는 능력의 운영 준비도. 다만 어느 쪽도 상대를 부르지 않고, 문서에도 두 게이트의 관계가 적혀 있지 않다. 승격을 실제로 수행할 때 어느 쪽을 만족해야 하는지가 코드에서 답해지지 않는다. 수정은 `promotionBlockers` 가 `GrpcAdvancedPromotionGate.evaluate` 의 결과를 포함하게 하거나, 두 게이트의 역할 분담을 자바독에 적는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 부트스트랩 대조가 문서 어디든의 부분 문자열을 본다
|
||
- slug: `grpc-advanced-resilience-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-resilience.md#L136`
|
||
- owning-module: `grpc-advanced-resilience` · priority: `P3`
|
||
- classification: 세 검사가 모두 문서 전체에 대한 부분 문자열 포함이다. JSON 파서를 쓰지 않은 이유는 자바독이 밝힌다 — 세 필드를 보려고 파서를 xDS 를 켜는 모든 배포의 실행 클래스패스에 올리지 않겠다는 것이다. 그 판단 자체는 이 저장소의 다른 결정들과 일관된다. 다만 검사의 형태가 그 판단보다 느슨하다. `"tls"` 가 문서 어디에든 있으면 통과한다. 통제 평면 채널이 `insecure` 로 설정되어 있고 다른 곳(예: 서버 리스너 설정)에 `tls` 라는 낱말이 있으면 두 번째 검사가 지나간다. 자원 이름공간이 주석·다른 필드·다른 서버 항목에 있어도 통과한다. 세 번째 검사가 막으려는 것은 "이 클라이언트가 자기 이름공간 밖을 구독하는 것" 인데, 문자열이 어딘가에 있다는 것은 그것이 이 클라이언트의 구독 대상이라는 뜻이 아니다. 그리고 이 검사가 막으려는 실패는 자바독이 스스로 "조용하다" 고 적은 것이다 — 아무것도 오류가 되지 않는 종류다. 느슨한 검사와 조용한 실패의 조합이 이 항목을 기록하는 이유다. 수정은 파서를 들이지 않고도 가능하다 — `"channel_creds"` 를 포함하는 객체 범위 안에서 `"type"` 값을 찾는 정도의 구조 인식이면 두 번째 검사가 실제 조건에 가까워진다. 또는 파서를 테스트 범위에만 두고 이 가드는 형태를 좁힌 정규식으로 바꾼다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 프로파일 검증기가 javadoc 이 든 두 실수 중 하나만 검사한다
|
||
- slug: `grpc-client-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-client.md#L199`
|
||
- owning-module: `grpc-client` · priority: `P3`
|
||
- classification: 구현된 것은 첫째와 **다른 것**이다. 이름이 같은 프로파일이 두 번 선언된 경우를 잡는다. javadoc 이 든 둘째는 **이름이 다르고 대상이 같은** 경우인데, 그 검사가 없다. 지도는 이름을 키로 쓰므로 같은 대상을 가리키는 두 이름은 서로를 만나지 않는다. 그리고 둘째가 실제로 더 찾기 어려운 형태다 — 이름이 같으면 설정 결속이 먼저 실패하거나 나중 것이 이기지만, 이름이 다르면 조용히 두 채널이 생긴다. 수정은 대상과 설정을 키로 하는 두 번째 지도를 두고 역방향 중복을 보고하는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 리졸버 검증기의 규칙이 하나뿐인데 javadoc 은 복수형으로 서술한다
|
||
- slug: `grpc-discovery-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-discovery.md#L177`
|
||
- owning-module: `grpc-discovery` · priority: `P3`
|
||
- classification: javadoc 은 "Checks a discovery configuration for **the things** that look right and are not" 라고 적는다. 실제로 담긴 규칙은 균형 정책의 무의미함 하나다. 나머지 위험 조합은 `GrpcResolverProfile` 정규 생성자가 이미 거부하므로 결과적으로 빈틈은 아니다. 다만 목록으로 보고하는 API 형태와 규칙 하나라는 내용이 어긋나 있어, 다음 사람이 여기에 규칙을 더할 자리로 읽거나 이미 여러 규칙이 있다고 읽는다. §17.1 이 실제로 그 자리다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `deadlineRemaining` 도 meter 가 없다. javadoc 은 그것이 기록된다고 말한다
|
||
- slug: `grpc-observability-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-observability.md#L213`
|
||
- owning-module: `grpc-observability` · priority: `P3`
|
||
- classification: §17.1 과 같은 형태가 `GrpcRpcObservation` 에도 있고, 이쪽은 클래스 javadoc 이 명시적으로 어긋난다. 두 값을 함께 들면서 "기록된다"고 단언하는데, `record(GrpcRpcObservation)` 이 등록하는 meter 는 넷이다. `queueWaitTime` 은 `QUEUE_WAIT` 타이머로 나간다. `deadlineRemaining` 은 나가는 곳이 없다 — meter 이름 상수 일곱 개 중에도 마감 잔량에 해당하는 것이 없고, `tags()` 에도 들어가지 않는다(태그로 넣으면 카디널리티가 터지므로 그것이 옳다). 그래서 이 성분을 읽는 코드는 `unusedDeadline()` 하나이고, 그 메서드의 production 호출자는 0 이다(§12.1). §4.6 은 이 성분의 검증 비대칭(음수 허용)이 "마감을 넘긴 호출을 표현하기 위한 것" 이라고 읽었다. 그 해석은 그대로 유효하다 — 다만 그 표현이 도달하는 곳이 아직 없다. 관측값으로서는 §17.1 의 `queueHighWatermark` 와 같은 처지다. `queueWaitTime` 과 같은 형태로 타이머를 하나 더 둔다(마감을 넘긴 경우는 `unusedDeadline()` 이 이미 빈 값으로 구분해 주므로 기록 대상에서 빼면 된다). 아니면 javadoc 의 "recorded" 를 "carried" 로 낮춘다. 지금은 관측 대상 둘을 나란히 약속하고 하나만 내보낸다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 오류 노출 거부 목록의 "호스트와 포트" 규칙이 IPv4 점표기만 본다
|
||
- slug: `grpc-policy-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L305`
|
||
- owning-module: `grpc-policy` · priority: `P3`
|
||
- classification: 아홉 패턴을 전부 읽으면 주소 형태를 보는 것은 이 하나다. 클래스 javadoc 은 거부 대상을 "a stack frame, a SQL fragment, a JDBC URL, a bearer token, **a host and port**, a file path" 로 서술하는데, 실제로 걸리는 host 는 IPv4 점표기뿐이다. IPv6 리터럴 — `fe80::1`, `[2001:db8::1]:5432` DNS 이름과 포트 — `documents-db.internal:5432`, `kafka-0.kafka-headless:9092` `jdbc:postgresql://db/app` 이 막히는 것은 host 규칙이 아니라 `jdbc:` 규칙 때문이다. 즉 이 구멍은 테스트에도 없다 — `exposurePolicyRefusesLeakyStrings` 의 아홉 사례 중 주소는 `upstream 10.0.3.14:5432 refused` 하나이고 IPv4 다. 닿는 경로는 `mapUnknown` 이다. 인식되지 않은 예외의 메시지를 `safeToExpose` 가 통과시키면 그대로 클라이언트로 간다. IPv6 클러스터나 쿠버네티스 서비스 이름을 쓰는 배포에서 상류 좌표가 밖으로 나간다. 등급이 P3 인 이유는 두 가지다. 이 리프가 build-only 라 오늘 닿지 않고, 노출되는 것이 자격증명이 아니라 내부 좌표다. 다만 이 정책이 존재하는 이유 자체가 "부분 마스킹이 아니라 통째 교체" 이므로, 목록에 빠진 형태는 통째로 통과한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 반환 목록이 자바독이 약속한 source order 가 아니다
|
||
- slug: `grpc-proto-contract-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-proto-contract.md#L191`
|
||
- owning-module: `grpc-proto-contract` · priority: `P3`
|
||
- classification: `validate` 의 javadoc 은 "@return every violation found, **in source order**" 라고 적는다. 실제로는 파일 앞머리의 `syntax`·`package` 위반이 40번째 줄의 `map` 위반보다 뒤에 온다. `describe()` 가 `file:line rule — detail` 형태를 만들고 그 형태의 목적이 빌드 로그를 읽는 것이므로, 정렬이 어긋나면 리뷰 목록으로서의 값이 줄어든다. 수정은 반환 직전에 `line` 으로 안정 정렬하는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 원시 API 규칙이 import 문만 보므로 완전 수식 사용과 와일드카드를 놓친다
|
||
- slug: `grpc-server-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-server.md#L167`
|
||
- owning-module: `grpc-server` · priority: `P3`
|
||
- classification: 이것이 가정에 그치지 않는 이유는 이 저장소 자신의 문체다. 같은 가족의 여러 파일이 완전 수식 참조를 본문에 그대로 쓴다. 즉 이 코드베이스에서 완전 수식 사용은 예외가 아니라 흔한 형태다. 규칙 클래스의 자바독은 "there is nothing to reach for" 를 목표로 든다. 지금 형태는 손이 닿는 경로 하나만 본다. 수정은 정규식을 타입 이름의 등장 자체로 넓히거나(오탐이 생기므로 주석·문자열 제거가 필요), 바이트코드 기반 검사로 옮기는 것이다. 후자가 이 저장소의 다른 아키텍처 게이트와 형태가 같다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다
|
||
- slug: `messaging-cloudevents-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L556`
|
||
- owning-module: `messaging-cloudevents`
|
||
- rule: 문서가 범위를 제한하면 그 제한을 누가 강제하는지 같이 적는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `CloudEventMapper` javadoc의 범위 제한이 강제되지 않는다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### OPEN QUESTION — 세 갈래 판정이 포트의 `boolean`에서 두 갈래로 접힌다
|
||
- slug: `messaging-inbox-jdbc-postgresql-f04`
|
||
- readiness: `OPEN`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L675`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql` · priority: `P3`
|
||
- classification: `InboxResult`가 세 값과 `isSafeToSettle()`을 갖는데 production은 `APPLIED`만 만든다. `InboxRepository.reserve`가 `boolean`을 반환하므로 `ALREADY_APPLIED`와 `CLAIMED_ELSEWHERE`가 같은 `false`로 들어온다. `TransactionalInboxHandler`는 그 경우 `HandleResult.success()`를 반환한다 — 정산한다. `InboxResult` javadoc이 세 값이 필요한 이유로 정확히 그 정산을 든다 — "would settle a message whose effect is still only half-written by another instance". **다만 그 상황이 PostgreSQL에서 실제로 발생 가능한지 확인하지 않았다**(§16). `ON CONFLICT DO NOTHING`이 미커밋 충돌에 대해 대기한다면 `CLAIMED_ELSEWHERE`는 도달 불가능한 상태이고 enum이 과설계인 것이며, 즉시 0을 반환한다면 이것은 실제 결함이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 지원 문서가 `deduplicatedPublish` 를 지원으로 적고, 코드는 거짓이며, 그 차이가 정확히 코드가 경고한 피해다
|
||
- slug: `messaging-kafka-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka.md#L208`
|
||
- owning-module: `messaging-kafka` · priority: `P1`
|
||
- classification: 코드의 판정이 옳고 그 근거가 javadoc 에 있다. `docs/messaging/support-matrix.md:55` 의 능력 표는 이 칸을 `O` 로 적는다. 그 차이가 무거운 이유는 이 플랫폼에서 이 플래그가 특별하기 때문이다. 능력 열둘 중 **부재가 예외를 만드는 유일한 플래그**다. 그래서 표를 읽고 중복 제거를 전제한 목적지를 설계한 팀은 실행 시점에 능력 예외를 만난다. 반대로 표를 읽고 "중복 제거가 있으니 모호를 그냥 재시도해도 된다" 고 결론지으면, 실제로는 중복이 저장된다. `MessagingCapabilities` 의 클래스 javadoc 이 그 피해를 미리 적는다 — "a silently weakened guarantee is indistinguishable from a working one until the incident." 수정은 문서 쪽이다. 코드가 이미 옳다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 태그 어휘가 존재하고 유일한 호출부가 우회해, 실패 분류가 기록되지 않는다
|
||
- slug: `messaging-observability-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L673`
|
||
- owning-module: `messaging-observability` · priority: `P2`
|
||
- classification: `DefaultMessagingObservationConvention`은 소비자가 0이다. 유일한 production 호출부(`DefaultMessagePublisher.observe:260-269`)가 `"publish"` 리터럴과 **4인자** `MessagingTags.of(...)`를 쓴다. 그 factory는 `failureCategory`와 `retryStage`를 `NONE`으로 고정한다. convention의 `publish(broker, dest, completion, Optional<FailureCategory>)`는 정확히 `failureCategory`를 채우려고 존재한다. convention javadoc이 "an adapter inventing its own spelling of 'rejected' silently breaks every alert that was watching for it"를 이유로 중앙화를 선언했고, 첫 호출부가 그것을 지나쳤다. 그리고 결과가 철자 문제에 그치지 않는다 — **`MessagingTags`가 선언한 6차원 중 4개만 채워진다.** 메트릭이 배선되더라도(§다음 항목) 실패한 발행이 `failureCategory=none`으로 기록되어, "왜 실패했는가"를 메트릭에서 나눌 수 없다. `PublishResult.failure()`에 `FailureDescriptor`가 이미 있으므로 값은 손에 있다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 타입이 문서화한 불변식은 타입이 강제한다
|
||
- slug: `messaging-observability-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L717`
|
||
- owning-module: `messaging-observability`
|
||
- rule: 타입이 문서화한 불변식은 타입이 강제한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 감사 이벤트가 redaction을 강제하지 않는다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 백오프 지터가 인스턴스를 분산시키지 못한다
|
||
- slug: `messaging-outbox-jdbc-postgresql-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L923`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql` · priority: `P3`
|
||
- classification: `jittered = capped - (capped/8) * (exponent % 3)` 는 `exponent` 만의 함수다. 같은 상태의 복제본들은 같은 값을 계산한다. javadoc 이 약속하는 "thundering herd 방지" 가 성립하지 않는다. `OutboxRelay` 가 이미 `defaultOwner()` 로 프로세스별 안정 식별자를 만든다(`pid@uuid8`). 그것의 해시를 지터에 섞으면 결정성(같은 프로세스에서 재현 가능)을 유지하면서 인스턴스 간 위상차가 생긴다. javadoc 이 난수를 거부한 이유("a random source would make the schedule impossible to test")도 그대로 지켜진다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 커넥션 획득 방식이 리프 안에서 갈린다
|
||
- slug: `messaging-outbox-jdbc-postgresql-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L929`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql` · priority: `P3`
|
||
- classification: `JdbcOutboxRepository.append` 는 `DataSourceUtils`, 나머지는 raw `dataSource.getConnection()`, `JdbcAdminOperationJournal` 은 전부 `DataSourceUtils`. 릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 된다는 판단은 타당하지만 어디에도 적혀 있지 않고, 같은 리프의 저널이 반대로 한다. `withConnection` 에 한 문장 — "릴레이 연산은 호출자 트랜잭션에 합류하지 않는다" — 을 붙이면 `append` 의 상세한 주석과 짝이 맞는다. 저널이 `DataSourceUtils` 를 쓰는 것이 의도인지도 확인이 필요하다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 구성 오류는 한 예외 타입과 안정 코드로 보고한다
|
||
- slug: `messaging-policy-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L826`
|
||
- owning-module: `messaging-policy`
|
||
- rule: 구성 오류는 한 예외 타입과 안정 코드로 보고한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 프로파일 검증 실패가 플랫폼 예외 계층 밖이다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 저장소 밖 문서를 절 번호로 인용하지 않는다
|
||
- slug: `messaging-policy-f07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L835`
|
||
- owning-module: `messaging-policy`
|
||
- rule: 저장소 밖 문서를 절 번호로 인용하지 않는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — javadoc이 해소되지 않는 설계 문서를 인용한다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다
|
||
- slug: `messaging-reliability-api-f06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L734`
|
||
- owning-module: `messaging-reliability-api`
|
||
- rule: 호출 컨텍스트가 계약이면 그 컨텍스트를 검증할 수단을 함께 정한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 트랜잭션 계약 셋이 타입으로 강제되지 않는다
|
||
- scope: 멱등·아웃박스·인박스 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — port 계약은 동시성 요구를 적는다
|
||
- slug: `messaging-schema-api-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L503`
|
||
- owning-module: `messaging-schema-api`
|
||
- rule: port 계약은 동시성 요구를 적는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — port 구현의 스레드 안전성 요구가 문서화되어 있지 않다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — 도달성 판정은 단어가 아니라 import로 확인한다
|
||
- slug: `messaging-schema-api-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L512`
|
||
- owning-module: `messaging-schema-api`
|
||
- rule: 도달성 판정은 단어가 아니라 import로 확인한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — `SchemaRegistry`라는 이름이 저장소에서 두 가지를 가리킨다
|
||
- scope: 스키마와 코덱 계열. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — CI에서 돈다고 선언한 게이트를 부르는 CI가 없다
|
||
- slug: `messaging-schema-avro-f01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L524`
|
||
- owning-module: `messaging-schema-avro` · priority: `P2`
|
||
- classification: `AvroCompatibilityGate` javadoc이 "Run in CI rather than at runtime"이라고 선언한다. 저장소 전체에서 이 클래스 참조는 자기 선언과 자기 테스트뿐이고, `src/build.gradle`의 9개 `verifyMessaging*` task 중 스키마 진화를 검사하는 것이 없다. 게이트의 존재 이유가 "한 번 발행되면 보존 로그에 영구히 남는다"인데, 그 보호가 어느 파이프라인에도 붙어 있지 않다. `AvroMessageCodec`의 미사용과 달리 이것은 membership으로 설명되지 않는다 — 런타임 편입 여부와 무관하게 CI 게이트는 붙었어야 한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 출고되는 신뢰성 체인 전체가 아무도 공급하지 않는 빈 뒤에 있고, 그 사슬이 자기 클래스 안을 가리킨다
|
||
- slug: `messaging-spring-boot-starter-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L300`
|
||
- owning-module: `messaging-spring-boot-starter` · priority: `P2`
|
||
- classification: 여섯 빈 전부가 애플리케이션이 공급해야 하는 타입에 걸려 있다. 조건 자체는 옳고 근거도 정확하다 — 플랫폼이 기본 구현을 주면 조용히 엉뚱한 곳에, 또는 아무 데도 쓰지 않게 된다. 문제는 저장소 안에 그 타입을 공급하는 코드가 없다는 것이다. `messaging-outbox-jdbc-postgresql` 의 `JdbcOutboxRepository` 는 스프링 스테레오타입도 `@Bean` 선언도 없고, `new JdbcOutboxRepository` 가 main 에 0 건이다. 그래서 이 스타터를 켠 배포는 발행 경로는 얻고 발신함 경로는 얻지 못하며, 그 사실이 시작 시점에 어떤 신호도 내지 않는다. 둘째와 셋째가 같은 설정 클래스 안에서 방금 선언된 빈의 존재를 조건으로 삼는다. 스프링은 `@ConditionalOnBean` 을 자동 설정 클래스에서만, 그리고 등록 순서에 의존하는 방식으로만 신뢰할 수 있다고 문서화한다. 지금은 첫 조건이 이미 거짓이라 결과가 드러나지 않는다. 발신함을 배선하는 순간 이 사슬이 실제로 평가된다. 같은 가족의 다른 결정과 대비된다. 관리 평면은 스위치가 켜졌을 때 만들어지지 **않는** 타입의 부재를 javadoc 에 명시한다(`DestructiveMessagingAdmin` 하나). 이쪽은 여섯이 조용히 빠진다. 수정은 둘이다. 발신함을 요구하는 설정에서 저장소 빈이 없으면 시작을 거부하는 검증(이 가족의 `StartupProfileValidation` 형태), 그리고 중계·작업자·수명을 하나의 `@Bean` 으로 합치거나 조건을 전부 최초 두 타입으로 표현하는 것.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 설정 경로의 재시도가 예외 분류를 표현할 수 없다
|
||
- slug: `messaging-spring-boot-starter-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L342`
|
||
- owning-module: `messaging-spring-boot-starter` · priority: `P3`
|
||
- classification: `RetryPolicy` 는 성분 열이고 그중 둘이 분류 집합이다. `DestinationSettings.Retry` 에는 이 둘에 대응하는 키가 없고, 컴파일러가 상수로 채운다. 빈 집합은 "기본 분류 그대로" 라는 중립값이므로 오동작은 아니다. 문제는 비대칭이다. `DestinationProfile` 을 자바로 선언한 배포는 두 집합을 조정할 수 있고, 문서대로 YAML 로 설정한 배포는 할 수 없다. 이 리프가 반복해서 근거로 든 규칙이 정확히 그 비대칭을 금지한다. 수정은 `Retry` 에 두 키를 더하는 것이다. `FailureCategory` 는 열거이므로 relaxed binding 이 그대로 처리한다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다
|
||
- slug: `messaging-spring-cloud-stream-bridge-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L570`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- rule: 한 개념의 등록 상태를 두 객체가 나눠 갖지 않는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 인터페이스를 publisher만 구현하고 두 클래스가 같은 바인딩에 각자 상태를 갖는다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 클래스 javadoc 이 강제되지 않는 규칙을 강제된다고 말한다
|
||
- slug: `messaging-testkit-f02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L950`
|
||
- owning-module: `messaging-testkit` · priority: `P2`
|
||
- classification: `BrokerFailureMatrix.java:18-20` 이 "A Stable adapter must cover every scenario. That rule is enforced by a test rather than documented" 라고 쓰고 있으나, `isComplete` 를 Stable 어댑터에 거는 테스트는 없다(`EVD-300`). 유일한 호출부는 Experimental 어댑터가 불완전함을 단언한다. 실제 Stable 인 `messaging-kafka` 는 `connection-refused` gap 을 가진 채 통과하며, 그 gap 은 같은 모듈이 명시적으로 단언한다. 코드 쪽 결정("gap 을 열거하되 비어 있음을 단언하지 않는다")은 옳고, 그 이유도 `CrossBrokerContractSuite.java:45-47` 에 적혀 있다. 문제는 **javadoc 이 갱신되지 않은 것**이다. 이 리프의 다른 javadoc 여섯 곳이 자기 이력을 정확히 남긴 것과 대비되어 더 눈에 띈다. 같은 이유로 테스트 메서드 이름 `everyStableAdapterCoversEveryFaultScenario` 도 본문과 맞지 않는다. `everyStableAdaptersGapsAreExactlyWhatTheEvidenceShows` 같은 이름이 본문을 정확히 기술한다. 수정 방향: javadoc 을 현재 규칙("Stable 은 live-broker 증거를 하나 이상 요구한다. 전 시나리오 커버리지는 목표이지 게이트가 아니며, gap 은 `knownGaps` 로 명명된다")으로 바꾸고, 테스트 이름을 본문에 맞춘다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `gitCommit` 은 기록되지만 읽혀 판정되지 않는다
|
||
- slug: `messaging-testkit-f08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L984`
|
||
- owning-module: `messaging-testkit` · priority: `P3`
|
||
- classification: `BrokerCertificationEvidence` javadoc 이 "without them the evidence cannot be checked against anything later" 라고 쓰지만, 실제로 `gitCommit` 을 읽어 무언가를 결정하는 코드는 없고 게이트는 오히려 그 필드를 비교에서 제외한다(§12.4(c)). 현재 매니페스트의 커밋은 HEAD 가 아니다(`e98b56eb` vs `21234e38`). "증거가 얼마나 오래된 트리에서 나왔는가" 를 보고하는 것은 유용한 진단이 될 수 있다 — 게이트로 만들 필요는 없고, `knownGaps` 처럼 사실로 노출하면 이 리프의 나머지 설계와 결이 맞는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 31 — runtime-contract-correctness
|
||
|
||
> 런타임 계약의 정확성 — 위 여덟에 들지 않는 나머지. 이 Topic 의 노드는 61개 canonical module SSOT 전수 recall 에서 나왔다.
|
||
> 각 노드는 SSOT §17 의 독립 finding 하나에 1:1 로 대응하고, `source` 가 그 정확한 앵커다.
|
||
|
||
### CASE — 하나의 상태 코드가 같은 메서드 안에서 두 답을 갖는다
|
||
- slug: `grpc-core-api-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-core-api.md#L200`
|
||
- owning-module: `grpc-core-api` · priority: `P3`
|
||
- classification: `forMutation` 은 스위치에 닿기 전에 `OK` 를 먼저 처리한다. 스위치의 `OK` 분기는 도달하지 않는다. 열거형 전수 처리를 컴파일러가 요구하므로 항목 자체는 필요하지만, 그 값이 위의 가드와 반대다. 결과는 잠재적 함정이다. 누군가 위의 `OK` 가드를 "중복이니까" 지우면 컴파일은 통과하고 `OK` 인 변경이 `COMPLETION_UNKNOWN` 이 된다 — 성공한 변경마다 대사(reconciliation)를 요구하게 된다. 이 리프의 다른 자리들은 그런 편집이 눈에 띄도록 설계되어 있다(예: 승격 메서드를 하나로 좁힌 것). 수정은 한 글자다. 스위치의 `OK` 를 `COMPLETED` 로 옮기면 두 자리의 답이 같아지고, 가드가 사라져도 결과가 바뀌지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 허용 태그 8개 중 둘은 값이 자유 문자열이고, 그중 하나는 bounded 열거형이 이미 존재한다
|
||
- slug: `grpc-observability-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-observability.md#L238`
|
||
- owning-module: `grpc-observability` · priority: `P3`
|
||
- classification: 값 검사는 키가 allowlist 를 통과한 뒤 `UNBOUNDED_VALUE` 세 형태만 본다. 그런데 태그 값의 출처는 균일하지 않다. `GrpcStreamObservation` 의 검증은 `terminationReason` 이 널이 아니고 공백이 아닌지만 본다. 호출자가 예외 메시지나 원격 상태 문자열을 그대로 넣으면 그 태그의 값 공간이 트래픽과 함께 자란다 — 이 클래스가 존재하는 이유로 든 바로 그 실패다. 그리고 그 개념의 bounded 열거형이 이미 저장소에 있다 — `grpc-policy` 의 `GrpcStreamTerminationReason`. 쓰지 않은 이유는 의존 방향으로 설명된다. 이 리프의 `allowed_dependencies` 는 `["grpc-core-api"]` 뿐이고 그 열거형은 `grpc-policy` 에 있다. 그래서 수정은 열거형을 `grpc-core-api` 로 옮기거나, `violations` 가 두 자유 문자열 태그에 대해 허용값 집합을 받도록 서명을 넓히는 것이다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 반사 모드를 명시하면 서비스·역할 허용 목록이 조용히 하드코딩으로 바뀐다
|
||
- slug: `grpc-spring-boot-starter-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-spring-boot-starter.md#L249`
|
||
- owning-module: `grpc-spring-boot-starter` · priority: `P3`
|
||
- classification: 두 갈래가 만드는 것이 같은 종류의 값이 아니다. 설정하지 않으면 `defaultFor(environment)` — 환경이 서비스 목록과 역할 목록을 함께 결정한다. 설정하면 모드만 운영자 것이고, **허용 서비스와 허용 역할은 이 자동 설정에 박힌 리터럴이 된다.** 운영자가 조정한다고 생각하는 것은 노출 수위 하나인데, 실제로는 노출 대상 집합까지 바뀐다. 그리고 그 두 리터럴은 설정 표면에 노출되어 있지 않으므로 되돌릴 방법이 `reflection-mode` 를 다시 비우는 것뿐이다. `ca-skeleton.grpc.platform` 은 `ignoreUnknownFields = false` 를 걸어 "오타가 조용히 기본값으로 남지 않게" 한 설정 표면이다. 같은 규율로 보면, 값을 하나 설정했을 때 설정하지 않은 두 값이 함께 바뀌는 것도 같은 종류의 침묵이다. 허용 서비스·역할을 `GrpcPlatformProperties` 에 올리거나, 명시 모드에서도 `defaultFor(environment)` 가 만든 정책의 모드만 바꾼 사본을 쓴다. 후자가 이 저장소의 다른 곳에서 쓰는 형태다(`GrpcProtoStyleManifest.allowingWellKnownTypes` 처럼 넓힌 사본).
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — 예외 승격이 에러 코드 문자열 접미사에 의존한다
|
||
- slug: `messaging-claim-check-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L525`
|
||
- owning-module: `messaging-claim-check` · priority: `P3`
|
||
- classification: `ClaimCheckResolver.verify`가 `validation.failure().code().endsWith("_MISMATCH")`로 `ClaimCheckIntegrityException` 승격을 결정한다. `ClaimCheckIntegrityGuard`의 세 코드 중 둘이 그 접미사를 갖는다. 두 클래스 사이의 계약이 **문자열 명명 규약**이고 어디에도 선언되지 않았다. guard가 코드를 바꾸면(예: `CLAIM_CHECK_DIGEST_INVALID`) 승격이 조용히 멈추고 poison message가 `PERMANENT_BUSINESS`로 분류된다 — 재시도 정책이 달라진다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `MessagingRedactor`가 상수 대신 문자열 리터럴을 쓴다
|
||
- slug: `messaging-core-api-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L871`
|
||
- owning-module: `messaging-core-api` · priority: `P3`
|
||
- classification: `messaging-observability/.../MessagingRedactor.java:24`가 `"msg.id"`를 리터럴로 갖는다. `ReservedHeaders.MESSAGE_ID` 상수가 있다. 상수가 바뀌면 redaction이 조용히 대상을 잃는다. 컴파일러가 잡지 않는다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 구성 오류는 한 예외 타입과 안정 코드로 보고한다
|
||
- slug: `messaging-kafka-share-experimental-f04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L494`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- rule: 구성 오류는 한 예외 타입과 안정 코드로 보고한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 두 거절이 다른 예외 계층을 쓴다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 구세대 전이 메서드가 신세대와 다른 행 상태를 남긴다
|
||
- slug: `messaging-outbox-jdbc-postgresql-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L917`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql` · priority: `P3`
|
||
- classification: `markPublished(MessageId, Instant)` 는 `lease_owner` 와 `next_attempt_at` 을 지우지 않는다. `markPublished(OutboxLease, Instant)` 는 지운다. `markAmbiguous`/`markFailed` 도 같다. 두 컬럼은 청구 술어와 부분 인덱스가 읽는 값이다. 이 저장소에 구세대를 부르는 프로덕션 코드는 없다. 그러나 포트에 남아 있고 `@Deprecated` 도 아니므로, 외부 구현이나 향후 코드가 부를 수 있다. 최소한 `@Deprecated` 와 "신세대를 쓰라"는 문장이 필요하고, 더 나은 것은 제거다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### CASE — `maxBatches` 가 하드코딩이고 현재는 의미가 없다
|
||
- slug: `messaging-outbox-jdbc-postgresql-f08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L935`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql` · priority: `P3`
|
||
- classification: starter 가 `20` 을 박아 넣는다(`:141`, `:170`). P1 을 고치기 전에는 이 값이 아무 일도 하지 않고, 고친 뒤에는 배치 크기와 함께 조정 대상이 된다. `OutboxProperties`/`InboxRetentionPolicy` 로 옮기는 것이 맞다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
### REFERENCE — 부팅 경로의 알고리즘 복잡도는 문서화한다
|
||
- slug: `messaging-policy-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L817`
|
||
- owning-module: `messaging-policy`
|
||
- rule: 부팅 경로의 알고리즘 복잡도는 문서화한다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — 사이클 검사가 경로마다 집합을 복사한다
|
||
- scope: 정책과 조립 경계. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### REFERENCE — `CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다
|
||
- slug: `messaging-runtime-core-f05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L745`
|
||
- owning-module: `messaging-runtime-core`
|
||
- rule: `CompletionStage`를 반환하는 메서드는 동기적으로 던지지 않는다
|
||
- purpose: 이 규칙이 지켜지지 않으면 무엇이 일어나는지가 근거 사건에 있다 — admission 실패만 예외로 전파된다
|
||
- scope: 메시징 어댑터의 소비·발행 경로. 같은 형태의 계약·검증·의존 선언을 다루는 자리에 적용한다.
|
||
- exceptions: SSOT 가 이 규칙의 예외를 적지 않았다. 반례가 관측되면 여기에 적는다 — 지금 상태는 "예외 미관측"이지 "예외 없음"이 아니다.
|
||
- grounds: 근거 사건은 위 `source` 의 finding 이 소유한다. 규칙과 사건을 한 기록에 섞지 않는다.
|
||
|
||
|
||
### CASE — 죽은 매개변수 하나가 유일한 비기본값에서 NPE 를 낳는다
|
||
- slug: `messaging-spring-boot-starter-f03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L319`
|
||
- owning-module: `messaging-spring-boot-starter` · priority: `P3`
|
||
- classification: 호출처가 셋이고 전부 `() -> true` 다. 그래서 이 매개변수는 값을 하나만 갖는다. 그리고 그것이 죽어 있다는 것보다 나쁜 성질이 있다 — 이 매개변수가 존재하는 이유("이 역할은 선택적이다")대로 `() -> false` 를 넘기면 `credential == null` 인 경로가 가드를 지나 다음 줄의 `credential.type()` 에서 NPE 로 죽는다. 즉 이 매개변수의 유일한 비기본값이 의도한 동작이 아니라 널 역참조다. 수정은 매개변수를 지우고 널 검사를 무조건으로 만드는 것이다. 선택적 역할이 필요해지는 날에는 `Optional` 을 돌려주는 별도 메서드가 그 자리다 — `admin` 이 이미 호출처에서 그렇게 다뤄진다.
|
||
- missing-verification: 이 노드는 canonical SSOT 의 판정을 그대로 옮긴 것이다. 그 SSOT 의 §16 이 해당 finding 에 대해 실행하지 못한 검증을 소유한다.
|
||
|
||
---
|
||
## TOPIC 32 — cross-leaf-integration-facts
|
||
|
||
> 리프 SSOT 가 원칙적으로 볼 수 없는 사실 — 두 리프를 겹치거나 레지스트리와 대조해야만 성립한다.
|
||
> integration/family 문서(`analysis/19`·`analysis/20`)가 독립 candidate source 인 경우가 여기다.
|
||
|
||
### CASE — messaging 마이그레이션 스트림을 적용하는 곳이 없고, 적용하려는 순간 버전이 충돌한다
|
||
- slug: `messaging-migration-stream-has-no-applier-and-collides-on-adoption`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L860`
|
||
- owning-module: `integration/19-messaging-platform` · priority: `P2`
|
||
- classification: 리프 하나만 읽어서는 보이지 않는 사실이다. 두 messaging 리프가 각자 마이그레이션을 갖는데 그 스트림을 적용하는 Flyway location 이 어떤 컴포지션에도 없다. 그리고 적용하려는 순간 두 리프가 같은 디렉터리에서 같은 버전 번호를 만들어 둔 것이 드러난다 — 즉 배선 부재가 번호 충돌을 가려 왔다. 두 사실이 한 사건인 이유는 순서다. 적용이 시작되는 날 첫 실패가 충돌이고, 그때까지는 어느 쪽도 관측되지 않는다.
|
||
- missing-verification: 가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.
|
||
- relations: `case:messaging-migrations-collide-at-v2`
|
||
|
||
### CASE — claim-check는 starter에 배선 코드가 한 줄도 없다
|
||
- slug: `claim-check-has-no-wiring-line-in-the-starter`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L926`
|
||
- owning-module: `integration/19-messaging-platform` · priority: `P3`
|
||
- classification: claim-check 리프는 `runtime_memberships` 가 출하 컴포지션이고 starter 의 허용 의존에도 들어 있다. 그런데 starter 에 저장소 구현도, 빈도, 오프로드를 부르는 발행 경로도 없다. 리프 SSOT 는 자기 안에서 '소비자 0' 까지만 말할 수 있고, '배포에는 실려 있다' 는 레지스트리와 starter 를 함께 읽어야 나온다.
|
||
- missing-verification: 가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.
|
||
|
||
### CASE — admin 스위치가 가드를 켜고 서비스는 켜지 않는다
|
||
- slug: `the-admin-switch-turns-on-the-guard-and-not-the-service`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L975`
|
||
- owning-module: `integration/19-messaging-platform` · priority: `P2`
|
||
- classification: 운영자가 admin 기능을 켜는 프로퍼티 하나가 파괴적 작업 가드를 활성화한다. 같은 스위치가 그 가드를 실제로 부르는 admin 서비스 빈은 만들지 않는다. 결과적으로 스위치는 '켜졌다' 는 상태를 만들고 그 상태를 소비하는 경로가 없다. 두 리프의 SSOT 를 겹쳐야만 보이는 형태다 — 한쪽은 가드의 조건을, 다른 쪽은 서비스 빈의 부재를 소유한다.
|
||
- missing-verification: 가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.
|
||
|
||
### CASE — `messaging-admin-api`는 main 25파일 · 1,613 LOC에 테스트 파일이 1개다
|
||
- slug: `twenty-five-main-files-and-one-test-file`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L997`
|
||
- owning-module: `integration/19-messaging-platform` · priority: `P3`
|
||
- classification: 가족 관점에서만 나오는 비율 관측이다. 이 리프의 운영자 표면 전체가 테스트 한 파일에 기대고 있고, 그 파일이 겨냥하지 않는 타입들이 §17 에서 각각 미검증으로 잡힌다. 리프 SSOT 는 개별 타입의 미검증을 말하고, 이 관측은 그것들이 한 원인에서 나온다는 것을 말한다.
|
||
- missing-verification: 가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.
|
||
|
||
### CASE — `MessagingPublicSurfaceContractTest`가 가족 밖(app-bootstrap)에 있다
|
||
- slug: `the-public-surface-contract-test-lives-outside-the-family`
|
||
- readiness: `READY`
|
||
- source: `analysis/19-messaging-platform.md#L1114`
|
||
- owning-module: `integration/19-messaging-platform` · priority: `P3`
|
||
- classification: messaging 가족의 공개 표면을 붙드는 계약 테스트가 그 가족이 아니라 컴포지션 리프에 있다. 그래서 messaging 리프만 빌드하는 경로에서는 그 계약이 돌지 않고, 가족 안의 어느 SSOT 도 자기 표면이 어디서 검증되는지 알 수 없다. 테스트의 소재가 곧 그 테스트가 도는 조건이라는 점이 이 관측의 요지다.
|
||
- missing-verification: 가족 문서의 판정을 옮긴 것이다. 실행 확인은 그 문서의 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
## TOPIC 33 — identity-and-value-contracts
|
||
|
||
> 값과 신원의 계약 — 무엇이 유효한 값이고 그 규칙을 누가 소유하는가.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 두 개의 계약 강제 형태
|
||
- slug: `adapter-inbound-web-c19`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1555`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: web 쪽이 구조적으로 우월하다. `build.gradle`이 그 이유를 적는다 — "a parity check that compares whatever happens to be present would report agreement across a matrix with a hole in it."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — "설계상 부재" 주장 6건이 구현·정책 계층까지 일치한다
|
||
- slug: `adapter-outbound-cache-redis-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L235`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: 주장이 API 문서에만 있는지 확인했다(`160-...` §8.4). `sdk/api` 전체에서 `SETNX`·`SETEX`·`PSETEX`·`ZREVRANGE`·`RPOPLPUSH`·`BRPOPLPUSH`·`GEORADIUS`가 등장하는 곳은 **"없다"고 적는 javadoc 네 줄뿐**이다. Lettuce 구현 계층에서 걸린 둘은 무해하다 — `HashOperationRequests:128`의 `HSETNX`(다른 명령이다), `WritePresence:6`의 주석("This is what replaces `SETNX` and `SETEX`"). 명령 정책 SSOT(`redis-command-policy.yml`, 1,406줄)에서 `KEYS`는 **`risk: R4`, `support: BLOCKED`**이고, 파일 머리의 표에 따르면 `BLOCKED`의 access는 `NONE`이다. 같은 자리에 `RANDOMKEY`(R2 BLOCKED)·`DUMP`·`RESTORE`·`MIGRATE`·`SELECT`·`SWAPDB`도 BLOCKED다. 즉 raw gateway로도 `KEYS`에 닿을 수 없다. 정책 파일 자체의 구조는 sub-scope 05에서 다룬다 — 머리 주석이 "Official server metadata … decides what a command *is*. This file decides what this SDK is willing to *do* with it. **The catalog drift gate compares the two and fails the build when the server grows a command this file has not judged.**"라고 적는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — SSRF 가드가 도달하는 호출처 전수
|
||
- slug: `adapter-outbound-notification-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L703`
|
||
- owning-module: `adapter-outbound-notification`
|
||
- classification: `NotificationEndpoints.requireExternallyRoutable`의 프로덕션 호출처는 **둘**뿐이다: 가드 자신의 javadoc이 지목하는 대상은 다른 둘이다: Web Push는 목록에 없다. §25.1.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — 공유 계약을 실제로 상속하는 어댑터
|
||
- slug: `adapter-outbound-notification-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L913`
|
||
- owning-module: `adapter-outbound-notification`
|
||
- classification: **8종 중 3종.** 클래스 javadoc은 "Subclasses supply an adapter and a fault harness; the assertions are here **so that a new provider cannot be added without answering the same three questions**"라고 쓰지만, 상속을 강제하는 장치는 없다. 이 저장소는 같은 종류의 강제를 다른 곳에서는 만들어 두었다 — `EndpointGuardCallSiteTest`(가드가 호출처에서 실제로 도달하는가), `verifyNotificationApiSurface`(공개 타입 586개 스냅샷 고정). 여기에는 없다. `ContractAdapters`가 크로스-프로바이더 스위트에 등록하는 것은 **5종**(ses · twilio · apns · webpush · webhook)이다. 빠진 둘은 fcm과 smtp이고, 그것은 harness의 구조적 한계로 설명된다 — 스위트는 HTTP 루프백 서버 위에서 돌고, SMTP는 JavaMail 릴레이로, FCM은 `FcmGateway` 심으로 나간다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — local-dev provider의 경로 방어와 publication
|
||
- slug: `adapter-outbound-objectstorage-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L601`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: `LocalObjectPathGuard`는 이 저장소에서 반복해 본 강한 형태다 — root 정규화 + `startsWith` 봉쇄 + root 자신 거부에 더해, 부모 경로를 **root부터 한 세그먼트씩 내려가며** 심링크와 비디렉터리를 거부하고(`createParentsWithoutLinks` / `rejectExistingLinks`), 대상 자신도 심링크면 거부한다. control key는 `control/v1/` 접두사 + `[a-z0-9._/-]+` + `//`·`/./`·`/../` 금지 + 세그먼트별 재검사다. 그리고 control 레코드는 물리 파일명에 `.record`를 붙인다 — 객체 저장소가 허용하는 `reference`와 `reference/lifecycle` 쌍이 파일시스템에서 파일/디렉터리 충돌을 일으키지 않도록. 논리 키는 그대로 유지된다. 발행은 **배타적 하드링크**다. `LocalDevObjectDataStore.create`가 임시 파일에 쓰고 `channel.force(true)` 후 `Files.createLink(target, temporary)`를 하며, `FileAlreadyExistsException`을 `CONFLICT`로, `UnsupportedOperationException`을 "local filesystem cannot prove immutable create"로 번역한다 — 하드링크를 지원하지 않는 파일시스템에서 조용히 약한 방식으로 내려가지 않는다. 앞선 `Files.exists(NOFOLLOW)` 검사는 빠른 경로일 뿐이고 배타성은 `createLink`가 준다. POSIX면 소유자 읽기 전용 권한을 씌운다. test가 이것들을 이름으로 잡는다 — `traversalAbsoluteUnicodePercentAndSymlinkEscapesAreRejected`, `exclusiveCreateRaceHasOneWinner`, `injectedDiskFailureLeavesNoFinalOrTemporaryData`, `restartInspectsCommittedDataWithoutReplayingProducer`, `corruptControlRecordRemainsPresentAn…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1)·(8.2) 도달성과 게이트
|
||
- slug: `app-bootstrap-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/18-app-bootstrap.md#L386`
|
||
- owning-module: `app-bootstrap`
|
||
- classification: 7개 main 파일 전부 `@Configuration`이고 `@ConditionalOnProperty`/`@ConditionalOnBean`으로 게이트된다. `MongoPlatformHealthConfig`가 `@ConditionalOnBean` + `@ConditionalOnMissingBean` + `@ConditionalOnProperty` 셋을 함께 쓰는데, 자동설정 안에서의 `@ConditionalOnBean`은 Boot가 평가 순서를 통제하므로 모듈 14 §7.1이 경고한 컴포넌트 스캔 상의 위험이 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Stable 모듈 목록과 불변식
|
||
- slug: `grpc-core-api-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-core-api.md#L105`
|
||
- owning-module: `grpc-core-api`
|
||
- classification: `GrpcStableModuleCatalog` 이 Stable 12 와 Advanced 6 을 상수로 든다. `GrpcStableBuildInvariant.advancedDependencyAllowed()` 가 인자를 받지 않는 이유가 적혀 있다. 그리고 누출을 던지지 않고 집합으로 돌려주는 이유도 적혀 있다 — 첫 하나만 보고하는 게이트는 넷을 지우는 일을 네 번의 대화로 만든다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-testkit-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L169`
|
||
- owning-module: `messaging-testkit`
|
||
- classification: 7개 테스트와 각각이 못 박는 것: 계약을 `abstract class` + `@Test` 로 만든 결정의 효과는 `InMemoryHarnessContractTest` 의 javadoc 에 있다. 즉 어댑터가 `@Test` 를 **삭제하는 방법이 없다**. 상속받는 순간 7개가 전부 실행된다. 어댑터 쪽에서 하나를 빼려면 이 파일을 고쳐야 하고, 그것은 리뷰에 보인다. 그리고 그 7개가 침묵으로 줄어드는 것을 막는 자물쇠가 하나 더 있다. 리플렉션으로 `@Test` 가 붙은 메서드 이름 집합을 상수와 정확히 대조한다. 계약에서 테스트 하나를 지우면 이 테스트가 깨진다. 추가해도 깨진다. 계약의 크기 자체가 잠겨 있다. 5개 시나리오, 그리고 각각이 `rationale` 을 **비어 있으면 생성 자체가 실패하도록** 강제한다. `REJECTED` 가 `BEFORE_TRANSMISSION` 하나뿐이라는 사실이 테스트로 잠겨 있다(`CrossBrokerContractSuite.aFailureBeforeTransmissionIsTheOnlyOneReportedAsRejected`). `byName` 은 알 수 없는 이름을 건너뛰지 않고 거절한다. 이 리프에서 가장 밀도 높은 설계다. 두 javadoc 이 **자기가 고친 결함을 이름 붙여** 남겼다. 네 가지 결정이 한 문단에 압축되어 있고, 넷 다 코드에서 확인된다. **(a) 커밋한다.** `src/main/resources/messaging/broker-certification-evidence.jsonl`. `build/` 를 읽으면 깨끗한 체크아웃에서 답이 달라진다는 이유가 명시되어 있다. 확인: `src` 와 `build/resources` 사본이 diff 로 동일(`EVD-300`). **(b) 손으로 못 쓰게 하는 게이트.** `messaging-kafka/build.gradle:82` 의 `verifyMessagingCertificationEvidence`. `gitCommit` 과 `observedAt` 을 정규식으로 지우고 나머지 집합을 비교한다. 그 둘은 매 실행마다 달라지므로 비교 대상이 아니라는 주석이 붙어 있다. `outputs.upToDateWhen { false }…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 34 — result-and-failure-algebra
|
||
|
||
> 결과와 실패의 대수 — 어떤 결과 값이 가능하고 무엇이 금지되는가.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 인증과 예외 처리의 인터셉터 순서
|
||
- slug: `adapter-inbound-grpc-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/15-adapter-inbound-grpc.md#L140`
|
||
- owning-module: `adapter-inbound-grpc`
|
||
- classification: `ServerInterceptors.intercept(service, exceptionInterceptor, authenticationInterceptor)` — gRPC 규약상 **마지막 인터셉터의 `interceptCall`이 먼저** 호출되므로 인증이 바깥, 예외 처리가 안쪽이다. 인증 인터셉터가 예외 처리 바깥에 있는데도 안전한 이유는 그것이 스스로 예외를 삼키기 때문이다: CLAUDE.md의 약속("정책이 `false`를 반환하거나 예외를 던진 요청은 ... 안정적인 `UNAUTHENTICATED` status/code/category로 종료된다")이 코드와 일치하고, 두 경우 모두 같은 `call.close(Status.UNAUTHENTICATED.withDescription(OperationalError.UNAUTHENTICATED.code()), trailersFor(...))`로 끝난다. 정책 진단은 클라이언트에 닿지 않는다. 중복 아님.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — guard를 지나지 않는 경로가 하나 있고, 그것이 선언돼 있다
|
||
- slug: `adapter-outbound-cache-redis-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L477`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `PubSubOperationRequests`의 javadoc이 예외를 스스로 밝힌다. 대체 검사가 실제로 있다. `channelTargets`·`shardTargets`·`patternTargets` 셋 다 빈 컬렉션을 거부하고 **모든 대상의 네임스페이스가 이 프로세스의 것과 같은지** 확인한다(`requireNamespace`, 다르면 "channel belongs to a namespace this process may not use"). `patternTargets`는 추가로 `context.sdkPermit(PATTERN_SUBSCRIBE)`를 호출한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Success / failure mechanics
|
||
- slug: `domain-core-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/01-domain-core.md#L158`
|
||
- owning-module: `domain-core`
|
||
- classification: 이 module의 runtime executable behavior는 매우 작다. annotation 자체에는 success/failure path가 없고, interface도 implementation을 가지지 않는다. 주요 failure mechanics는 **build-time architecture violation**이다. forbidden framework/domain dependency → `DOMAIN_IS_PURE` domain logger dependency → `DOMAIN_HAS_NO_LOGGER` public no-arg value object → `VALUE_OBJECTS_HAVE_NO_PUBLIC_NO_ARG_CONSTRUCTOR` public `set*` aggregate mutator → `AGGREGATE_ROOT_SETTERS_ARE_NOT_PUBLIC` non-record domain event → `DOMAIN_EVENTS_ARE_RECORDS` enumerated transport dependency → `DOMAIN_EVENTS_ARE_TRANSPORT_FREE` `id` field raw type not assignable to `ResourceId` → `NO_LONG_ID_PK` project dependency not in registry → `verifyCleanArchitectureDependencies` production -> `sample-portfolio` edge → settings registry validation and root dependency verification, plus cross-module ArchUnit rule
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-cloudevents-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L451`
|
||
- owning-module: `messaging-cloudevents`
|
||
- classification: build.gradle 주석이 이전 결함 하나를 보존한다. 이것이 `src/messaging/CLAUDE.md:40-43`의 게이트를 만든 사례군에 속한다 — "source에서 public/protected 시그니처에 등장하는 vendor 라이브러리를 뽑아 그 leaf의 `build.gradle`이 `api`로 선언했는지 대조". 이 leaf는 그 게이트를 두 좌표로 나눠 통과한 모범 사례다. 코드 주석이 남긴 두 매핑 결정(§4.2)도 실패 이력의 성격을 갖는다 — "defaulted to the production instant"와 "inventing a tombstone"은 하지 않기로 한 것들이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-inbox-jdbc-postgresql-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L581`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- classification: **한 결함이 두 파일에 기록돼 있고, 그중 하나가 그것을 막는 테스트다.** 그리고 그 테스트가 "hermetic: the refusal has to happen before any connection is requested, and the data source below fails the test by being asked for one"이라고 자기 설계를 적는다 — **DataSource가 요청받으면 테스트가 실패하도록** 만들어 검사 순서까지 고정한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 분류 — 타입 있는 신호만 본다
|
||
- slug: `messaging-pulsar-experimental-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-pulsar-experimental.md#L50`
|
||
- owning-module: `messaging-pulsar-experimental`
|
||
- classification: javadoc 이 이전 구현과 그 결함을 적는다. 기본값이 모호로 바뀐 것이 핵심이다. 알 수 없는 실패에서 안전한 방향은 모호다. 확인된 성공은 복제 증거로 기록된다 — 전송 미래가 설정된 수의 저장 노드에 기록된 뒤에야 해소되므로 영수증이 아니라 복제 증거다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-schema-api-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L432`
|
||
- owning-module: `messaging-schema-api`
|
||
- classification: 코드 주석이 보존한 이전 결함: 두 사례 다 형태가 같다 — **검사가 없었던 게 아니라 검사의 위치/키가 틀렸다.** `messaging-core-api` §13의 "문자 vs 바이트, 정확일치 vs 세그먼트" 목록과 같은 계열이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-testkit-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L492`
|
||
- owning-module: `messaging-testkit`
|
||
- classification: 이 리프의 실패 처리 원칙은 하나다: **모르는 것을 아는 척하지 않는다.** 마지막 두 줄의 구분이 의도적이다. `CompatibilityMatrix.of("messaging-artemis")` 는 던지고(`anUnknownAdapterIsNotSilentlyTreatedAsSupported`), `matrix.coverageOf("messaging-artemis", …)` 는 `NOT_COVERED` 를 돌려준다(`aFaultThatWasNeverRecordedReadsAsUncoveredRatherThanPassing`). 전자는 "지원 목록에 없는 것을 지원인 척"을 막고, 후자는 "기록 없음"이 곧 "커버 안 됨"이라는 자연스러운 읽기다. `DockerAvailability` 는 반대 방향의 실패 처리다. `Class.forName("org.testcontainers.DockerClientFactory")` 를 리플렉션으로 부르고 어떤 예외든 `false` 로 삼킨다(`:26-34`). 이 리프가 testcontainers 에 의존하지 않으면서 그 존재를 물어볼 수 있게 하는 유일한 방법이고, 결과를 `static final` 로 1회만 캐시한다. 주목할 점: **"skip 은 성공이 아니다"** 라는 반대 규칙이 인증 레인에는 적용되어 있다. 일반 컨테이너 스위트는 `DockerAvailability` 로 skip 하고, 인증 레인만 **가드 없이 실패**한다. 대신 `test` 태그에서 빼서 노트북 빌드를 깨지 않는다. 두 규칙이 충돌하지 않게 배치되어 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-transport-spi-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L579`
|
||
- owning-module: `messaging-transport-spi`
|
||
- classification: 코드 주석이 네 결함을 보존한다. 전부 **장기 실행에서만 드러나는** 종류다. 네 번째와 `LeakTrackingRuntime.closeCount()` javadoc("Closing twice is as much a defect as never closing")이 같은 주제를 반대편에서 말한다 — **해제는 정확히 한 번이어야 하고, 0번도 2번도 결함이다.**
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Activation and health snapshot
|
||
- slug: `shared-contract-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/02-shared-contract.md#L108`
|
||
- owning-module: `shared-contract`
|
||
- classification: `MasterSwitchParser`는 unset=false, true/false case-insensitive만 허용하며 `yes`, `1`, `on`, whitespace-padded value를 invalid로 처리한다. canonical+legacy가 동시에 있으면 값이 같아도 ambiguous로 실패하고 legacy-only는 replacement property를 반환한다. 이는 operator configuration을 permissive coercion하지 않는 fail-closed contract다. `RedisHealthSnapshotProvider`는 Redis client/connection/credential을 shared boundary로 새지 않게 role/capability/state/reason/semantic freshness만 projection한다. eviction policy는 runtime CONFIG 조회 증명이 아니라 configured expectation임을 enum 이름과 javadoc으로 명시한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 35 — state-machines-and-ownership
|
||
|
||
> 상태 기계와 소유권 — 전이·펜싱·세대·커밋 위치.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 두 능력 목록이 커서에 대해 다르게 답한다
|
||
- slug: `adapter-inbound-graphql-c12`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L867`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `GraphQlStableCapabilityManifest.STABLE`에 `SIGNED_CURSOR_CONNECTION`이 들어 있다. `CLAUDE.md` 등급표는 cursor 서명을 `modelled`(요청 경로에 없음)로 매긴다. 두 목록의 용도가 다르다 — 매니페스트는 `requireStable(capability)`로 **릴리스 게이트가 소비하는 기계 판정**이고, 등급표는 사람이 읽는 공시다. 그러나 같은 능력에 대해 하나는 "Stable에서 지원"이라 하고 하나는 "요청 경로에 없음"이라 한다. §29.2.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — 릴리스 게이트 자체
|
||
- slug: `adapter-inbound-graphql-c13`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L873`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `release` 9개 파일 전부 autoconf=0이다. 릴리스 게이트는 런타임 컴포넌트가 아니라 빌드·릴리스 시점 도구이므로 정상이다. 다만 `GraphQlReleaseReportWriter`(57)는 main_other=0 · test=1로, 게이트 결과를 기록할 작성기에 호출자가 없다. CLAUDE.md가 그 상태를 명시한다 — "`graphqlPerformanceTest` 레인이 자리를 예약, **증거 없으면 릴리스 게이트가 거부**". 즉 게이트는 CI 레인에서 호출되도록 설계됐다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 캐시 정책이 두 벌이다
|
||
- slug: `adapter-inbound-web-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L864`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `@Component`이므로 컴포넌트 스캔이 잡고 Spring이 `Filter` 빈을 체인에 넣는다. **모든 응답에 `no-store`를 붙인다.** 배선되지 않은 것: `cache` 패키지 4개 파일 310 LOC. `web.cache.` 패키지를 참조하는 파일이 자기 패키지 밖에 **0개**다. `SecurityConfig:80`이 Spring Security의 기본 캐시 헤더 작성기를 끄면서 그 이유를 적는다 — "`CacheControlFilter` **owns the cache header policy**". 소유자는 24줄짜리 상수 두 개이고, 프로파일·지시자·`Vary` 규칙을 갖춘 310줄은 소유하지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.4) 카운트 드리프트 — 선언된 능력 11개, 활성화 게이트 2개
|
||
- slug: `adapter-inbound-web-c16`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1217`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `WebAdvancedFeature`의 상수: `advanced/**` 전체에서 프로덕션 `@Configuration`은 셋이고(`MvcStreamingExecutorConfiguration` · `VirtualThreadMvcConfiguration` · `VirtualThreadSettings`) 실제 `@ConditionalOnProperty` 접두사는 둘이다: 나머지 아홉(`WEBFLUX_BLOCKING_BRIDGE` · `JSON_MERGE_PATCH` · `JSON_PATCH` · `SSE` · `JSON_SEQUENCE` · `FUNCTIONAL_WEBFLUX` · `CBOR` · `XML` · `RATELIMIT_DRAFT_HEADERS`)에는 프로퍼티도, `@Configuration`도, 빈도 없다. §36.1.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 승격 게이트
|
||
- slug: `adapter-inbound-websocket-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/17-adapter-inbound-websocket.md#L420`
|
||
- owning-module: `adapter-inbound-websocket`
|
||
- classification: `advanced/release/AdvancedPromotionGate`(120)와 `release/WebSocketStableReleaseGate`(106, §11)가 각각 Advanced 승격과 Stable 릴리스를 판정한다. 둘 다 main 참조 0이고 테스트만 있다 — 릴리스 시점 도구이므로 런타임 미배선이 정상이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 두 프로그래밍 모델이 같은 request builder를 공유한다
|
||
- slug: `adapter-outbound-cache-redis-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L456`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `ValueOperationRequests`의 javadoc이 불변식을 적는다 — "Both the blocking and the reactive string operations call exactly these methods, so **a change to a permit, a budget, an encoding, or a command choice cannot apply to one API and not the other.**" 구조가 그것을 보장한다. `LettuceRedisValueOperations`와 `LettuceReactiveRedisValueOperations`는 둘 다 생성자에서 `new ValueOperationRequests(gateway, context, counters)`를 만들고, 차이는 `SyncRedisCommandExecutor` vs `ReactiveRedisCommandExecutor` 하나뿐이다. 각 메서드는 `executor.execute(requests.xxx(...))` 한 줄이고, reactive 쪽은 그 위에 `flatMap`/`then` 같은 형태 변환만 얹는다. 11개 계열 전부에서 확인했다(`162-...` §8.2) — Value·Hash·List·Set·SortedSet·Key·Geo·Bitmap·Stream·HyperLogLog·PubSub 모두 sync와 reactive 양쪽이 같은 `*OperationRequests`를 생성한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `CommandPolicyGuard` — 순서가 고정된 단일 입장 지점
|
||
- slug: `adapter-outbound-cache-redis-c10`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L560`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: javadoc이 순서와 그 이유를 적는다 — "Validation order is fixed and **each step is cheaper than the one after it**, so an obviously inadmissible command is refused before anything is encoded or sent." 각 단계가 구체적이다. `requireReachable`은 `BLOCKED`거나 `access == NONE`이면 거부하고 R3/R4를 애플리케이션 경로에서 배제한다. `requireCapability`는 명령의 최소 버전을 프로브된 서버 버전과 대조한다. `requireNamespace`는 모든 키의 네임스페이스를 확인하고 렌더까지 수행한다. `requireSameSlot`은 Cluster에서 두 개 이상 슬롯이면 `RedisCrossSlotException`을 **서버를 부르기 전에** 던진다. `effectiveTimeout`은 블로킹 명령이 유한한 server block을 선언하지 않으면 거부하고, 설정 상한을 넘으면 거부하며, 통과하면 `BLOCKING_MARGIN`(2초)을 더한다. 이 클래스에는 두 개의 수정 이력이 주석으로 남아 있고, 둘 다 이 저장소에서 반복해 본 종류다. **(a) 죽은 중복 mechanism을 지운 기록.** `validateReply(...)`가 있었고 아무도 부르지 않았다. **(b) 절대 발화하지 못하던 조건.** 다중 키 permit 검사가 advanced permit 검사와 한 조건으로 접혀 있었고, "둘 다 없음"이 위에서 이미 던지므로 다중 키 절은 도달 불가였다 — "set algebra over any number of keys was admitted on an advanced permit alone." 지금은 `request.keys().size() > 1 && request.multiKeyPermit().isEmpty()`가 독립 조건이다. test `rejectsAMultiKeyCommandCarryingOnlyAnAdvancedPermit`과 `aSingleKeyAdvancedCommandStillNeedsNoMultiKeyPermit`가 양쪽…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 상태 전이가 인접 행렬이고 terminal이 진짜 terminal이다
|
||
- slug: `adapter-outbound-fileserver-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L184`
|
||
- owning-module: `adapter-outbound-fileserver`
|
||
- classification: `validateOperationTransition`이 여섯 가지를 순서대로 강제한다: requestFingerprint 불변 → 불변 identity 10개 필드 불변 → `stateRevision` 감소 금지 → 동일 revision 다른 내용 금지 → **정확히 +1** 증가 → 인접 전이 행렬. 행렬은 README가 적은 사슬과 일치하고, 모든 비terminal 상태에서 `QUARANTINED`로만 이탈할 수 있으며 `PUBLISHED`·`QUARANTINED`는 후속 전이가 없다(`case PUBLISHED, QUARANTINED -> false`). 봉인 이후 사실은 얼어붙는다 — `requireSealedFactsUnchanged`가 byteSize·rowCount·columnCount·sha256·formulaMitigatedCount·sealedAt을 고정하고, `MANIFEST_PUBLISHED` 이후에는 `manifestDigest`, `REFERENCE_PUBLISHED` 이후에는 `referenceDigest`도 고정된다. `current.equals(candidate)`는 전이가 아니라 **복구(repair)**로 취급된다 — 부모 디렉터리를 다시 force하고 정확 read-back만 수행한다. 크래시 후 같은 레코드를 다시 쓰는 재시도가 conflict가 되지 않게 하는 처리이고, `parentForcedCallbackFailureIsRepairedByIdenticalOperationRetry`가 이를 고정한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 두 개의 락 형태가 각자의 쓰기 원시연산에 맞춰져 있다
|
||
- slug: `adapter-outbound-fileserver-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L192`
|
||
- owning-module: `adapter-outbound-fileserver`
|
||
- classification: 한 클래스 안에 락이 두 종류다. 얼핏 비대칭으로 보이지만 각자의 커밋 방식이 다르다. 즉 배타성이 필요한 쪽(replace)에는 OS 락을 두고, 원시연산 자체가 배타적인 쪽(create-link)에는 JVM 스트라이프만 둔 것이다. 후자의 root 미포함은 **과잉 직렬화** 방향이라(다른 root의 같은 fileId가 같은 스트라이프를 공유) 배타 누락으로는 이어지지 않고, `fileId`는 `SecureRandom` 16바이트라 실질 충돌도 없다. 결함이 아니라 설계로 기록한다. collision 경로도 닫혀 있다 — `createLink`가 충돌하면 임시 파일을 정확히 지우고, 기존 레코드를 읽어 identity와 내용 동등성을 확인한 뒤 같으면 repair, 다르면 `CONFLICT`다. `concurrentCrossInstanceImmutableCollisionIsNeverClassifiedAsStorage`가 그 분류를 고정한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — payload 계층이 자신의 잔여 위험을 먼저 선언한다
|
||
- slug: `adapter-outbound-fileserver-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L558`
|
||
- owning-module: `adapter-outbound-fileserver`
|
||
- classification: `LocalPersistentPayloadOperations`의 클래스 javadoc이 무엇이 서술자 상대이고 무엇이 아닌지를 앞에서 밝힌다. 전수 검사가 그 서술과 일치한다(`147-...` §8.2). 이 파일의 `Files.*` 호출은 정확히 그 셋 — `Files.createLink`(:941), `Files.createDirectory`(:802), 그리고 force/stat/FileStore 조회 — 뿐이고, 각각 앞뒤로 `fileKey`·소유자·권한·FileStore 재확인이 붙는다. JDK가 `linkat`/`mkdirat`를 노출하지 않으므로 서술자 상대 대응물이 없고, 그 사실을 숨기는 대신 적었다. **이것이 §32와의 차이다.** 여기서는 잔여 경로 연산이 (a) 문서에 선언되고 (b) identity 검사로 감싸인다. `AtomicMoveContentPublisher`의 발행 rename은 (a) 어디에도 선언되지 않고 (b) 같은 모듈이 "a precheck could only ever approximate"라고 적은 사전검사 하나로만 보호된다. 같은 저장소가 같은 문제를 한 번은 정직하게, 한 번은 그렇지 않게 다룬 대비다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `ClientRuntimeRegistry` — 세대 교체가 틈으로 관측되지 않는다
|
||
- slug: `adapter-outbound-httpclient-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L112`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: "A swap publishes the replacement first and drains the predecessor afterwards, so a rotation is never observable as a gap." `acquire`는 관측한 세대가 예약 직전에 draining으로 넘어가면 **새로 발행된 세대에 대해 재시도**한다. 과거 누수 두 건이 코드와 주석에 남아 있다. 교체된 세대가 `runtimes`에서 빠지고 스케줄된 drain 작업만 소유하게 되어, 그 작업이 발화하기 전에 레지스트리가 닫히면 "leaked the whole generation — and the resource-bound suite could not see it, because nothing enumerated it." 지금은 `retired` 집합이 추적한다. `close()`가 `forEach`로 닫다가 첫 예외에서 멈춰 "a single misbehaving pool left every remaining connection, thread and socket open — **shutdown leaked more the worse the failure was.**" 지금은 전부 닫고 실패를 suppressed로 모은다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 재시도 결정표가 순서로 표현돼 있다
|
||
- slug: `adapter-outbound-httpclient-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L305`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: `DefaultRetryEligibilityEngine.decide`의 javadoc이 규칙이다 — "The order is the point. Cheap absolute blockers come first (attempts, budget, replayability, first byte, deadline, draining), then ambiguity, then status- and failure-specific rules. **A later rule can never re-enable something an earlier rule forbade.**" 절대 차단 여섯이 먼저다 — 시도 수 소진 · 예산 소진 · 본문 재생 불가 · **첫 바이트 전달됨** · 런타임 draining · 남은 deadline이 최소 시도 예산 이하. 그다음 영구 실패 범주, 그다음 증거, 그다음 상태/실패별. 상태별 규칙에 수정 이력이 붙어 있다. 그리고 `RetryContext.safelyIdempotent()`가 이 모듈의 D-09를 구현한다 — HTTP 메서드는 `RetryContext`에 **아예 없다**("so a POST with a registered idempotency key and a GET against a non-idempotent RPC endpoint are both handled correctly instead of by method-name folklore"). 키 기반 멱등성은 **키가 실제로 전송됐는지**까지 요구한다. test `aKeyThatWasNeverSentDoesNotMakeARepeatSafe`가 그것을 고정한다. `Retry-After`는 남은 deadline 안에 들어갈 때만 존중된다(`allowWithin`), 그리고 존중된 `Retry-After`는 `maxBackoff`로 잘리지 **않는다** — 잘라 버리면 업스트림이 요청한 대기보다 일찍 다시 두드리게 되기 때문이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약이 컴파일되어 닫힌다
|
||
- slug: `adapter-outbound-messaging-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/12-adapter-outbound-messaging.md#L237`
|
||
- owning-module: `adapter-outbound-messaging`
|
||
- classification: `ContractCatalogCompiler`가 **정확한 record 타입 토큰**으로부터 불변 카탈로그를 만들고, test 이름이 무엇을 거부하는지 전부 적는다 — 중복 stable/schema/payload 신원, 음수 버전, 잘못된 payload kind, null·공백·중복·반사 불일치 성분 순서, 서술자 누락, **payload 버전 사이의 logical destination 드리프트**. 특히 두 test가 이 계층의 성격을 보여 준다. `recursivelyFreezesOnlyTheClosedDeclaredGenericPayloadGraph` / `rejectsOpenRawWildcardMapJsonTreeInterfaceAndGenericRecordGraphs` — 열린 타입(raw·wildcard·`Map`·JSON 트리·인터페이스·제네릭 record 그래프)을 페이로드로 받지 않는다. 봉투 작성기가 shape을 따라 스냅샷할 수 있으려면 그래프가 닫혀 있어야 한다(§11). `snapshotsEveryContributionAccessorExactlyOnceIncludingAStatefulSchemaHash` / `statefulDescriptorCannotBypassCrossVersionLogicalDestinationDrift` — 기여 접근자를 **정확히 한 번만** 호출한다. 가변 서술자가 검사와 저장 사이에 값을 바꿔 규칙을 우회하는 경로를 닫는다. `compiledContractUsesOnlyAStaticPublicCompositionBridgeWithoutReflectionLeak` — 컴파일된 계약이 반사를 밖으로 새게 하지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 다섯 개의 닫힌 전이표가 있고 terminal이 진짜 terminal이다
|
||
- slug: `adapter-outbound-objectstorage-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L244`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: `ObjectOperationStateMachine`이 publication·scan·reference·direct-grant·multipart 다섯 계열의 전이를 각각 switch로 적는다. terminal 처리가 계열마다 명시적이다. 뒤 둘은 **branch 전이**(EXPIRED/ABORTED/FAILED/CORRUPT)를 별도로 허용해, 정상 사슬 어디서든 실패로 빠질 수 있되 terminal에서는 나올 수 없게 한다. `requireNextRevision(current, next)`가 `next == current + 1`을 강제한다 — revision은 건너뛰지도 되돌아가지도 못한다. `ObjectOperationStateMachineTest`가 그 셋을 이름으로 고정한다: `publicationFollowsScanFreeAndScanRequiredPaths`, `terminalOutOfOrderAndStaleRevisionTransitionsFailClosed`, `independentStateFamiliesDoNotImplyEachOther`.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 이 sub-scope의 설계 — 비밀은 durable하지 않고, 승인은 명시적으로 닫힌다
|
||
- slug: `adapter-outbound-objectstorage-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L420`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: durable record가 **비밀을 담지 않는다**는 것이 출발점이다. `DirectTransferSessionRecord`의 한 줄 javadoc이 그것이다 — "Durable non-secret direct-transfer session state; bearer material is deliberately absent." 저장되는 것은 generation·제약 다이제스트·서명 시각·만료·credential revision·reference revision뿐이고, presigned URI와 서명 헤더는 **process-local 캐시**에만 남는다. 프로세스가 재시작하면 이미 발급된 grant는 재현되지 않고 `"issued direct grant bearer material is unavailable after process restart"`로 명시적으로 실패한다 — 조용히 새로 서명해서 두 번째 bearer를 만드는 대신이다. 상태 전이도 CAS로 순서가 고정된다. `SESSION_RESERVED → GRANT_PREPARED → GRANT_ISSUED → DATA_UPLOADED`이고, test 이름이 그 순서를 그대로 못박는다 — `preparedCasPrecedesSigningAndIssuedCasPrecedesReturningTheBearerGrant`. 서명 **전에** prepared가 durable해야 하고, bearer를 **반환하기 전에** issued가 durable해야 한다. multipart 쪽에서 가장 흥미로운 것은 완료 시점의 **admission drain**이다. `DirectMultipartCompletionVerifier.requireAdmissionDrained`는 provider가 "controlled ingress가 비었다"고 권위 있게 말해 주지 않으면, `마지막 grant 만료 + 검증된 시계 오차 + 최대 in-flight 지평` 이 지나기 전에는 완료를 거부한다. 이미 발급된 part PUT이 아직 날아가고 있을 수 있기 때문이다. test 이름이 `completionHorizonRejectsWhileIssuedPartRequestsMayStillArrive`와 `lateGrant…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Sub-scope 02 — API contracts (`api/**`)
|
||
- slug: `adapter-outbound-persistence-jpa-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L70`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: public top-level production type도 정확히 49개다. `docs/architecture/jpa-api-surface.txt`의 committed API baseline 역시 `api` namespace에서 49개를 기록하고 있어 현재 이름 목록 drift는 없다. Gradle `verifyJpaApiSurface`가 이 surface의 추가/삭제를 fail-closed로 검증한다. 이 package에는 Spring/JPA/Repository/Entity/Configuration annotation이 하나도 없다. 즉 JPA adapter 안에 위치하지만 **API vocabulary 자체는 Spring bean discovery나 JPA mapping으로 활성화되지 않는다.** 실제 composition은 `app-bootstrap` 및 implementation package가 소유한다. `api/**`는 implementation package와 달리 의도적으로 외부 adopter surface다. committed API baseline 상단도 `api`를 intended external package로 명시한다. 따라서 다음 두 사실을 구분해야 한다. 1. repository 내부 production consumer가 있는가 2. public library contract로 존재할 이유가 있는가 예를 들어 `JpaEntityNotFoundException`은 현재 repository production에서 자신을 제외한 참조 파일이 0개다. 하지만 이 한 사실만으로 dead type이라고 판정하지 않았다. external API surface는 repository 내부에서 직접 생성되지 않더라도 adopter가 catch/translate하는 계약일 수 있기 때문이다. 반대로 public API라는 이유로 내부 invariant 결함까지 “미사용이라 안전”으로 넘기지는 않는다. `SignedJsonCursorCodec`처럼 codec 자체가 public contract이고 자기 encode/decode algebra가 불일치하면 repository 내부 consumer 유무와 무관하게 API defect…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Error API — provider exception을 stable failure algebra로 변환
|
||
- slug: `adapter-outbound-persistence-jpa-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L208`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: error hierarchy의 핵심은 “예외 class를 많이 만든 것”이 아니라 provider-specific signal을 bounded semantic category로 변환하는 것이다. 대표 category는: serialization failure optimistic conflict lock not available connection unavailable unique/FK/not-null/check constraint entity not found schema mismatch data corruption completion unknown 이 category는 뒤의 retry policy/metric이 SQLSTATE/provider message를 직접 해석하지 않게 하는 중간 vocabulary다. `JpaFailureContext`가 가지는 정보는 operation, SQLSTATE/constraint, attempt, retryability, completion-unknown, elapsed, trace 등으로 제한된다. 중요한 invariant는 다음이다. arbitrary identifier는 그대로 담지 않고 bounded/redacted form으로 축약 malformed SQLSTATE는 `redacted` absent SQLSTATE는 sentinel로 표현 completion unknown과 retryable=true를 동시에 표현할 수 없음 completion-unknown factory는 항상 automatic retry를 차단하는 형태를 만든다 즉 “exception이 발생한 뒤 로그에서 실수하지 말자”보다 앞선 위치에서 **failure context가 위험한 shape 자체를 표현하기 어렵게** 만든다. base exception message는 provider cause message를 그대로 복사하지 않고 category + bounded context로 만든다. dedicated test도 provider cause에 email marker를 넣었을 때 top-level exception message에 노출되지 않는 것을 검증한다. 동시에 raw `Throwable cause`는 보존한다.…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Transaction API — 실행체보다 먼저 retry 가능 상태를 제한한다
|
||
- slug: `adapter-outbound-persistence-jpa-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L462`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: retryProfile write profile은 positive timeout이 필수다. read-only는 zero timeout을 “connection default” 의미로 허용한다. 지원 propagation을 REQUIRED / MANDATORY / REQUIRES_NEW로 좁혀 SUPPORTS/NESTED/NOT_SUPPORTED/NEVER처럼 “실제로 transaction 안에 있는가”를 흐리는 mode를 surface에서 제거했다. isolation 역시 PostgreSQL에서 의미가 겹치는 READ_UNCOMMITTED를 expose하지 않는다. retryable category allowlist는 다음 contender 계열로 제한된다. serialization failure optimistic conflict lock not available connection unavailable `COMPLETION_UNKNOWN`을 넣으면 constructor가 즉시 거부한다. Unique constraint 같은 ineligible category도 거부한다. 즉 failure translator가 retryability를 판단하고, profile이 category allowlist를 가진다고 해서 “어떤 failure도 설정으로 retry 가능하게” 만들 수 없다. RETRY_FULL_TRANSACTION 세 가지이며 retry만 non-zero delay를 가질 수 있다. 이 분리 덕분에 completion unknown이 `delay=0 retry`처럼 표현되지 않는다. “모르겠음”을 “즉시 한 번 더”와 구분한다. `RetryDecision.reason`은 javadoc상 bounded diagnostic/low-cardinality-safe string으로 설명된다. 그러나 constructor는 non-null/nonblank만 확인하고 길이/형식 상한은 없다. runtime constructor probe에서는 100,000-character reason도 accepted됐다. 다만 actual `JpaRetryObservation`은 decision.reason을 metric tag로 사용하지 않는다. met…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — API surface verification
|
||
- slug: `adapter-outbound-persistence-jpa-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L639`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `verifyJpaApiSurface --rerun-tasks`가 통과했다. 이 task가 증명하는 것은 **public type names가 committed baseline과 동일하다**는 것이다. method semantics나 constructor invariant까지 ABI/API compatibility를 검증하는 것은 아니다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Sub-scope 03 — transaction + persistence failure
|
||
- slug: `adapter-outbound-persistence-jpa-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L721`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 모든 51개 source/test를 FULL_READ했다. 이 scope에서는 implementation class를 샘플링하지 않고 transaction state machine, retry budget, Spring mapping, failure translation, root wiring, consumer reachability까지 연결했다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 같은 leaf 안에 두 개의 transaction model이 존재한다
|
||
- slug: `adapter-outbound-persistence-jpa-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L737`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 현재 persistence-jpa에는 transaction을 표현하는 두 계열이 동시에 존재한다. input/output vocabulary: `TransactionRequest` `TransactionPolicyId` `CallBudget` `TransactionResult` `TransactionOutcome` `OperationId` `TransactionPhase` `ReconciliationReference` 이 모델은 application-core가 소유한다. use case가 outbound adapter type을 import하지 않아도 transaction policy와 uncertain outcome을 표현할 수 있다. input/output vocabulary: `PersistenceOperationName` `TransactionProfile` `RetryProfile` `JpaPersistenceException` `TransactionCompletionEvidence` `JpaPlatformRuntimeAutoConfiguration`은 `PlatformTransactionManager`가 있으면 `SpringJpaTransactionExecutor` bean을 만들고, 그 executor가 있으면 `FullTransactionRetryCoordinator` bean도 만든다. 따라서 source tree 수준에서는 B가 단순 historical class가 아니라 **현재 runtime bean graph에도 포함되는 구현**이다. 그러나 repository production call search에서는 `FullTransactionRetryCoordinator.execute(...)`를 실제 business/application code가 호출하는 경로가 확인되지 않았다. 반대로 application-core transaction port는 sample/use-case/composition에서 canonical contract로 사용된다. 이 공존 자체는 곧바로 defect가 아니다. `api/**`는 intended external surface이므로 fork/application이 B를 programma…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `SpringPolicyTransactionPort`: transaction result를 boolean 성공/실패보다 세밀하게 표현
|
||
- slug: `adapter-outbound-persistence-jpa-c12`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L832`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `PolicyTransactionPort.inTransaction(...)`은 단순 예외 기반 wrapper가 아니다. 결과는 최소 다음 상태를 구분한다. `CommittedWithPostCommitFailure` `Participating` `DeterminateRollback` `Indeterminate` 핵심은 **commit exception = rollback**으로 가정하지 않는 것이다. commit 호출 전에 phase를 `COMMIT_REQUESTED`로 올리고, Spring `TransactionSynchronization` sentinel로 실제 callback을 관찰한다. commit에서 exception이 발생해도: 1. `afterCommit()`이 이미 확인됐으면 `CommittedWithPostCommitFailure` 2. rollback callback/`UnexpectedRollbackException`/replay-candidate가 확인되면 `DeterminateRollback` 3. 그 외에는 `Indeterminate` 즉 연결 끊김 같은 애매한 exception을 “rollback이겠지”라고 간주하지 않는다. `Indeterminate`는 retry 대상이 아니다. replay 조건은 모두 만족해야 한다. policy = `COMMAND_SERIALIZABLE_REPLAY_SAFE` 현재 attempt가 physical transaction owner attempt < configured max current thread not interrupted result가 `DeterminateRollback` failure가 40001 serialization 또는 40P01 deadlock replay candidate 따라서 commit ack를 못 받은 상태는 replay되지 않는다. 이 점은 뒤에서 다룰 JPA public API completion-evidence wiring gap의 중요한 mitigation이다. **현재 canonical application path는 completion evidence infrastructure가 없어도 불확정 commit을 자동 재실행하지 않는다.**…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — CallBudget를 transaction timeout보다 먼저 적용한다
|
||
- slug: `adapter-outbound-persistence-jpa-c13`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L879`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: application policy path는 timeout을 단순히 `TransactionDefinition.setTimeout()` 하나로 끝내지 않는다. `ca-skeleton.jpa.transaction` settings는 transaction/resource-budget defaults를 가진다. 주요 invariant: duration positive duration <= 1 day retry max attempts 1..5 statement timeout <= transaction timeout lock timeout < statement timeout completion/acquisition/action margin hierarchy 즉 runtime에서 무한 retry나 무한 transaction timeout을 property 하나로 열 수 없게 hard cap을 둔다. 이 점은 API `RetryProfile.maxAttempts`가 upper bound를 갖지 않는 것과 대비된다. canonical application path는 실제 deployment settings에서 최대 5회를 강제한다. CallBudget admission은 connection pool을 빌리기 **전**부터 시작한다. transaction을 열 가치가 있으려면 남은 budget이 최소 다음을 감당해야 한다. begin 후에는 실제 남은 budget으로: Spring whole-transaction timeout statement timeout lock timeout idle-in-transaction timeout 따라서 pool에서 오래 기다린 요청이 “원래 5초 timeout이었으니 DB에서 다시 5초”를 받지 않는다. 이미 소비한 wall-clock budget을 transaction layer가 다시 주지 않는 구조다. canonical path의 retry backoff도 CallBudget-aware다. 다음 attempt를 시작하기 전에: jitter delay 다음 acquisition reserve 다음 최소 transaction/action margin 을 모두 감당할 수 있는지 확인한다. budget이 부족하면 sle…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `CommitFailureClassifier`
|
||
- slug: `adapter-outbound-persistence-jpa-c17`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1074`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: completion unknown candidate: SQLSTATE 40003 connection class 08* admin shutdown / crash / cannot-connect-now 계열 transport break cause 중요한 건 이 classifier를 generic SQLSTATE translator 대신 **commit call 내부에서만** 적용한다는 것이다. connection reset이 query 실행 중 발생했다면 connection unavailable일 수 있지만, provider에게 COMMIT을 보낸 후 reset됐다면 “commit됐는지 모름”이다. SQLSTATE만으로 이 둘을 구분할 수 없고 transaction phase가 필요하다. PostgreSQL classifier source도 이 이유를 직접 설명한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — completion-unknown metric도 현재 transaction path에서 호출되지 않는다
|
||
- slug: `adapter-outbound-persistence-jpa-c19`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1210`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `JpaTransactionObservation.recordCompletionUnknown(...)` 구현은 존재한다. 하지만 production에서: `JpaObservabilityAutoConfiguration` construction = 0 `JpaTransactionObservation.recordCompletionUnknown(...)` call = 0 `recordCommitted/recordRolledBack/recordTimedOut` call도 0 `JpaPlatformRuntimeAutoConfiguration`이 만드는 default `RetryEventListener`도 empty implementation이며, `JpaObservabilityAutoConfiguration`을 통해 metric listener로 합성하지 않는다. 따라서 runbook의 `jpa.transaction.completion.unknown` signal은 현재 source wiring으로는 생성 근거를 찾지 못했다. 이 observability factory 전체의 reachability 문제는 later observation/baseline capability sub-scope에서 다시 exhaustive하게 확인한다. 여기서는 completion-unknown path의 cross-scope evidence로만 기록한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Querydsl integration은 production runtime classpath를 강제로 오염시키지 않는다
|
||
- slug: `adapter-outbound-persistence-jpa-c30`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2087`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `QuerydslJpaSupport`는 Advanced opt-in으로 설계돼 있다. lockfile에서 Querydsl은: compileClasspath test/integration/performance classpaths 에는 나타나지만 production `runtimeClasspath` configuration에는 포함되지 않는다. 따라서 JPA leaf를 사용하는 것만으로 Querydsl runtime dependency가 Stable deployment에 따라오는 구조는 아니다. `QuerydslJpaSupport`도: bounded page size <= 500 null predicate는 explicit unbounded opt-in 없으면 거부 registered `QueryName`을 Hibernate comment hint로 적용 현재 production consumer는 확인되지 않았다. 이는 Advanced opt-in helper의 미채택 상태로 기록하며 dead-code defect로 단정하지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — release-task existence validator
|
||
- slug: `adapter-outbound-persistence-jpa-c34`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2523`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: fresh `verifyJpaReleaseGateTasks`도 성공했다. 이 success는 오히려 validator limitation의 evidence다. task semantic coverage/tag를 검사하지 않기 때문이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — PostgreSQL failure translation: SQLSTATE 분류는 맞지만 `40003` 의미가 translator에서 소실된다
|
||
- slug: `adapter-outbound-persistence-jpa-c35`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2632`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `PostgreSqlFailureClassifier`는 PostgreSQL SQLSTATE를 bounded `FailureCategory`로 분류한다. serialization failure, deadlock, lock-not-available, constraint family, timeout, connection failure, schema/data 문제를 문자열 메시지가 아니라 SQLSTATE/structured server field 기준으로 다루는 방향은 적절하다. constraint 이름도 server error field에서 꺼내 catalog로 번역하므로 localized message parsing에 의존하지 않는다. 문제는 `COMPLETION_UNKNOWN`이다. 현재 `PostgreSqlExceptionTranslator.translate()`는 classifier 결과가 `COMPLETION_UNKNOWN`이어도 `JpaFailureContext`의 `completionUnknown`을 항상 `false`로 만들고, switch에서 `COMPLETION_UNKNOWN`을 `UNKNOWN`과 함께 일반 `JpaPersistenceException(FailureCategory.UNKNOWN, ...)`으로 강등한다. 직접 probe에서 SQLSTATE `40003`은 다음처럼 변환됐다. 여기서 단순 진단 정보만 사라지는 것이 아니다. 현재 `DefaultJpaRetryPolicy`는 `TransactionCompletionUnknownException` 또는 `FailureCategory.COMPLETION_UNKNOWN`을 가장 먼저 검사해 `RECONCILE`로 보낸다. 그런데 실제 translator를 통과시키면 focused policy probe 결과가 다음과 같다. 즉 **재실행은 막지만, commit 결과를 확인해야 하는 reconciliation 경로도 잃는다.** fail-closed라는 이유로 안전하다고 볼 수 없는 이유다. commit이 실제로 적용됐는지 알 수 없는 상태를 terminal failure로 바꾸면 caller는 설계된 recovery protocol을 실행할 근거를 잃는다. 현재 r…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — PostgreSQL Idempotency V2: owner/CAS 구조는 강하지만 replay 경계가 두 군데 어긋난다
|
||
- slug: `adapter-outbound-persistence-jpa-c36`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2669`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `PostgreSqlOwnerSafeIdempotencyStore`는 row lock, owner token, attempt, state revision, operation id와 transition digest를 결합해 claim/renew/fail/complete를 보호한다. `renew`와 `markFailed`는 동일 operation id replay에서도 semantic argument를 digest에 넣어 `SAME_ARGUMENTS`와 `DIFFERENT_ARGUMENTS`를 분리한다. 이 구조 자체는 강하다. 현재 revision에서는 `PostgreSqlIdempotencyProviderConfig`가 이 store를 production provider로 실제 생성하므로 아래 두 finding은 dormant helper 문제가 아니다. `inspect()`는 row가 `COMPLETED`이고 response payload가 있으면 `replayUntil`이 이미 지난 값인지 확인하지 않고 무조건 `COMPLETED_REPLAY`를 반환한다. 반면 claim path는 DB time과 expiry를 보고 만료된 row를 takeover 가능 상태로 처리한다. 실제 PostgreSQL 16에서 replay TTL 25ms로 완료한 뒤 50ms를 기다린 probe 결과: 즉 같은 시점의 같은 row가: Application의 `IdempotencyExecutorV2`는 reconciliation에서 `COMPLETED_REPLAY`를 실제 저장 응답 반환 신호로 사용한다. 따라서 이 불일치는 단순 introspection 문제가 아니라 **만료 후 새 실행이 허용된 시점에도 이전 응답을 reconciliation 결과로 반환할 수 있는 lifecycle correctness 문제**다. JPA 설계 문서가 동일 Idempotency V2 contract를 구현한다고 참조하는 Redis state machine도 `COMPLETED -> [*] : replay TTL expires`로 수명을 끝낸다. JPA `inspect()`만 이 만료를 무시한다. **판정: P1 — production idempotency lifecy…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 확인된 안전 경계
|
||
- slug: `adapter-outbound-persistence-jpa-c38`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2780`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: native write와 COPY는 caller가 임의 SQL identifier를 조립하도록 두지 않고 registered statement/name boundary를 사용한다. 값은 JDBC parameter 또는 COPY stream으로 전달된다. COPY에는 format/size bound와 transaction requirement가 있고, work claiming은 등록된 queue definition과 PostgreSQL `FOR UPDATE ... SKIP LOCKED` 경계를 사용한다. JSON path/query support와 range query support도 registry/typed value boundary를 두고 실제 값은 bind한다. constraint translation 역시 structured SQLSTATE/server fields를 사용한다. 이번 sub-scope에서 이 영역의 새로운 SQL-injection/runtime-wiring defect는 확인되지 않았다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Vendor migrations
|
||
- slug: `adapter-outbound-persistence-jpa-c39`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2805`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 다음 9개 migration을 모두 읽었다. 확인한 경계는 다음과 같다. idempotency owner/state/replay/transition metadata의 persisted shape outbox claim/delivery/index shape integer advisory/row-lock support table와 expiry extension capability schema registry adoption/widening request hash `char`/`varchar` drift 보정 durable operation / live-event log schema real PostgreSQL probe에서 Flyway는 vendor 9 migrations를 모두 validate/apply했다. 이번 sub-scope에서 migration 순서, 현재 schema 제약, index 선언 자체로 승격할 신규 defect는 확인하지 못했다. capability-specific migration의 완전한 cross-stream adoption은 각 owning capability scope에서 다시 본다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Idempotency real-PostgreSQL TTL boundaries
|
||
- slug: `adapter-outbound-persistence-jpa-c40`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2883`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `evidence/raw/066-postgresql-idempotency-replay-boundary-probe.txt` exact `postgresqlIdempotencyIntegrationTest` lane `complete()` changed replay TTL false-same replay 재현 expired COMPLETED row의 `inspect()`/`claim()` lifecycle 불일치 재현 temporary test는 실행 후 source에서 복원 BUILD SUCCESSFUL
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 이번 scope에서 finding으로 승격하지 않은 항목
|
||
- slug: `adapter-outbound-persistence-jpa-c41`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2913`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: registered native write/COPY의 SQL/value boundary work-claim `SKIP LOCKED` 기본 구조 structured SQLSTATE/constraint-name 추출 array/json helper의 bounded value handling polling outbox cutover sentinel의 transition별 반복 검사 차이: claim 자체가 immutable sentinel을 요구하고 current evidence만으로 stale claim이 cutover를 우회한다고 입증되지 않아 보류 vendor migration 9개의 현재 적용 순서/문법
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — H2 idempotency와 V2 owner 필드
|
||
- slug: `adapter-outbound-persistence-jpa-c43`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3119`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 처음에는 `H2IdempotencyClaimRepository`의 MERGE/takeover가 V2 owner/transition field를 초기화하지 않는 점을 의심했다. 그러나 baseline `IdempotencyRecordEntity` 자체가 V1 field만 mapping하고, owner-safe V2는 PostgreSQL capability stream으로 분리돼 현재 별도 activation contract를 가진다. 서로 다른 schema generation의 field를 H2 V1이 reset하지 않는 것은 현 계약 위반이 아니다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — quota FIFO settlement 자체
|
||
- slug: `adapter-outbound-persistence-jpa-c46`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3350`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: reservation row가 upload id와 연결되지 않아 `JpaQuotaCommitGateway`가 scope의 가장 오래된 live reservation부터 정산하는 것은 `docs/fileserver/design-deviations.md`에 명시적으로 기록된 adaptation이다. row identity와 실제 upload identity가 1:1이 아닌 것 자체는 현재 설계 계약이다. 다만 그 문서가 전제로 둔 aggregate byte enforcement가 실제로 없다는 점은 §78의 별도 P1 finding으로 올렸다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — cleanup fenced lease의 expiry-after / takeover-before window
|
||
- slug: `adapter-outbound-persistence-jpa-c47`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3354`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: cleanup settlement query는 claim token을 fence하고 reaper takeover가 token을 교체한다. lease expiry 직후 아직 takeover 전인 worker가 settle할 수 있는 window는 보이지만, 새 owner가 생긴 뒤 stale worker가 상태를 덮어쓰는 race는 token CAS가 막는다. durable-operation과 달리 현재 계약만으로 “expiry 순간부터 절대 settle 금지”라고 확정할 충분한 근거가 없어 finding으로 올리지 않았다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Notification composition과 schema lifecycle
|
||
- slug: `adapter-outbound-persistence-jpa-c48`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3401`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: Notification JPA capability는 production opt-in path로 실제 composition된다. `PersistenceJpaRootAutoConfiguration`이 `NotificationJpaPersistenceFacade`를 import한다. facade가 `NotificationJpaPersistenceConfig`를 import하고 entity/repository/store bean을 조립한다. application-side worker/config가 recipient lease, reconciliation, provider-event ledger, admin operation store를 실제 소비한다. `NotificationSchemaActivation`은 capability registry를 읽어 startup activation을 검사한다. schema stream은 V1~V10까지 진화했지만 registry는 V4에서 `jpa-notification-platform-v4`, `feature_revision=4`, `INSTALLED_INACTIVE`를 기록한 뒤 더 이상 revision을 올리지 않는다. 반면 current Java mapping과 SQL은 V5~V10에서 추가된 column/constraint에 실제 의존한다. 이 drift가 §88의 startup false-positive를 만든다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — tenant-bound repository guard
|
||
- slug: `adapter-outbound-persistence-jpa-c50`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3565`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: tenant-sensitive lookup이 전부 완전하다고 corpus 전체 결론을 내리지는 않았지만, `TenantBoundRepositoryGuard`와 tenant-qualified repository method가 존재하고 이번 68-file owning scope에서 즉시 재현 가능한 cross-tenant bypass는 확정하지 못했다. 별도 inbound/application authorization 조합은 cross-scope 단계가 소유한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — schema identifier selection/reset
|
||
- slug: `adapter-outbound-persistence-jpa-c52`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3780`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `SchemaTenantRegistry`는 unquoted PostgreSQL identifier shape를 제한하고, connection provider는 schema 값을 statement text에 직접 붙이지 않고 bound `set_config`로 적용하며 release 시 neutral `pg_catalog`로 reset한다. 별도 failure-in-reset / pool-implementation semantics까지 corpus 전체 보장은 하지 않지만, 현재 happy-path isolation contract를 뒤집을 evidence는 없었다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `CommitAmbiguityProxy` / `PostgreSqlContractExtension`
|
||
- slug: `adapter-outbound-persistence-jpa-c54`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4017`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 두 public helper는 defining file 밖 exact FQN reference가 0이다. 특히 `PostgreSqlContractExtension.serverVersion()`은 이름과 달리 database의 `SHOW server_version`이 아니라 Docker image + container id를 반환하지만 현재 integration support는 별도 `JpaPlatformContractSupport.serverVersion()`로 실제 server version을 읽는다. 따라서 잘못된 current evidence로 분류하지 않고 dead/unadopted helper로 기록한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `JpaReleaseManifest`의 regex parser
|
||
- slug: `adapter-outbound-persistence-jpa-c55`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4021`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: Java testkit parser 자체는 정규식 기반이라 일반-purpose JSON parser가 아니다. 그러나 실제 registry 파일은 root Gradle `verifyJpaReleaseGateTasks`에서 `JsonSlurper`로 다시 parse되고 real task graph까지 resolve한다. 현재 malformed JSON을 Java regex parser 하나가 받아들일 가능성만으로 release fail-open을 별도 finding으로 중복 승격하지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — release gate 소속은 양방향으로 검증되지 않는다
|
||
- slug: `adapter-outbound-persistence-jpa-c57`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4306`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 근거: `evidence/raw/112-pool-lane-claim-registry-reachability.txt`. `config/jpa/release-registry.json`의 gate는 6개이고 모두 blocking이다. pool lane은 registry에도, `docs/jpa/support-matrix.md` §Release gates 6행에도, testkit `JpaReleaseGate.required()`에도 없다(세 곳 모두 grep exit=1). 그런데 `jpaPlatformReleaseGate`는 `dependsOn jpaPlatformPoolContractTest`를 갖고, root `jpaReleaseGate`가 그것을 다시 의존한다. `verifyJpaReleaseGateTasks`는 registry → task graph 한 방향만 검사한다(registry의 각 gate가 실제 `Test` task로 resolve되는가). 반대 방향 — release gate에 들어 있는 lane이 registry에 있는가 — 은 어디서도 검사되지 않는다. 따라서 `jpaPlatformReleaseGate`에서 pool lane 의존을 지워도 어떤 verifier도 반응하지 않고, 남는 실행 경로는 nightly workflow 한 줄뿐이다. 이것을 결함으로 올리지는 않는다. `jpaPlatformReleaseGate`의 주석이 밝힌 집계 기준은 "documented gate가 검증되지 않은 채 통과하게 만드는 lane"이고, pool lane은 문서화된 gate를 뒷받침하지 않으므로 기준상 registry에 없는 것이 일관적이다. 다만 그 결과로 이 lane의 release gate 소속만은 아무 계약도 보호하지 않는다는 사실을 기록한다. P3.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `JpaPlatformContractSupport`의 컨테이너 수명 서술은 실제와 다르다
|
||
- slug: `adapter-outbound-persistence-jpa-c58`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4541`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 클래스 javadoc은 이렇게 말한다. 실제 사용은 정확히 그 "per class"다. `JpaPlatformContractSupport.start()` 호출 지점은 31곳이고 대부분 `@BeforeAll`에서 시작해 `@AfterAll`에서 `close()`한다. JVM 수준 공유 인스턴스나 static holder는 없다. `StablePostgreSqlMatrixContractTest`는 test마다, `JpaPlatformContractSupportOwnershipTest`는 test마다(5개) 컨테이너를 새로 띄운다. 실측치는 다음과 같다(`115-integration-lane-original-verification.txt`, XML의 Testcontainers 로그 집계). 한 번의 전체 tag lane 통과에 PostgreSQL 컨테이너가 87번 기동한다. 그럼에도 5개 lane 전체가 3분 10초에 끝났으므로 비용 주장이 무너지는 수준은 아니다. 기록하는 이유는 서술과 구현의 불일치다 — 클래스가 자기 설계 근거로 내세운 "JVM 공유"가 소비자 31곳 어디에서도 성립하지 않는다. P3. 같은 클래스의 다른 서술은 사실이다. multi-version 선택을 fail-closed로 거부하는 것(`start()`가 `selected.size() != 1`이면 예외), 그리고 "the CI matrix fans out"은 `jpa-release.yml`(16/17/18), `jpa-pr.yml`(16/18), `jpa-nightly.yml`이 `-Pjpa.matrix.versions`로 실제 fan-out하는 것으로 확인된다. `JpaPlatformContractSupportOwnershipTest`가 지키는 pool 소유권(호출당 새 pool을 만들어 참조를 잃던 과거 결함)도 실제 assertion으로 고정돼 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — reactive 경로가 명시적으로 배치한 세 가지
|
||
- slug: `adapter-outbound-persistence-mongo-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L648`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: `DefaultReactiveMongoExecutor`의 javadoc이 blocking 경로가 공짜로 얻는 것과 여기서 직접 배치해야 하는 것을 대비한다. observation scope를 Reactor 자원(`Mono.using`/`Flux.using`)으로 두어 완료·오류·**취소** 모두에서 닫는다. HTTP 클라이언트 연결 해제가 취소를 일으키므로 취소가 흔한 경우다. timeout을 조립된 publisher에 적용한다. 구독 전에 적용하면 "람다를 만드는 데 걸린 시간"을 재게 된다. context를 Reactor Context로 옮긴다(`ReactiveMongoContextKeys`). 체인은 operator 경계마다 스레드를 바꾸므로 구독 시점의 `ThreadLocal`은 driver 응답 시점에 이미 없다. 기록해 둘 관측 하나: `executeMany(...)`는 성공을 `doOnComplete`로 기록하므로 **취소된 stream은 success도 failure도 기록하지 않는다.** observation은 `close()`되고 초기 tag(`result=unknown`, `failureCategory=none`)로 한 번 계수된다. 취소가 흔한 경로라는 점을 감안하면 이는 의도된 분류로 보이지만, `result=unknown` bucket이 "취소"와 "관측 시작 직후 예외"를 함께 담는다는 사실은 계약에 없다. P3/기록.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — migration은 fencing을 정면으로 다룬다
|
||
- slug: `adapter-outbound-persistence-mongo-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L868`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: `MongoMigrationLock.fence()`의 javadoc이 이 sub-scope에서 가장 정확한 문장을 담고 있다. 그래서 lease 위에 monotonic fencing token을 얹고, `MongoCollectionMigrationLock.tryAcquire`가 그 token을 **lease를 부여하는 같은 조건부 update 안에서 서버가 증가**시킨다("A token handed out anywhere else could be handed out twice"). `held()`는 owner 이름이 같아도 fence가 다르면 false를 반환한다 — 프로세스가 재시작했거나 운영자가 owner 문자열을 재사용한 경우다. `matchedCount`를 쓰는 이유(같은 값을 다시 쓰면 `modifiedCount`가 0이라 소유권 판정이 뒤집힌다)도 두 곳에 적혀 있다. `MongoMigrationHeartbeat`은 이미 고쳐진 결함의 산물이다: runner가 `execute`가 **반환된 뒤에** 한 번만 refresh했으므로, 40분짜리 `execute`는 35분 동안 만료된 lease를 들고 있었고 그 사이 두 번째 runner가 정당하게 획득해 같은 migration을 동시에 돌렸다. 이제 heartbeat이 migration에게 넘겨진다 — batch 경계를 아는 것은 migration뿐이기 때문이다. `MongoCollectionMigrationLedger.saveCheckpoint`에는 **두 개의** 결함 이력이 주석으로 남아 있다. upsert 하나로는 "매치할 게 없었다"와 "fence filter가 배제했다"를 구분할 수 없어 *모든 migration의 첫 checkpoint*가 "a newer migration runner owns the lease"로 거부됐고, 동시에 진짜 배제 경로는 unique index의 duplicate-key로 죽어 그 문장을 만드는 분기가 **도달 불가**였다. 지금은 replace-then-insert로 두 경우를 분리한다. `MongoMigration`에 `rollback`이 없는 것도 명시적 결정이다 — "A rollback method implies the rever…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `OutboundSupportConfig`: unconditional shared bean seam과 실제 runtime wiring
|
||
- slug: `adapter-outbound-support-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L318`
|
||
- owning-module: `adapter-outbound-support`
|
||
- classification: `OutboundSupportConfig`는 `@Configuration`이며 `FailOpenDependencyLogger` bean 하나만 제공한다. 별도 master property condition은 없다. 이는 support 자체를 optional capability로 취급하지 않고, 실제 provider/client capability의 on/off를 sibling adapter가 소유하게 하려는 구조다. `OutboundSupportConfig`를 support 밖 production Java에서 명시적으로 참조하는 파일은 0개다. 그러나 실제 composition root `CaSkeletonApplication`은 다음 broad package를 component scan한다. `AUTO_CONFIGURED_PACKAGES` exclusion에는 messaging/notification/persistence 등은 들어가지만 support package는 포함되지 않는다. 따라서 support config는 broad component scan으로 도달한다. registry도 support runtime membership을 `app-bootstrap`으로 선언하고 `app-bootstrap/build.gradle`이 support project를 직접 `implementation`한다. 따라서 이 configuration은 현재 **active scanned path**다. support config 자체에는 `@ConditionalOnProperty`가 없고 `@ConditionalOnMissingBean`만 있다. 이것은 같은 optional adapter들의 master switch 누락으로 판정하지 않았다. support는 provider/client를 생성하지 않는다. logger bean 하나만 default로 제공한다. actual messaging/notification/httpclient 등은 자기 capability root에서 activation을 소유한다. support README와 config javadoc 모두 이 비대칭을 의도적으로 설명한다. `OptionalAdapterBeanGatingT…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — idempotency, inbox, outbox: uncertainty를 상태로 보존한다
|
||
- slug: `application-core-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L118`
|
||
- owning-module: `application-core`
|
||
- classification: 초기 contract는 scope + request fingerprint로 claim/replay를 제공하고, same key/different fingerprint를 conflict로 분리한다. completed result는 replay하고 in-flight는 bounded poll한다. 이 버전은 “DB operation의 효과가 이미 발생했지만 응답만 잃은 상태”를 충분히 표현하지 못한다. V2는 owner-safe CAS handle에 scope/token/attempt/revision/claimOperationId를 넣고 stale owner mutation을 거부한다. processing-start를 durable하게 확인하기 전에는 body를 실행하지 않으며 claim/start/completion의 unknown result는 inspect/reconcile 대상으로 남긴다. processing start 이후 ordinary RuntimeException은 효과가 없다고 증명할 수 없으므로 `EFFECT_UNKNOWN_ABANDONED` 쪽으로 분류되고 자동 replay 권한을 주지 않는다. 명시적인 `RetryableNoEffect`만 안전 재시도 근거로 취급한다. scope digest는 versioned keyed digest + operation code로 정규화되고 raw identity는 외부 surface에서 제거된다. lease/replay TTL과 owner token grammar도 bounded다. **Historical evidence.** V2 contract가 인접한 package에 중복 복제돼 구현체들이 서로 다른 nominal type을 참조한 문제가 있었고, singular contract를 유지하는 regression test가 존재한다. Inbox contract는 same-store 처리와 owner-safe receive/process state를 모델링한다. `RECEIVED -> PROCESSING -> COMPLETED/RETRYABLE/DEAD` 상태를 가지고 ACK는 handler transaction commit 이후에만 가능하다. expired owner가 늦게 결과를…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — cache, lease, lock: 동시성 완화와 correctness authority를 구분한다
|
||
- slug: `application-core-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L152`
|
||
- owning-module: `application-core`
|
||
- classification: `CacheAsideExecutor`는 fresh/negative hit, hard miss, stale, incompatible schema, provider unavailable을 명시적으로 구분한다. source load에는 key-local single-flight와 global source bulkhead를 함께 적용한다. in-flight key 수, waiter 수, source concurrency, admission wait, load deadline이 모두 bounded다. stale value는 hard expiry 이전이며 **classified transient failure**일 때만 fallback될 수 있다. permanent failure에는 stale을 반환하지 않는다. source load 중 invalidation이 발생하면 lookup 때 캡처한 `CacheWriteCondition`이 더 이상 일치하지 않아 이전 source result의 refill을 거부한다. 이는 invalidate 직후 늦게 끝난 source load가 stale value를 resurrect하는 race를 막는다. optional distributed refresh coordination은 soft lease로 한 pod만 refresh하도록 하지만 correctness lock은 아니다. owner는 lease 획득 후 cache를 재확인해 다른 pod가 이미 fill했다면 source를 호출하지 않는다. claim 결과가 indeterminate이면 **동일 attempt token으로 한 번만 재시도**한다. contender는 stale이 아직 valid하면 즉시 stale을 반환할 수 있다. `CacheSingleFlight`는 waiter timeout/interruption을 보존하고 완료된 flight를 제거한다. leader가 영원히 남아 key bound를 점유하지 않도록 monotonic deadline 이후 abandoned flight를 opportunistic reap한다. V2 `DistributedLeasePort`는 caller가 provider send 전에 owner/operation t…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — fileserver: DB metadata와 physical content 사이의 실패 seam을 명시한다
|
||
- slug: `application-core-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L200`
|
||
- owning-module: `application-core`
|
||
- classification: fileserver는 application-core 안의 가장 큰 독립 orchestration 중 하나다. 핵심 contract는 “metadata transaction과 filesystem/object I/O가 원자적이지 않다”는 사실을 숨기지 않고 recovery model을 두는 것이다. upload admission은 authorization을 quota/storage admission보다 먼저 수행해 denial이 side-effect-free이도록 한다. reservation metadata/session은 DB transaction에서 만들지만 staging physical object는 외부 작업이므로 실패 시 compensation/reconciliation 대상이 된다. writer는 one-writer lease + fencing token을 사용한다. stale token은 append/finalize를 진행할 수 없고 takeover는 새 token을 만든다. append 시 metadata offset과 physical length가 다르면 자동 repair하지 않고 conflict로 중단한다. finalize는 declared length, server-computed digest, optional client digest를 순서대로 검사한다. client digest는 server digest를 대체하지 않는다. 이후 VERIFYING으로 이동하고 verifier가 publish 승인해야 READY가 된다. READY가 유일한 public/downloadable state다. publish physical success 뒤 READY metadata transaction이 실패하면 결과는 단순 retryable failure가 아니라 `AmbiguousCompletionException`과 recovery queue로 간다. physical publish가 이미 발생했을 수 있기 때문이다. cancel/cleanup race를 막기 위해 cleanup claim은 writer/cleaner barrier를 형성하며 stale cleanup claim을 reclaim하는 경로가 실제로 호출된다. physic…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 관찰: 재사용 가능한 도메인 “내용”보다 도메인 모델링 계약을 소유한다
|
||
- slug: `domain-core-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/01-domain-core.md#L68`
|
||
- owning-module: `domain-core`
|
||
- classification: 현재 `domain-core`에는 WorkLog 같은 실제 aggregate가 없다. 실제 샘플 aggregate/value object/event는 `sample-portfolio`에 있다. 이 모듈에 남은 production surface는 다음 두 종류다. 1. **식별자 추상화** — `ResourceId`, `IdFactory` 2. **모델링 표식** — `@ValueObject`, `@AggregateRoot`, `@DomainEvent` 따라서 “business concepts, entities, value objects…”를 둘 수 있는 계층이라는 정책과 달리, 현재 snapshot의 실제 contents는 skeleton 전반에서 사용할 **domain-layer contract/marker**에 가깝다. 이는 현재 source에 대한 관찰이며, 향후 실제 production domain type이 이 module에 추가되지 않는다는 뜻은 아니다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 헤징 예산
|
||
- slug: `grpc-advanced-resilience-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-resilience.md#L58`
|
||
- owning-module: `grpc-advanced-resilience`
|
||
- classification: 토큰 버킷이다. 헤지 하나가 `round(1/ratio)` 토큰을 쓰고, 완료된 호출 하나가 토큰 하나를 돌려준다. 상한이 조용한 구간 뒤의 폭주를 제한한다. 비율 상한이 0.5 이고 그 근거가 적혀 있다. 그리고 왜 재시도 예산보다 더 급한지도 적는다. 소비는 정확한 비교 후 교체 루프다 — 이 가족에서 원자성을 제대로 다룬 몇 안 되는 곳이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 소비자 컴파일 게이트
|
||
- slug: `grpc-codegen-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-codegen.md#L104`
|
||
- owning-module: `grpc-codegen`
|
||
- classification: `GrpcConsumerFixture.fromJavaSource` 가 릴리스된 소비자의 자바 소스에서 요구 사항 셋을 기계적으로 유도한다. 그리고 그것이 컴파일의 근사라는 것과, 근사인 이유(ADR-GRPC-002)를 함께 적는다. `breaksAgainst` 는 세 종류를 따로 보고한다 — 서비스 경로, 메서드 경로, 자바 패키지. 하나의 개수로 합치지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 생성자가 거부하는 것과 검증기가 보고하는 것
|
||
- slug: `grpc-discovery-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-discovery.md#L85`
|
||
- owning-module: `grpc-discovery`
|
||
- classification: `GrpcResolverProfile` 정규 생성자가 네 조합을 아예 만들 수 없게 한다 — 주소 0 이하, 단일 엔드포인트 리졸버에 복수 주소, 음수 갱신 주기, DNS 인데 갱신 주기 0. `GrpcKubernetesProfile` 정규 생성자는 셋을 막는다 — 메시 라우팅에 in-process 재시도 소유자, 긴 스트림인데 재접속 예산 0, 긴 스트림인데 배수 유예 0. 두 겹의 역할 분담이 이 저장소의 다른 곳에 적힌 규칙과 같다 — 위험한 조합은 정책이 아니라 생성자가 거부하게 만든다. 그리고 그 분담 때문에 검증기의 재시도 소유자 규칙은 일부 조합에서만 발화한다. `MESH` + `GRPC_PLATFORM` 은 생성자가 먼저 던지므로(둘 다 in-process 재시도) 검증기까지 오지 않고, `MESH` + `NONE` 이나 `K8S_VIP` + `SERVICE_MESH` 는 생성자를 통과해 검증기가 잡는다. 도달 불가 분기가 아니라 역할 분담이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 스키마가 계약이다
|
||
- slug: `grpc-operation-ledger-jpa-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-operation-ledger-jpa.md#L53`
|
||
- owning-module: `grpc-operation-ledger-jpa`
|
||
- classification: 마이그레이션 헤더가 왜 애플리케이션 검사가 아니라 제약인지 적는다. 커밋 행이 결과를 반드시 갖는다는 검사를 자바 record 와 DB 양쪽에 둔 이유도 적혀 있다 — 마이그레이션·백필·지원 스크립트가 쓴 행은 record 를 지나지 않는다. 전용 Flyway 위치(`db/migration/grpc`)를 쓰는 이유도 적혀 있다. gRPC 플랫폼을 채택하지 않은 배포가 이 테이블을 만들도록 강요받지 않기 위해서다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 저장 키와 유니크 제약이 같은 행을 가리킨다
|
||
- slug: `grpc-operation-ledger-jpa-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-operation-ledger-jpa.md#L76`
|
||
- owning-module: `grpc-operation-ledger-jpa`
|
||
- classification: `GrpcOperationIdentity`(grpc-core-api): 즉 기본 키는 유니크 제약의 세 컬럼을 이어 붙인 파생값이다. 엔티티 javadoc 이 그 이중 저장을 설명한다 — 복합 쪽이 원자성을 주고, 파생 키가 조회에 단일 컬럼 기본 키를 준다. 같은 신원의 두 번째 청구는 **같은 기본 키 행**을 겨냥한다. §17.1 이 그 사실에서 나온다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 상태 전이
|
||
- slug: `grpc-operation-ledger-jpa-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-operation-ledger-jpa.md#L108`
|
||
- owning-module: `grpc-operation-ledger-jpa`
|
||
- classification: `IN_PROGRESS` 에서만 전이할 수 있다(`requireInProgress`). 커밋은 결과 참조가 비면 거부한다. `EnumType.STRING` 을 쓰는 이유가 javadoc 에 있다 — 서수 컬럼은 열거형에 값이 끼어들면 저장된 모든 행을 조용히 다른 값으로 만든다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 검증기가 담은 규칙
|
||
- slug: `grpc-spring-boot-starter-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-spring-boot-starter.md#L87`
|
||
- owning-module: `grpc-spring-boot-starter`
|
||
- classification: javadoc 이 선정 기준을 적는다. 전송·보안 — production 전송이 아니면 거부, 배포 환경에서 TLS 미사용·trust-all·반사 전체 공개 거부 실행기 — 큐 용량 1 미만(무제한) 거부, 풀 크기 양수 요구 메서드 — 단항인데 사용 가능한 마감이 0, 명시적 재시도가 멱등 프로파일과 모순, 멱등 키 필수인데 원장 비활성, Stable 범위 밖 RPC 종류 채널 — Stable 스킴 요구, 두 재시도 소유자가 동시에 in-process 재시도 고급 격리 — Stable 스타터가 advanced 의존을 끌면 위반 그리고 한 번에 전부 모아 실패한다 — "so a deployment learns the whole list in one restart."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 릴리스 게이트 — 문서가 후속이 아니라 차단 사유다
|
||
- slug: `grpc-testkit-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-testkit.md#L102`
|
||
- owning-module: `grpc-testkit`
|
||
- classification: 차단 사유가 다섯 갈래다 — 호환성 표의 누락 결과, 생산되지 않은 증거 등급, 스키마 발행 거부, 런북 부재, 결정 기록 부재, 지원 표 부재. `requireCertified` 는 능력이 이번 릴리스가 낸 증거로 인증되지 않으면 던지고, 메시지에 실제로 돈 등급을 나열한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-admin-api-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L588`
|
||
- owning-module: `messaging-admin-api`
|
||
- classification: 이 리프에 실행 코드가 없으므로 동시성 계약은 전부 **인터페이스 문서로 표현**되어 있고, 강제는 구현 리프의 몫이다. 펜싱 토큰의 하한이 타입으로 강제된다. `begin`/`checkpoint` 의 javadoc 이 각각 던져야 할 조건을 명시한다 — `begin` 은 "already completed, or another runtime holds a live lease", `checkpoint` 는 "the lease has been taken over by a newer token". 즉 **오래된 토큰의 쓰기를 거절하는 것이 구현 의무**로 문서화되어 있다. 이 리프의 모든 타입은 record 이거나 불변 final 클래스다. `DestructiveOperationGuard` 는 `final boolean` 하나만 갖고, `HmacApprovalVerifier` 는 키를 clone 해 보관한다. 공유해도 안전하다. 수명주기 훅은 없다. `MessagingAdminDurabilityValidator`(스타터, `InitializingBean`)가 유일한 기동 시점 훅이며 이 리프 밖이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-admin-api-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L802`
|
||
- owning-module: `messaging-admin-api`
|
||
- classification: `messaging-testkit` 과 마찬가지로 이 리프도 **javadoc 이 커밋 로그를 대신한다**. 여섯 개의 "이전에는 이랬다" 기록이 있고, 전부 같은 결함 계열을 가리킨다: **자기 자신을 근거로 삼는 주장.** 일곱 개가 하나의 이야기다: **승인이 처음에는 데이터였고, 지금은 타입이다.** 커밋 로그 자체는 정보가 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-admin-runtime-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L163`
|
||
- owning-module: `messaging-admin-runtime`
|
||
- classification: 코드가 그 순서를 지킨다. 실패 경로의 근거도 있다. 저널에 들어가는 실패 코드는 정제된다. 리스 길이 선택에도 근거가 붙어 있다. **리플레이와 리드라이브의 비대칭이 하나 있다.** 리드라이브는 `lease.resumeFrom()` 과 체크포인트 콜백을 실행 측에 넘기지만, 리플레이는 넘기지 않는다. `ReplayService.replay(...)` 시그니처에 `resumeFrom` 이 없다(`ReplayService.java:53-54`). 즉 리플레이는 리스를 받지만 재개하지 않는다 — 죽으면 처음부터 다시 읽는다. 클래스 javadoc 의 "a retry continues the same operation instead of either redoing it" 은 리드라이브에만 해당한다. 세 가지 실패를 고쳤다고 javadoc 이 적는다. 세 수정이 코드에 있다. 발행 → 확인 → 정산 순서가 이 리프의 핵심 불변식이다. **세 번째 수정 — 재개 — 는 인덱스 계산이 틀렸다.** §12.1(a)에서 상술한다. 판단은 옳다. 구현이 그 판단을 `dryRun` 파라미터로 표현한다. `DestructiveOperationGuard.authorize` 는 `dryRun` 이 참이면 즉시 반환한다(`DestructiveOperationGuard.java:54-56`). 즉 격리 리플레이는 "승인 불필요" 가 아니라 "dry run 인 척" 으로 통과한다. 감사 이벤트는 그 구분을 남긴다 — `approval.map(VerifiedApproval::ticket).orElse("isolated")`(`:77`) — 그러나 guard 쪽에는 남지 않는다. §17 P3. 마지막 문장이 지켜지는지가 이 클래스의 값어치다. 확인 결과 지켜진다. `begin` 의 `claim(...)` 이 네 갈래다(`:64-115`). 펜스는 `update(...)` 에 있다. 그리고 키 생성이 `ApprovalGrant.canonicalForm()` 의 규칙을 그대로 가져온다. 길이 접두 규칙이 `messaging-admin-api` 밖으로 전파된 사례다. (그 규칙이 **닿지 않은** 유일한 곳이 계획 다이제스트라는 점은 `analysis/messaging/m…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-admin-runtime-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L493`
|
||
- owning-module: `messaging-admin-runtime`
|
||
- classification: 트랜잭션 경계 없음 — `InMemoryAdminOperationJournal` 은 `ConcurrentHashMap.compute(...)` 로 키 단위 원자성을 얻는다(`:44, :198`). `begin` 의 검사-후-갱신 전체가 `compute` 람다 안에 있어 두 복제본이 동시에 `begin` 해도 하나만 성공한다. `AdminOperationJournalTest.twoReplicasRacingProduceExactlyOneLease` 가 그것을 검증한다. 펜싱 토큰은 세 지점에서 동작한다: 인수 시 `existing.leaseToken() + 1`(`:110`), 쓰기 시 토큰 대조(`:206`), 그리고 clamp 로 인한 단조성(`:128, :147, :167`). `aRuntimeThatLostItsLeaseCannotWriteOverTheSuccessor` 가 세 가지를 한 번에 확인한다 — 낡은 리스의 `complete(30)` 이 거절되고 기록은 45·STARTED 로 남는다. `RedriveService`·`ReplayService`·`DefaultMessagingAdminService` 는 모두 불변 필드만 갖는다. `clock` 을 `Supplier<Instant>` 로 주입받아 시간도 외부화되어 있다. 수명주기 훅 없음. 이 리프의 어떤 클래스도 `InitializingBean`·`SmartLifecycle` 을 구현하지 않는다 — 이것이 §17 첫 항목의 직접 원인이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-admin-runtime-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L772`
|
||
- owning-module: `messaging-admin-runtime`
|
||
- classification: 이 리프도 javadoc 이 이력을 대신한다. 다섯 개의 "이전에는 이랬다" 가 있고 전부 **분산 실행의 실패**를 가리킨다. 다섯이 하나의 이야기다: **크래시와 복제본을 고려하지 않은 admin 평면**. 고친 결과가 리스·펜싱·체크포인트·per-item 경계다. 그리고 마지막 두 항목이 §12.1(a)와 이어진다 — "재시도가 처음부터 다시 시작하는" 문제를 고치려고 `resumeFrom` 을 도입했고, 도입한 지점의 인덱스 계산이 실패분을 고려하지 않았다. 커밋 로그는 정보가 없다(4개, messaging 전체 공통).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-claim-check-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L57`
|
||
- owning-module: `messaging-claim-check`
|
||
- classification: **Claim Check 패턴** — 브로커 한계를 넘는 payload를 객체 저장소에 두고 메시지는 참조만 나른다. `messaging-reliability-api`의 `ClaimCheckReference`(storageKey·sizeBytes·sha256·expiresAt)를 값 타입으로 쓰고, 이 leaf가 그것을 만들고 검증하는 동작을 소유한다. 경계 진술이 두 클래스에 있다. **"decode perfectly into the wrong object"**가 이 leaf의 위협 모델이다 — 실패가 아니라 잘못된 성공.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-claim-check-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L85`
|
||
- owning-module: `messaging-claim-check`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `messaging-reliability-api`(api). 나가는 것: `messaging-spring-boot-starter`의 `allowed_dependencies`에 포함된다. **배선: 없다.** `ClaimCheckStore`의 production 구현이 0이고(유일한 구현은 테스트의 `FakeStore`), `ClaimCheckPublisher`·`ClaimCheckResolver`·`ClaimCheckPolicy` 생성이 leaf 밖에서 0건이다. 그런데 **`runtime_memberships`가 `["app-bootstrap"]`이다.** starter closure를 통해 배포 아티팩트에 실린다. `messaging-cloudevents`와 같은 조합이다 — **싣고 쓰지 않는다**(`analysis/messaging/messaging-cloudevents.md` §12.1).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-claim-check-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L124`
|
||
- owning-module: `messaging-claim-check`
|
||
- classification: javadoc이 이유를 적는다. **이것이 `messaging-reliability-api`의 `InboxRepository.purgeProcessedBefore` javadoc이 요구하고 강제하지 않는 것과 같은 형태의 규칙인데, 이쪽은 생성자가 강제한다.** 같은 저장소에서 같은 종류의 시간 관계 규칙을 한 곳은 강제하고 한 곳은 문서로만 둔다 — 그 leaf §17이 소유한다. 문턱과 목적지 payload 상한을 분리한 이유도 명시돼 있다. `DEFAULT_THRESHOLD_BYTES = 262,144` = 1 MiB의 1/4이고 javadoc이 그렇게 부른다. `defaults()`가 브로커 1일 보존 + 1일 재시도 경로에 대해 3일 보존을 준다 — 요구치(2일)보다 1일 여유. 두 번째가 `messaging-core-api`의 3상태와 직접 연결된다 — `REJECTED`와 `AMBIGUOUS`를 구분할 수 없는 시점에 삭제하면 모호한 발행의 payload를 지운다. 오프로드된 메시지는 payload를 **아예 갖지 않는다**. `Offloaded` record가 양방향 방어 복사를 한다(생성자 `payload.clone()`, 접근자 `payload.clone()`) — `EncodedMessage`(schema-api)·`OutboxRecord`(reliability-api)와 같은 패턴이다. **`ClaimCheckStore.delete`가 이 leaf에서 호출되지 않는다.** 인터페이스에 선언돼 있고 publisher가 의도적으로 안 부른다("Nothing here deletes on failure"). 보존 sweep이 부를 것을 전제하는데 그 sweep이 이 leaf에 없다. 크기 검사가 digest보다 먼저인 것이 합리적이다 — 크기 불일치는 SHA-256 계산 없이 즉시 판정된다. `sha256(byte[])`가 `HexFormat.of().formatHex(...)`로 **소문자** hex를 만든다. `ClaimCheckReference`의 정규식이 `[a-f0-9]{64}`이므로 두 쪽이 맞는다. `verify`가 검증된 payload의 **복사본**을 반환한다. **저장소가 아직 반환하더라도 거절한다.**…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-claim-check-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L260`
|
||
- owning-module: `messaging-claim-check`
|
||
- classification: **발행:** `publisher.offload(encodedPayload)` → 문턱 이하면 인라인 → 초과면 `store.put` → `Offloaded(빈 바이트, reference)` **소비:** `resolver.resolve(inline, reference, now)` → reference 없으면 인라인 → 만료 확인 → `store.get` → null이면 NOT_FOUND → `guard.verify`(만료·크기·digest) → `_MISMATCH`면 `ClaimCheckIntegrityException` 두 경로 모두 production에서 호출되지 않는다(§12.1).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-claim-check-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L270`
|
||
- owning-module: `messaging-claim-check`
|
||
- classification: **분류가 두 단계로 정확하다.** 만료·부재는 운영 문제(`PERMANENT_BUSINESS`), 크기·digest 불일치는 오염(`POISON_MESSAGE`). 두 예외 클래스와 두 카테고리가 그 구분을 담는다. `ClaimCheckIntegrityGuard.sha256`이 `NoSuchAlgorithmException`을 `IllegalStateException("Java runtime does not provide SHA-256")`으로 감싼다 — 복구 불가능한 환경 문제이므로 메시지 실패가 아니다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-claim-check-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L286`
|
||
- owning-module: `messaging-claim-check`
|
||
- classification: `ClaimCheckPublisher`·`ClaimCheckResolver`는 final 필드만 갖는 불변 객체다. `ClaimCheckIntegrityGuard`는 상태가 없고 `ClaimCheckResolver`가 인스턴스를 필드로 하나 만든다. `MessageDigest.getInstance("SHA-256")`이 **호출마다** 새 인스턴스를 만든다 — `MessageDigest`는 스레드 안전하지 않으므로 이것이 옳다. 재사용했다면 동시 호출이 서로의 상태를 오염시킨다. `ClaimCheckStore` 구현의 스레드 안전성 요구는 인터페이스 javadoc에 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-claim-check-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-claim-check.md#L436`
|
||
- owning-module: `messaging-claim-check`
|
||
- classification: 이 leaf의 javadoc은 이전 결함을 서술하지 않는다 — 대신 **막으려는 사고**를 서술한다. **"rare in a test and routine under load"**가 이 저장소 전반의 주제다 — `messaging-observability`의 카디널리티, `messaging-security`의 회전 경합, `messaging-transport-spi`의 자원 누수가 같은 형태다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-cloudevents-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L321`
|
||
- owning-module: `messaging-cloudevents`
|
||
- classification: `DefaultCloudEventMapper`는 **상태가 없다** — 필드가 `SPEC_CONTENT_TYPE_FALLBACK` 상수 하나뿐이고 모든 메서드가 인자만 쓴다. 스레드 안전하다. 다만 그 사실이 javadoc에 적혀 있지 않다. `CloudEventExtensions`는 상수 홀더이고 private 생성자를 갖는다. `CloudEventBuilder`는 호출마다 새로 만들어진다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-core-api-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L70`
|
||
- owning-module: `messaging-core-api`
|
||
- classification: 이 leaf는 **브로커 중립 공개 계약**을 소유한다. 여기에는 구현이 거의 없다 — 85개 타입 중 인터페이스 11개, enum 12개, record 46개, 유틸리티 final class 5개, 예외 26개이고, 실행 가능한 로직은 `UuidV7.next()`, `WireSafeText.require`, `MessageHeaders.validateAndCopy`, 그리고 record 생성자의 검증뿐이다. **무엇이 아닌가**가 이 leaf에서는 무엇인가만큼 중요하고, 코드가 그것을 직접 말한다. `build.gradle` 전문: `src/main/java` 전체에서 `java.*`와 자기 패키지 밖 import는 **0개**다(`evidence/raw/269` §F). Spring도, Kafka·AMQP 클라이언트도, Reactor도 없다. 이것은 우연이 아니라 원래 계획이 명시한 제약이고(`docs/superpowers/plans/2026-08-10-messaging-platform-implementation-plan.md:13` — "`messaging-core-api`에는 Spring Kafka, Spring AMQP, Pulsar, NATS, Spring `Message<?>`, Reactor 의존성을 넣지 않는다"), 현재 소스에서 재측정해도 참이다. 경계는 세 방향으로 그어져 있다. **브로커 쪽으로.** `MessageDestination`은 논리 이름·카탈로그 타입·payload 클래스만 갖고 topic/exchange/queue/subject를 갖지 않는다(`destination/MessageDestination.java:9-11`). `DestinationName`의 패턴 `[a-z0-9][a-z0-9.-]{0,159}`은 `:`과 `/`와 공백을 배제해서 `topic://orders` 같은 물리 주소를 논리 이름으로 밀어 넣는 것을 생성자에서 막는다(`destination/DestinationName.java:16`). 주석이 이유를 적는다 — "otherwise the physical mapping owned by the destination profile could be bypassed from applic…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 패키지/컴포넌트 지도
|
||
- slug: `messaging-core-api-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L123`
|
||
- owning-module: `messaging-core-api`
|
||
- classification: `MessageEnvelope<T>`가 중심이고 나머지 11개가 그 필드 타입이다. 부속: `UuidV7`(생성기), `WireSafeText`(검증 유틸). 봉투는 불변이고 네 가지 파생 메서드가 있다 — `withPayload`, `withContentType`, `withTenant`, `withHeaders`. 넷 다 `messageId`를 복사한다. `withPayload`의 javadoc이 그 이유를 적는다: "Encoding, decoding, Claim Check offloading, and DLQ forwarding all need this, and every one of them must keep `messageId()` intact — which is exactly what this method guarantees by construction"(`MessageEnvelope.java:80-82`). `HeaderName`, `HeaderValue`, `MessageHeaders`, `ReservedHeaders`, `CanonicalEnvelopeHeaders`. `ReservedHeaders`는 23개 이름 상수와 `msg.` **prefix 전체**를 소유한다. `CanonicalEnvelopeHeaders`는 그 예약 네임스페이스를 둘로 쪼갠다 — 봉투 필드가 이미 갖고 있는 15개(`ENVELOPE_FIELDS`)와, 봉투에 대응 필드가 없어서 헤더로만 이동할 수 있는 나머지 8개(`REDRIVE_ID`, `REDRIVE_COUNT`, `RETRY_ATTEMPT`, `FIRST_FAILURE_AT`, `LAST_FAILURE_AT`, `FAILURE_CATEGORY`, `FAILURE_CODE`, `ORIGIN_DESTINATION`). `MessageDestination<T>`, `DestinationName`, `DestinationKind`(7), `MessagingCapabilities`(boolean 12), `DestinationCapabilities`, `ConfirmationRequirement`(3), `CapabilityRegistry`. 퍼블리셔 4종(`MessagePublisher`, `…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-core-api-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L463`
|
||
- owning-module: `messaging-core-api`
|
||
- classification: 트랜잭션 개념이 이 leaf에는 두 가지 형태로만 등장하고 둘 다 **선언**이다. `MessagingCapabilities.brokerTransaction` — 브로커가 트랜잭션 스코프를 제공하는가 `ProcessingGuarantee.BROKER_TRANSACTIONAL` — "Atomicity holds only inside the transaction scope the broker itself defines" `ExternalSideEffectGuarantee.INBOX_TRANSACTIONAL` — "An Inbox row and the side effect commit inside the same database transaction" DB 트랜잭션은 이 leaf가 만지지 않는다. 동시성 지점은 **하나**다: `UuidV7.STATE`(`AtomicLong`). `updateAndGet`이 CAS 루프이므로 다중 스레드에서도 각 호출이 서로 다른 packed state를 얻는다. `RANDOM`(`SecureRandom`)은 thread-safe다. `MessageHeaders`는 생성 시 `LinkedHashMap`에 복사하고 `Collections.unmodifiableMap`으로 감싸 반환하므로 공유 안전하다. 다만 `find(String)`이 `values.entrySet().stream()` 선형 탐색이다 — 최대 64개이므로 실용상 문제는 아니지만 hot path에서 반복 호출되면 O(n)이다. 수명주기 개념은 `DeliveryContext.shutdownRequested`뿐이고, javadoc이 목적을 적는다 — "during a graceful drain the platform stops creating new retry attempts, and a long-running handler that can wind down early shortens the drain instead of being cancelled at the deadline." **이 필드는 production에서 도달 불가능하다**(§12.1).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-core-api-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L734`
|
||
- owning-module: `messaging-core-api`
|
||
- classification: 이 leaf를 건드린 커밋은 4개다. 최초 커밋 메시지는 **24개 leaf**라고 적었고 현재 registry의 messaging leaf는 **25개**다. 이후 커밋에서 하나가 늘었다는 뜻이며, 커밋 메시지는 그 시점의 사실이므로 drift로 분류하지 않는다. **코드 주석이 보존한 실패 이력**이 이 leaf의 가장 밀도 높은 사료다. 아래는 전부 "예전에는 이랬고 그래서 무엇이 깨졌다"를 현재 코드가 직접 적어 둔 것이다. 이 목록 자체가 이 leaf의 성격을 말한다 — **13개 이상의 wire 경계 결함을 한 번에 정리한 흔적**이고, 대부분이 "검사가 없었다"가 아니라 "검사가 잘못된 단위(문자 vs 바이트, 정확일치 vs 세그먼트, 이름목록 vs prefix)로 되어 있었다"이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-inbox-jdbc-postgresql-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L319`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- classification: **수신 처리:** `handleOnce(name, delivery, action)` → `consumer.runOnce(messageId, name, now, () -> { action.apply(delivery); return APPLIED; })` → runner가 트랜잭션 열기 → `repository.reserve(...)` → 세 검사 → `INSERT … ON CONFLICT DO NOTHING` → 1행이면 부작용 실행, 0행이면 `duplicate()` → 커밋 → `HandleResult.success()` **실패:** action 예외 → `ActionFailedException` → runner가 롤백(예약도 함께) → `HandleResult.Retry("INBOX_ACTION_FAILED")` **보존:** `cleanupJob.runOnce(now)` → `policy.cutoff(now)` → 무제한 DELETE 1회 → 두 번째 호출 0 → 종료
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-inbox-jdbc-postgresql-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L329`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- classification: **SQL 실패 셋이 전부 `MessagingConfigurationException`이다.** 그 예외의 카테고리는 `CONFIGURATION`이고 `retryable = false`다. 그런데 `SQLException`의 원인은 대부분 **일시적 인프라 문제**(연결 끊김, 데드락, 타임아웃)다. 즉 재시도 가능한 실패가 재시도 불가로 분류된다. §17. `INBOX_ACTION_FAILED`만 `TRANSIENT_INFRASTRUCTURE`/`retryable = true`이고 예외가 아니라 `HandleResult`로 흐른다 — 분류가 정확하다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-inbox-jdbc-postgresql-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L346`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- classification: **이 leaf의 주제 자체가 트랜잭션이다.** `DataSourceUtils.getConnection`은 활성 트랜잭션에 묶인 커넥션이 있으면 그것을 주고, 없으면 새로 연다. 그래서 `requireActiveTransaction`이 **먼저** 도는 것이 필수다 — 없으면 새 커넥션이 열리고 자동 커밋된다. 그것이 §4.1의 이전 결함이다. `isProcessed`와 두 `purge*`는 `dataSource.getConnection()`을 직접 쓴다 — 트랜잭션에 참여하지 않는다. javadoc이 그것을 명시한다("The no-argument overload is provided only for retention sweeps and read-only queries"). 동시성 원시 요소는 DB에 있다. Java 쪽에 락이나 원자 변수가 없다. 수명주기 참여 없음 — `InboxCleanupJob`을 스케줄링하는 것은 starter다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 커밋은 연속 워터마크로만 전진한다
|
||
- slug: `messaging-kafka-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka.md#L87`
|
||
- owning-module: `messaging-kafka`
|
||
- classification: 그 대가도 적혀 있다 — 느린 메시지 하나가 그 파티션의 워터마크를 붙든다. 그것이 옳은 교환이라는 근거는 대안이 메시지를 잃는다는 것이고, 지연은 소비자 랙으로 보인다는 것이다. 그리고 등록만 되고 제출되지 않은 오프셋을 되돌리는 경로가 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-kafka-share-experimental-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L131`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- classification: 다섯 필드. 생성자가 `shareGroup` 공백과 `maxDeliveryCount < 1`을 거절한다. `maxDeliveryCount`가 javadoc에서 "how many times a record may be re-acquired before it is released"라고 정의된다 — Share Group의 재획득 한계다. **이 필드를 읽는 코드가 이 leaf에 없다.** validator도 registrar도 쓰지 않는다. **두 거절의 예외 타입이 다르다.** 첫째는 `MessagingCapabilityUnavailableException`(카테고리 `CONFIGURATION`, 안정 코드 있음), 둘째는 `IllegalArgumentException`(코드 없음). 둘 다 설정 오류인데 하나만 플랫폼 실패 어휘를 쓴다. §17. 에러 메시지가 **프로퍼티 키를 직접 적는다** — `backend.messaging.experimental.kafka-share=true`. 그 키를 읽는 코드가 이 저장소에 없다(§12.4). `spec`은 **null 검사만 받는다.** `ShareRegistration`은 `profile`과 `AtomicBoolean active` 둘만 갖는다. `TransportConsumerSpec`은 `(DestinationProfile profile, Function<TransportDelivery, CompletionStage<Void>> sink)`이고, `sink`가 플랫폼이 전달마다 부르는 콜백이다(`messaging-transport-spi` §4.5). 그 sink가 저장되지 않으므로 **어떤 메시지도 전달되지 않는다.** Kafka 소비자도 만들어지지 않는다 — `kafka-clients`를 import하는 코드가 없다. 즉 `register(...)`는 **아무것도 등록하지 않고** `isActive() == true`인 객체를 반환한다. §17. registrar javadoc이 이유를 적는다. **예외를 던지지 않고 실패한 `CompletionStage`를 반환한다** — `TransportConsumerRegistration.pause`의 반환 타입이 `CompletionSta…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-kafka-share-experimental-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L258`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- classification: `ShareRegistration.active`가 `AtomicBoolean`이다. `close()`가 `set(false)`이고 CAS가 아니므로 두 번 닫아도 무해하다(멱등). `KafkaShareProfileValidator`·`KafkaShareWorkQueueCapability`는 상태가 없다. `KafkaShareGroupRegistrar`는 validator 참조 하나만 갖는다. 수명주기 참여 없음 — `TransportConsumerRegistration`이 `AutoCloseable`이지만 이 구현은 닫을 자원을 갖지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-observability-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L52`
|
||
- owning-module: `messaging-observability`
|
||
- classification: 이 leaf는 **"메시징이 무엇을 밖으로 내보내도 되는가"**를 소유한다. 메트릭·추적·감사 셋이 여기 있고, 셋 다 같은 제약 아래 있다 — **경계가 알려진 값만 나간다.** Micrometer를 `api`로 선언한 이유가 build.gradle에 있다. `src/messaging/CLAUDE.md:40-43`의 vendor `api` 게이트를 통과한다. 다만 `MessagingObservation` 인터페이스 자체는 Micrometer를 모른다 — 벤더는 `MessagingMetrics` 한 클래스에만 나타난다. 즉 **seam은 중립이고 구현만 벤더에 묶인다.** 의존이 `messaging-core-api` 하나뿐인 것도 의도적이다. `MessagingTracer`가 `TraceContext`·`MessageHeaders`를 쓰고 `DefaultMessagingObservationConvention`이 `PublishCompletion`·`FailureCategory`를 쓴다. policy나 transport는 필요 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-observability-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L363`
|
||
- owning-module: `messaging-observability`
|
||
- classification: 트랜잭션 없음. 이 leaf는 messaging family에서 `messaging-transport-spi` 다음으로 동시성이 조밀하다. `MessagingRedactor`·`MessagingTracer`·`DefaultMessagingObservationConvention`은 상태가 없다. `MessagingTags`는 불변 record다. **`synchronized(values)`가 `Set` 인스턴스를 락으로 쓴다.** 그 `Set`은 `ConcurrentHashMap.newKeySet()`이고 외부에 노출되지 않으므로(`observed` 맵이 private) 외부 락 경합은 없다. 차원별로 락이 분리되는 효과도 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-observability-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L597`
|
||
- owning-module: `messaging-observability`
|
||
- classification: 세 번째가 가장 무겁다 — **경계가 있었는데 기본 차원만 보호했고 진단 값은 그 밖이었다.** 현재는 값이 태그가 되지 않고 키만 별도 guard 차원(`"diagnostic"`)을 통과한다. 첫 두 개는 같은 주제의 두 형태다 — **경계는 예산을 정확히 소비할 때만 경계다.** `messaging-policy`의 슬롯 누수 방지, `messaging-transport-spi`의 `endWork` clamp와 같은 계열이고 각 leaf §13이 소유한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-outbox-jdbc-postgresql-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L486`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql`
|
||
- classification: **두 가지 커넥션 획득 방식이 공존한다.** 릴레이 연산이 비즈니스 트랜잭션에 합류하면 안 되므로 `withConnection` 의 선택은 타당하다. 다만 그 판단이 주석으로 남아 있지 않고, 같은 리프의 저널은 반대 방식을 쓴다. §17 P3. **동시성 제어는 전부 데이터베이스에 있다.** `FOR UPDATE SKIP LOCKED`(청구), 서버측 토큰 증가, 펜싱 술어, `ON CONFLICT DO NOTHING`, 복합 기본키. Java 쪽에 락이 없다. **수명주기**: `OutboxRelayWorker` 는 데몬 스레드 1개, `setExecuteExistingDelayedTasksAfterShutdownPolicy(false)`, `start()` 멱등, `stop(deadline)` 드레인 후 실패 시 `shutdownNow()`. 셋 다 근거 주석이 있다(`:79-88`, `:92`, `:121-122`).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-policy-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L56`
|
||
- owning-module: `messaging-policy`
|
||
- classification: 이 leaf는 **"이 목적지는 무엇을 약속하는가"**를 소유한다. 브로커를 만지지 않고 벤더 의존성이 0이며, 대신 브로커 어댑터가 따라야 할 판단을 미리 계산한다. 경계 규칙 하나가 leaf 전체를 관통한다: **모순은 부팅 실패여야 한다.** 두 번째 경계는 **물리 주소의 격리**다. `messaging-core-api`의 `DestinationName`이 `:`과 `/`를 정규식으로 막고(그쪽 §4), 이 leaf가 물리 주소를 독점한다. 두 leaf가 같은 경계를 양쪽에서 지킨다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-policy-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L135`
|
||
- owning-module: `messaging-policy`
|
||
- classification: 프로파일 하나에 대해 순서대로 검사한다. 11번과 12번이 짝이다 — 전자는 목적지 수준 동시성, 후자는 순서 단위 안 동시성. 둘 다 있어야 "순서 보장"이 실제로 성립한다. 이 leaf에서 가장 정교한 판단이다. `Edge` enum이 `RETRY`와 `DEAD_LETTER` 둘을 갖고, `walk`가 두 간선을 동시에 따라간다. **`onPath`가 전역 방문 집합이 아니라 현재 경로다.** 각 분기마다 `new LinkedHashSet<>(onPath)`로 복사하므로 형제 분기가 서로의 방문 기록을 오염시키지 않는다. 다이아몬드(A→C, B→C)는 사이클이 아니고, 그것을 사이클로 판정하면 정상 구성이 부팅에 실패한다. 테스트가 두 경우를 각각 붙든다 — `aMixedEdgeCycleIsRejected`(retry/DLQ 교대 사이클 거절)와 `aSharedDeadLetterIsNotACycle`(다이아몬드 허용). 미등록 목적지도 여기서 잡힌다 — `anUnregisteredRetryDestinationIsRejected`. **비용 주의.** 매 분기마다 `onPath`와 `path`를 복사하므로 시간·공간이 경로 수에 지수적이다. 목적지 수가 수십 개인 정상 구성에서는 문제가 없지만, 이 성질이 어디에도 기록되지 않았다 — §17의 P3. `admit`의 실제 순서: 1. `payloadGuard.checkPayload` → 초과면 `MessageTooLargeException` 2. `acceptingNewWork` 확인 → 종료 중이면 `MessageBackpressureException("SHUTTING_DOWN")` 3. `reserve(destination)` — 목적지별 CAS 루프 → 초과면 `DESTINATION_IN_FLIGHT_LIMIT_EXCEEDED` 4. `limiter.tryAcquire()` — 프로세스 전역 semaphore, 유한 대기 → 실패면 목적지 슬롯 **반납 후** `IN_FLIGHT_LIMIT_EXCEEDED` **두 개의 천장이 있는 이유**도 명시돼 있다. **거절이 모호하지 않은 것이 설계의 핵심**이다 — "Both refusals happen before transmissi…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-policy-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L451`
|
||
- owning-module: `messaging-policy`
|
||
- classification: 동시성 지점은 `MessagingAdmissionController`와 `InFlightLimiter` 둘이다. `reserve`의 CAS 루프는 `AtomicInteger.updateAndGet`으로 쓸 수 있었지만 조건부 실패(`return false`)가 필요해서 직접 루프를 돈다. `release`에 **미세한 경합**이 있다. `getAndUpdate`로 감소한 뒤 `computeIfPresent`로 0인 항목을 제거하는데, 그 사이에 다른 스레드가 `computeIfAbsent`로 같은 키를 만들고 증가시킬 수 있다. 그러면 `computeIfPresent`의 람다가 `value.get() == 0`을 보지 못해 제거하지 않는다 — 안전한 방향의 경합이다(누수가 아니라 제거 실패). 반대 순서였다면 살아 있는 카운터를 지울 수 있었다. `DefaultRetryDecisionEngine`·`BackoffCalculator`·`DeadLetterOrchestrator`·`DeadLetterEnvelopeFactory`·`DestinationProfileValidator`는 전부 상태가 없거나 불변이다. `BackoffCalculator`의 기본 생성자가 `ThreadLocalRandom`을 쓰므로 스레드 안전하다. 수명주기 참여는 `stopAcceptingNewWork()` 하나이고, `MessagingShutdownLifecycle`(starter)이 종료 1단계에서 부른다(`messaging-transport-spi` §12.1 참조).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-policy-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L700`
|
||
- owning-module: `messaging-policy`
|
||
- classification: 이 leaf의 주석은 이전 결함보다 **왜 이 형태여야 하는가**를 더 많이 적는다. 그중 이전 상태를 직접 서술하는 것은 셋이다. 세 번째와 다섯 번째가 같은 형태다 — **반납이 획득보다 많으면 제한이 사라진다.** `messaging-transport-spi`의 `GracefulShutdownCoordinator.endWork` clamp와 `DefaultMessagingRuntimeRegistry`의 "정확히 한 번 close"도 같은 계열이고, 그 leaf §13이 소유한다. 저장소 전체에서 반복되는 주제다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-reliability-api-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L51`
|
||
- owning-module: `messaging-reliability-api`
|
||
- classification: 이 leaf는 **effectively-once 처리의 계약**을 소유한다. 구현이 없다 — 13개 중 인터페이스 5개, record 5개, enum 3개이고 실행 가능한 로직은 record 생성자 검증과 `isExpired`/`expiredAt` 술어 정도다. 벤더 의존성 0, 저장소 기술 중립이다. 세 개의 독립적인 메커니즘을 담는다. **Outbox** — dual-write 문제의 답. **Inbox** — 소비 측 중복 제거. **Claim Check** — 브로커 밖 payload 참조. 그리고 셋의 관계를 `OutboxRecord`가 명시한다. **Outbox 하나로는 부족하다는 것을 타입의 javadoc이 직접 말한다.** 이 저장소에서 반복되는 "보장을 과대 진술하지 않는다"의 예다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-reliability-api-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L92`
|
||
- owning-module: `messaging-reliability-api`
|
||
- classification: 들어오는 것: `messaging-core-api`(api) 하나. 나가는 것: `messaging-outbox-jdbc-postgresql`, `messaging-inbox-jdbc-postgresql`, `messaging-claim-check`, `messaging-spring-boot-starter`. **구현 leaf가 셋 있고 전부 배선된다.** `ReliableMessagePublisher`는 구현도 소비자도 0이다(§12.1). Outbox에 행을 쓰는 애플리케이션 측 진입점인데, 그 진입점이 없다. 이 leaf 자체는 Spring 주석을 갖지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-reliability-api-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L143`
|
||
- owning-module: `messaging-reliability-api`
|
||
- classification: 이 leaf에서 가장 중요한 안전 장치이고, 이전 결함이 javadoc에 통째로 있다. `token < 1`을 거절하는 이유도 적혀 있다 — `"a claim's token starts at 1; 0 is the value of a row nobody has claimed"`. `expiredAt(now)`가 `!now.isBefore(expiresAt)`다. 두 값이고 `STALE_LEASE`의 javadoc이 운영 의미까지 적는다. **"belongs on a metric"** — 그 메트릭이 존재하는지는 outbox leaf가 답한다. `PENDING` → `IN_FLIGHT` → `PUBLISHED` / `AMBIGUOUS` / `FAILED` / `EXHAUSTED`. **두 쌍의 구분이 각각 이유를 갖는다.** `AMBIGUOUS` vs `FAILED`: `EXHAUSTED` vs `FAILED`: `OutboxRepository.markExhausted`의 javadoc이 같은 말을 반복한다 — "The first needs a fix, the second a redrive." **`FAILED`의 의미가 애플리케이션 쪽 동명 enum과 반대다.** `CleanArchitectureTest.APPLICATION_DOES_NOT_DEPEND_ON_THE_MESSAGING_PLATFORM`의 `.because(...)`가 그것을 ArchUnit 규칙의 근거로 든다 — "its `OutboxStatus.FAILED` means the opposite of the legacy `OutboxEventStatus.FAILED`, so the two models cannot be mixed by name without inverting retryable and terminal." 즉 **이 enum의 의미가 저장소 규칙 하나의 존재 이유다.** `safeToSettle` 플래그가 상수에 붙어 있다. 세 번째의 javadoc이 결론을 적는다 — "Do *not* settle. The other transaction may still roll back, and this delivery is the only remaining copy tha…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-reliability-api-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L338`
|
||
- owning-module: `messaging-reliability-api`
|
||
- classification: **Outbox 쓰기:** 애플리케이션 트랜잭션 안에서 `ReliableMessagePublisher.addToOutbox(...)` → `OutboxRepository.append(record)` — **진입점 구현이 없다**(§12.1) **Outbox 릴레이:** `claimBatch(owner, size, lease, now, maxAttempts)` → `List<OutboxLease>` → 각 lease에 대해 발행 → 결과에 따라 `markPublished`/`markAmbiguous`/`markExhausted`/`markFailed`(lease 기반) → `APPLIED`면 정상, `STALE_LEASE`면 다른 릴레이가 가져감 **Inbox:** `handleOnce(consumerName, delivery, action)` → 한 트랜잭션 안에서 `reserve(messageId, consumerId, now)` → true면 `action.apply(delivery)` → 커밋
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-reliability-api-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L348`
|
||
- owning-module: `messaging-reliability-api`
|
||
- classification: **이 leaf는 `MessagingException`을 하나도 던지지 않는다.** 실패를 상태와 반환값으로 표현한다. `IllegalArgumentException`을 던지는 곳은 record 생성자 여섯이다 — 전부 호출자의 프로그래밍 오류다. `TransactionalMessageAction.apply`가 `throws Exception`이다 — javadoc: "rolling back both it and the inbox reservation". 즉 예외가 롤백 신호이고, 그 처리는 구현 leaf가 소유한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-reliability-api-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L366`
|
||
- owning-module: `messaging-reliability-api`
|
||
- classification: **이 leaf 전체가 트랜잭션 계약이다.** 그런데 코드에는 트랜잭션이 없다 — 전부 javadoc이 요구하는 규약이다. 마지막 하나만 타입이 강제한다. 동시성 원시 요소는 하나 — **fencing token**. 그것이 `OutboxLease.token`이고 검사는 구현의 SQL `WHERE`에 있다(§12.1). 모든 record가 불변이다. 상태를 가진 클래스가 하나도 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-reliability-api-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-reliability-api.md#L610`
|
||
- owning-module: `messaging-reliability-api`
|
||
- classification: 이 leaf의 javadoc은 **세 개의 서로 다른 결함**을 보존한다. 첫 둘이 같은 사건의 두 측면이다 — fencing token(감지 수단)과 반환값(감지 결과의 전달 수단). 둘 다 있어야 stale lease가 관측된다. 세 번째의 마지막 문장이 이 저장소에서 가장 날카로운 진술 중 하나다 — **"which makes the publish path — direct, polling or CDC — part of the message's meaning."** 전달 경로가 메시지 내용을 바꾸면 그것은 더 이상 전달이 아니다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-runtime-core-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L387`
|
||
- owning-module: `messaging-runtime-core`
|
||
- classification: **발행(조립됨):** §4.1의 8단계. **소비(미조립):** `TransportDelivery` → `handler.apply(envelope)` → `HandleResult` 4분기 → `OneShotSettlement`로 정확히 한 번 정산. **세대 설치(조립됨):** `InitializingBean` → `transport.getIfAvailable()` → null이면 조용히 반환(이유가 주석에 있음) → `new TransportMessagingRuntime(brokerName, 1L, transport)` → `runtimes.install(...)`.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-runtime-core-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L429`
|
||
- owning-module: `messaging-runtime-core`
|
||
- classification: `DefaultMessagePublisher` 자체는 불변이고 상태를 갖지 않는다 — 필드 여덟이 전부 final 협력자다. `lease`만 메서드 지역 변수이고 `handle` 람다가 `held`라는 effectively-final 복사본으로 캡처한다. 수명주기 참여는 `TransportMessagingRuntime.close()`뿐이고, 그것을 부르는 것은 registry(회전 시)와 컨텍스트 종료 두 경로다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-schema-api-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L62`
|
||
- owning-module: `messaging-schema-api`
|
||
- classification: 이 leaf는 **"바이트를 어떻게 만들고 읽는가"의 계약**을 소유한다. 실제 포맷 구현은 갖지 않는다 — 단 하나의 예외가 `RawBytesMessageCodec`이고, 그것은 포맷이 아니라 포맷의 부재를 구현한다. 경계 규칙 하나가 모든 곳에 반복된다: **codec은 닫힌 registry에 대해서만 동작한다.** `build.gradle`는 `api project(':messaging:messaging-core-api')` 하나뿐이고 vendor 의존성이 없다. 포맷별 vendor(`jackson`, `avro`, `protobuf`)는 각자 leaf가 갖는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-schema-api-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L267`
|
||
- owning-module: `messaging-schema-api`
|
||
- classification: 이 leaf가 던지는 예외는 셋이고 전부 `messaging-core-api` 소유다. `IllegalArgumentException`도 던진다 — `BoundedByteSink` 생성자의 `maxBytes < 1`, `requireFits`의 음수, `SchemaReference`의 빈 subject. 이들은 **호출자의 프로그래밍 오류**이고 메시지 실패가 아니므로 `MessagingException` 계층 밖인 것이 일관적이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-schema-api-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L282`
|
||
- owning-module: `messaging-schema-api`
|
||
- classification: 동시성: `BoundedByteSink`가 **의도적으로 thread-safe가 아니다.** javadoc이 명시한다 — "Not thread-safe, and not meant to be: an instance belongs to a single encode call." 실제로 codec들이 매 `encode` 호출마다 새로 만든다. `EncodedMessage`, `MessageContractKey`, `SchemaReference`는 불변이다. `SchemaCompatibilityValidator`는 registry 참조만 갖고 상태가 없다. `MessageCodecRegistry`/`SchemaRegistry` 구현의 스레드 안전성은 이 leaf가 규정하지 않는다 — port javadoc에 그에 대한 요구가 없다. 이것은 §17의 P3 항목이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-schema-avro-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L298`
|
||
- owning-module: `messaging-schema-avro`
|
||
- classification: `AvroMessageCodec`은 불변이다 — `schemas`가 `Map.copyOf`된 평탄 맵, `maxBytes`는 int. `BoundedByteSink`·`BinaryEncoder`·`DatumReader`·`BinaryDecoder`는 전부 호출마다 새로 만들어진다. `EncoderFactory.get()`/`DecoderFactory.get()`은 Avro의 싱글턴 팩토리이고 스레드 안전하다. 다만 `binaryDecoder(encoded, null)`의 두 번째 인자가 재사용 decoder 자리인데 항상 `null`을 넘긴다 — 재사용하지 않으므로 공유 상태가 없다. 성능을 버리고 안전을 택한 형태다. `AvroCompatibilityGate`는 상태가 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-schema-json-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L263`
|
||
- owning-module: `messaging-schema-json`
|
||
- classification: 동시성: `JacksonMessageCodec`은 불변이다 — `registry`는 `Map.copyOf`, `maxBytes`는 int, `mapper`는 빌드 후 재구성되지 않는 Jackson `ObjectMapper`(스레드 안전). `BoundedByteSink`는 매 `encode`마다 새로 만들어지므로 공유되지 않는다. `PlatformOverheadPerformanceTest.aRoundTripDoesNotAllocateAGrowingRetainedSet`이 codec이 메시지별 상태를 보유하지 않음을 간접 확인한다(메시지당 유지 메모리 64바이트 미만).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-schema-json-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L396`
|
||
- owning-module: `messaging-schema-json`
|
||
- classification: `JsonContractRegistryTest` 클래스 javadoc이 이 codec에서 만난 두 결함을 남겼다. 두 결함 다 `messaging-schema-api`가 소유하는 타입(`MessageContractKey`, `BoundedByteSink`)으로 고쳐졌다. 즉 **이 leaf에서 발견된 문제가 상위 leaf의 타입을 만들어냈다.** `MessagingCoreAutoConfiguration:420-427`의 주석은 이 codec이 아니라 publisher 조립 결함(MSG-INT-003)을 기록하는데, 같은 configuration 안에 있으므로 조립 이력의 맥락으로 참조할 가치가 있다 — "no configuration produced one … the starter did not depend on that leaf."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-schema-protobuf-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L76`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `protobuf-java:4.29.3`(api). 나가는 것: **없다.** 어떤 leaf의 `allowed_dependencies`에도 없고 starter 목록에도 없다. 런타임 배선: 없음. bean 없음(Spring 주석 0개). lockfile이 확인하는 실제 해석: 컴파일/런타임은 4.29.3, annotation processor 경로만 4.33.2다. §12.4에서 저장소 전체의 protobuf 버전 지형을 다룬다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-schema-protobuf-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L258`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- classification: `ProtobufMessageCodec`은 불변이다 — `contracts`는 `Map.copyOf`, `maxBytes`는 int. `ProtobufMessageContract`는 record이고 `Class`/`Parser` 둘 다 protobuf-java에서 스레드 안전하다. `BoundedByteSink`는 매 encode마다 새로 만들어진다. `Map.copyOf`가 여기서는 **얕은 복사 문제가 없다** — `Map<MessageContractKey, ProtobufMessageContract>`가 이미 평탄한 한 레벨이다. `AvroMessageCodec`이 중첩 맵을 받아 `flatten`이 필요했던 것과 대비된다(§`messaging-schema-avro` §4.2). 두 codec이 같은 registry 개념을 다른 형태로 받았고, 평탄한 쪽이 결함을 만들지 않았다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-security-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L52`
|
||
- owning-module: `messaging-security`
|
||
- classification: 이 leaf는 **"브로커에 연결하기 전에 무엇이 참이어야 하는가"**를 소유한다. 벤더 의존성이 0이고 브로커를 만지지 않는다 — 어댑터의 security configurer가 이 leaf의 타입을 받아 실제 클라이언트 설정을 만든다. 세 가지 원칙이 코드 전반에 반복된다. **(a) 비밀은 참조로만 다룬다.** `BrokerCredentialProfile`의 다섯 변형 전부가 `credentialId` 하나만 갖는다 — `SaslScram`, `OAuth2`, `MutualTls`, `UsernamePassword`, `Nkey`. sealed interface이므로 여섯 번째를 만들려면 이 파일을 고쳐야 한다. **(b) 타입이 통제의 일부다.** `CredentialProvider.resolve`가 `char[]`을 반환하고 `CredentialRuntime`이 그것을 참조로 보관하며 `clear()`가 `Arrays.fill(material, '\0')` 후 빈 배열로 교체한다. **(c) 역할 분리가 강제된다.** `BrokerSecurityProfile`이 producer·consumer·admin 세 자격증명을 **별도 필드**로 갖는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-security-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L141`
|
||
- owning-module: `messaging-security`
|
||
- classification: 이 leaf에서 가장 조밀한 동시성 코드이고, 이전 결함이 주석에 통째로 남아 있다. **두 개의 서로 다른 결함이 한 경합에서 나왔다.** 1. 진 쪽의 교체본이 맵에서 사라지고 `clear()`도 안 됨 → **소유자 없는 비밀이 힙에 남음** 2. 진 쪽이 이긴 쪽이 쓰고 있는 material을 `clear()`할 수 있음 → **사용 중인 자격증명이 지워짐** `ConcurrentHashMap.compute`가 해당 bin의 락을 잡으므로 fetch가 정확히 한 번 일어난다. 그리고 **`clear()`가 `replacement` 생성 후에 온다** — 주석이 그 순서의 이유를 적는다: "no reader sees a window with no usable credential — and only by the thread that replaced it, so the material a concurrent reader holds is never wiped underneath it." **대가.** `compute`의 람다 안에서 `provider.resolve(...)`가 호출된다. 즉 **외부 I/O가 맵 bin 락을 잡은 채로 일어난다.** 같은 credential id를 요청하는 다른 스레드는 그 동안 막히고, `ConcurrentHashMap` 문서는 compute 람다 안에서 같은 맵을 갱신하지 말라고 요구한다(여기서는 지켜진다). 다른 키는 다른 bin이면 막히지 않지만 해시 충돌 시 같은 bin이면 막힌다. §17. 소거 판정이 `material.length == 0`이다. 생성자가 빈 배열을 거절하므로(`"credential material must not be empty"`) 길이 0은 소거된 상태를 뜻한다 — 별도 플래그 없이 같은 필드로 상태를 표현한다. **`material` 필드가 `volatile`이 아니다.** `clear()`가 다른 스레드에서 호출되면 `material()`이 옛 참조를 볼 수 있다. 실제 경로에서는 `compute` 안에서만 `clear()`가 불리고 그 전에 `replacement`가 맵에 들어가므로 위험이 낮지만, `clearAll()`은 락 없이 순회한다. §17. `DEFAUL…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-security-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L327`
|
||
- owning-module: `messaging-security`
|
||
- classification: **자격증명 해석:** 어댑터의 security configurer → `registry.resolve(credentialId, now)` → 캐시 유효하면 반환 → 아니면 `compute` 안에서 `provider.resolve` + `provider.expiresAt` → 새 `CredentialRuntime` 설치 → 옛 것 `clear()` **시작 검증(1):** starter가 `MessageSecurityValidator` bean 생성 → `validate(profile)` 호출 지점은 starter가 소유 **시작 검증(2):** 어댑터 configurer가 `BrokerTlsPolicy.validate(profile, enabledProtocols)` 호출 **발행 권한:** `DefaultMessagePublisher` → `access.mayPublish(name)` → false면 `PublishResult(REJECTED, PUBLISH_FORBIDDEN)`
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-security-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L359`
|
||
- owning-module: `messaging-security`
|
||
- classification: 레코드 여섯(`BrokerSecurityProfile`, `BrokerCredentialProfile` 5변형, `DestinationAccessPolicy`, `BrokerAclManifest`, `CredentialRotationPlan`)은 전부 불변이다. `BrokerTlsPolicy`·`MessageSecurityValidator`·`DestinationAccessValidator`는 상태가 없거나 불변 참조만 갖는다. 수명주기 참여는 `clearAll()`뿐이고 "for shutdown"이라고 javadoc이 적는다. **그것을 부르는 코드가 저장소에 없다** — 종료 시 자격증명이 소거되지 않는다. §17.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-security-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L562`
|
||
- owning-module: `messaging-security`
|
||
- classification: 두 번째가 `messaging-schema-api` §12.3의 허용목록/거부목록 축과 같은 주제이고, 여기서는 **거부목록이 실제로 뚫린 기록**이 남아 있다. 첫 번째는 이 저장소가 반복하는 "정확히 한 번" 주제의 보안 판본이다 — `messaging-transport-spi`의 세대 close, `messaging-policy`의 permit 반납과 같은 계열이며, 여기서는 실패의 결과가 **비밀 잔류**다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-spring-cloud-stream-bridge-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L341`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- classification: 각 맵은 스레드 안전하지만 **두 맵의 갱신이 원자적이지 않다**(§4.5). 정산이나 자원 해제가 없으므로 다른 동시성 지점은 없다. `StreamBridgePolicyGuard`·`BindingProfileValidator`는 상태가 없다(`BindingProfileValidator`가 guard 인스턴스를 필드로 하나 갖지만 그것도 무상태). 수명주기 참여 없음 — `close()`나 `stop()`이 없다. 등록된 핸들러를 해제하는 방법이 없다. §17.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-testkit-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L539`
|
||
- owning-module: `messaging-testkit`
|
||
- classification: 트랜잭션 없음. 동시성 관련해서 세 가지를 확인했다. **`CertifiedEvidence.RECORDED` 는 `static final` 이며 클래스 초기화 시 1회 로드된다**(`:34`). JVM 클래스 초기화 락이 스레드 안전을 보장하고, 반환되는 `List` 는 `Stream.toList()` 결과라 불변이다. 테스트가 병렬로 돌아도 안전하다. **`BrokerFailureMatrix` 는 가변이고 동기화가 없다**(`LinkedHashMap`, `:23`). 그러나 `from(...)` 이 매번 새 인스턴스를 만들고 그 안에서만 `record(...)` 를 호출한 뒤 반환하므로, 실제 사용 패턴에서 공유되는 인스턴스가 없다. `CrossBrokerContractSuite` 는 필드 하나(`:34`)를 갖지만 JUnit5 기본 생명주기가 메서드당 인스턴스라 매 테스트가 자기 행렬을 만든다. `public BrokerFailureMatrix record(...)` 가 노출되어 있어 원리상 외부에서 공유·변형할 수 있으나, 실제 그런 호출부는 0건이다(`git grep` 상 `matrix.record` 는 `from(...)` 내부 1건). **`InMemoryMessagingHarness` 는 전부 비동기화 컬렉션**(`ArrayDeque`, `ArrayList`, `LinkedHashSet`)이고 `CompletableFuture.completedFuture(...)` 로 즉시 완료한다. 결정론이 목적이므로 옳다 — 실제 스레드 전환이 하나도 없다. 수명주기는 `MessagingAdapterHarness` 의 두 메서드에 압축되어 있다. `stopsAcceptingNewWorkDuringShutdown` 이 `beginShutdown()` 후 `isAcceptingWork()==false` 와 발행 로컬 거절 둘 다를 요구한다. 플래그만 바꾸고 발행을 계속 받는 구현은 통과하지 못한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-testkit-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L848`
|
||
- owning-module: `messaging-testkit`
|
||
- classification: 커밋 메시지는 정보가 거의 없다. 그러나 이 리프는 **코드 주석이 커밋 로그를 대신하는 드문 사례**다. 세 개의 javadoc 이 각각 "무엇이 틀렸었고 왜 지금 형태인가" 를 남겼다. 여섯 곳이 같은 결함의 여섯 얼굴이다: **자기 자신을 검증하는 상수**. 그리고 여섯 곳 모두 지금은 매니페스트를 가리킨다. `messaging-rabbit` 이 Stable 에서 Experimental 로 **강등된 흔적**도 남아 있다: `CompatibilityMatrixTest.theStableSetIsExactlyWhatALaneHasCertified` 의 `.as("RabbitMQ passes the shared contract, but no fault scenario has been run against it")`. 강등의 근거가 "계약은 통과하지만 결함 증거가 없다" 로 정확히 적혀 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-transport-spi-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L62`
|
||
- owning-module: `messaging-transport-spi`
|
||
- classification: 브로커 어댑터가 구현할 **SPI**와, 그 어댑터들의 **수명주기·세대 관리**를 소유한다. 벤더 의존성이 0이다. 가장 중요한 경계 규칙이 `MessagingTransport`의 javadoc에 있다. 13개 타입 중 어느 것도 브로커 네이티브 타입을 시그니처에 노출하지 않는다. `BrokerPosition`(core-api)이 `Map<String,String> diagnosticAttributes()`로 좌표를 문자열로만 내보내는 것과 같은 규율이다. 두 번째 경계는 **인코딩 위치**다. `TransportPublishRequest`도 대칭이다 — "The payload arrives already encoded and the profile arrives already validated, so an adapter never chooses a codec or a limit for itself. That is what keeps two adapters from disagreeing about what 'the same message' means."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-transport-spi-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L124`
|
||
- owning-module: `messaging-transport-spi`
|
||
- classification: 세 타입이 그 모델을 이룬다. 이 leaf의 유일한 실질 구현이고 동시성 설계가 조밀하다. `ConcurrentHashMap.put`이 원자적이므로 호출자는 옛 세대 또는 새 세대만 본다 — javadoc: "never a half-rebuilt connection pool". `computeIfPresent`의 리맵 함수가 **버킷 잠금 안에서** 실행되므로, 조회와 증가가 원자적이다. `get` 후 증가였다면 그 사이에 `install`이 세대를 교체해 이미 은퇴한 세대의 계수를 올릴 수 있다. `closed`가 CAS로 보호되므로 **정확히 한 번만** `runtime.close()`가 불린다. 테스트가 그것을 직접 단언한다(`aRetiredGenerationIsClosedExactlyOnce`, `as("a second close on a real connection pool throws from a shutdown hook")`). `Lease.close()`도 자체 `AtomicBoolean released`로 멱등이다 — 두 층의 멱등성이다. **세대별 은퇴 시각** 이전 결함의 기록이다. 하나의 타임스탬프를 전체 목록에 적용하면 회전이 겹칠 때 판정이 호출자가 우연히 넘긴 값에 좌우된다. **닫힌 세대의 목록 제거** `drainingCount()`가 관측 지표이므로, 이미 닫힌 세대가 목록에 남으면 지표가 영원히 0으로 안 떨어진다. **`close()`가 현재 세대까지 닫는다** 이것도 이전 결함이다. 회전 없이 종료하는 프로세스(=대부분의 프로세스)가 연결을 정리하지 않았다. **동시성 미세 결함 하나.** `close()`가 `draining`은 `synchronized`로 비우지만 `current`는 `List.copyOf(current.keySet())` 후 하나씩 `remove`한다. 그 사이에 `install`이 새 세대를 넣으면 그 세대는 닫히지 않는다. 종료 중 설치는 정상 시나리오가 아니므로 실질 위험은 낮다 — §17의 P3. `tryBeginWork`가 **이중 검사**다. 증가 후 다시 확인해서, 증가와 `beginDrain` 사이의 경합에서 계수를 되돌린다. 이 패턴이 없으면 드레인 시작 직후 …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-transport-spi-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L326`
|
||
- owning-module: `messaging-transport-spi`
|
||
- classification: **발행:** 상위(`DefaultMessagePublisher`)가 `TransportPublishRequest`를 만들어 `MessagingTransport.publish` → 어댑터가 `TransportPublishResult(PublishResult)` 반환 **수신:** 상위가 `TransportConsumerSpec(profile, sink)`로 `register` → 어댑터가 메시지마다 `sink.apply(TransportDelivery)` → 상위가 `TransportSettlement`으로 정산 **회전:** 새 `MessagingRuntime` 생성 → `registry.install(runtime, now)` → 옛 세대 `retire` → lease가 0이면 즉시 close, 아니면 `draining`에 적재 → 스케줄러가 `closeExpiredDraining(now)` 호출 **종료:** (실제 경로) `MessagingShutdownLifecycle.stop()` → `admission.stopAcceptingNewWork()` → `drain.beginDrain(now)` → 50 ms 폴링으로 `isDrained` 대기 → 마감 도달 시 중단
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-transport-spi-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L338`
|
||
- owning-module: `messaging-transport-spi`
|
||
- classification: 이 leaf가 직접 던지는 예외는 **하나**다. 나머지는 `IllegalArgumentException`(생성자 인자 검증)과 `NullPointerException`(`Objects.requireNonNull`)이다. 이 leaf가 다루는 실패의 대부분은 **예외가 아니라 상태**다 — 드레인 마감 초과는 `abandonedWorkAtDeadline(now)`가 true를 반환하는 것이고, 세대 강제 종료는 `closeExpiredDraining`의 반환 계수다. **포기가 조용하지 않다는 것이 설계다.** 마감에 도달한 작업은 정산되지 않은 채 버려지고, 브로커가 재전달한다. `GracefulShutdownCoordinator` javadoc: "rather than the platform pretending it completed."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 트랜잭션·동시성·수명주기
|
||
- slug: `messaging-transport-spi-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L352`
|
||
- owning-module: `messaging-transport-spi`
|
||
- classification: 이 leaf는 messaging family에서 **동시성 밀도가 가장 높다.** **주목할 비대칭:** `DefaultMessagingRuntimeRegistry`가 `current`는 lock-free(`ConcurrentHashMap`)로, `draining`은 `synchronized ArrayList`로 다룬다. `draining`은 회전 때만 접근하므로 경합이 없다 — 합리적 선택이지만 주석이 없다. 수명주기는 §4.4의 8단계가 **선언**이고 §12.1이 실현 상태를 다룬다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 36 — transaction-and-consistency-models
|
||
|
||
> 트랜잭션과 일관성 모델 — 멱등·아웃박스·인박스.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — 정책의 실제 적용 지점
|
||
- slug: `adapter-inbound-websocket-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/17-adapter-inbound-websocket.md#L251`
|
||
- owning-module: `adapter-inbound-websocket`
|
||
- classification: 이 sub-scope에서 실제로 요청 경로에 있는 것은 **`stomp` 패키지가 참조하는 것뿐**이다. `stomp/WebSocketInboundAuthorizationInterceptor`(53)와 `stomp/AuthenticatedHandshakeInterceptor`(34)가 `WebSocketConfig`에 등록되고, 그 둘은 `stomp/WebSocketProperties`를 쓴다. `security`(4) · `authz`(1) · `idempotency`(4) · `budget`(1)의 플랫폼 정책 타입은 `stomp`가 참조하지 않는다. 즉 **인증 프로파일 · 티켓 · origin 정책 · 메시지 권한 · 연결 예산 · 명령 멱등성이 모두 요청 경로 밖이다.**
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 스크립트와 트랜잭션 — 등록이 배포 단계이고, 창(window)은 노드에 고정된다
|
||
- slug: `adapter-outbound-cache-redis-c13`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L701`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `RedisScriptRegistry`의 규칙 — "Registration is a deployment step, not a request-time one. A script that was never registered has no digest and therefore **no way to reach the server**, which is what makes 'only reviewed scripts run' a structural property rather than a convention." 같은 identity에 다른 body를 등록하면 거부하고, 실행 시점에도 body가 등록본과 같은지 다시 본다. README가 주장하는 복구 사슬 `EVALSHA → NOSCRIPT → SCRIPT LOAD → digest verify → EVALSHA`는 **실재한다** — `LettuceRedisScriptOperations:26` javadoc이 "NOSCRIPT is the one failure retried automatically"라고 적고, `:129-133`이 `RedisNoScriptException` 또는 메시지 접두 `NOSCRIPT`를 잡아 `:100`에서 `registry.forget(script.id())`를 호출한다. 다음 호출이 `digest(...)`에서 다시 `SCRIPT LOAD`한다. `RedisTransactionRunner`는 Cluster에서의 `MULTI` 문제를 정면으로 다룬다. javadoc이 문제와 해법을 적는다 — 다른 레인은 명령마다 슬롯 소유 노드로 라우팅하는데 "that is exactly what a `MULTI` window must not do: the queued commands would be spread across nodes and none of them would be part of the same window." 해법은 연결이 아니라 **라우팅 결정**이었다 — 감시 키(또는 명시적 슬롯 태그)에서 노드를 정해 레인을 고정한다. 감시 키가 없는 Cluster 트랜잭션은 거부하고 그 이유를 적는다 — "the keys the callback will queue are not …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Capability API — 실행 기능과 지원 등급을 reportable contract로 분리
|
||
- slug: `adapter-outbound-persistence-jpa-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L126`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 현재 enum은 16개 capability id를 갖는다. app-bootstrap의 `JpaPlatformAutoConfiguration.capabilities()` 역시 16개를 선언하므로 **enum catalog와 current composition count는 일치**한다. Stable composition은 대표적으로 다음을 기본 지원으로 보고한다. transaction retry completion evidence keyset pagination runtime-role verification observability Advanced capability는 PostgreSQL native write/work claim/JSONB/array-range, bulk DML, stateless session, COPY, L2 cache, Envers 등을 constraints와 함께 보고한다. 이 분리는 “classpath에 코드가 있다”와 “현재 composition이 기본 지원한다고 약속한다”를 동일시하지 않는다. capability enum은 vocabulary이고, `CapabilitySupport`가 support level을 결합하며, app-bootstrap composition이 실제 현재 report를 구성한다. constructor가 보장하는 것은: capability non-null level non-null constraints list defensive copy 각 constraint non-null / non-blank `usableByDefault()`는 STABLE만 true다. Advanced/Experimental이 “존재하므로 기본 사용 가능”으로 오해되지 않게 support level을 코드에 남긴다. `CapabilitySupport`는 단순 문서용 record가 아니다. 실제 production 흐름은: `JpaPlatformReport`는 JDBC URL/user/password/SQL/entity catalog를 필드로 갖지 않도록 설계되어 있고, privilege detail도 boolean으로 축약한다. 즉 management endpoint의 reconnaissance surface를 …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — B. persistence-jpa public API boundary
|
||
- slug: `adapter-outbound-persistence-jpa-c10`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L763`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: input/output vocabulary: `PersistenceOperationName` `TransactionProfile` `RetryProfile` `JpaPersistenceException` `TransactionCompletionEvidence` `JpaPlatformRuntimeAutoConfiguration`은 `PlatformTransactionManager`가 있으면 `SpringJpaTransactionExecutor` bean을 만들고, 그 executor가 있으면 `FullTransactionRetryCoordinator` bean도 만든다. 따라서 source tree 수준에서는 B가 단순 historical class가 아니라 **현재 runtime bean graph에도 포함되는 구현**이다. 그러나 repository production call search에서는 `FullTransactionRetryCoordinator.execute(...)`를 실제 business/application code가 호출하는 경로가 확인되지 않았다. 반대로 application-core transaction port는 sample/use-case/composition에서 canonical contract로 사용된다. 이 공존 자체는 곧바로 defect가 아니다. `api/**`는 intended external surface이므로 fork/application이 B를 programmatically 사용할 수 있다. 문제는 문서가 두 boundary의 관계를 일관되게 설명하지 못하고, 일부 composition helper는 실제 type relationship과 다른 설명을 한다는 점이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `SpringTransactionPort`: application-core의 실제 Spring 구현
|
||
- slug: `adapter-outbound-persistence-jpa-c11`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L788`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `SpringTransactionPort`는 `PolicyTransactionPort`를 구현하며 `JpaAdapterComponentsConfig`의 narrow component scan으로 등록된다. 이 wiring은 중요하다. root `CaSkeletonApplication`은 persistence package를 broad scan에서 의도적으로 제외한다. 그래서 adapter leaf 내부의 `@Component`를 “annotation이 있으니 알아서 등록될 것”이라고 볼 수 없다. `JpaAdapterComponentsConfig` source에는 과거 실제 회귀가 기록돼 있다. persistence package를 broad scan에서 제외 `SpringTransactionPort` 같은 component를 별도 scan하지 않음 처음 transaction port가 필요한 capability가 조립될 때 unsatisfied dependency로 드러남 해결: JPA master switch 아래에서만 persistence adapter package를 narrow scan 즉 이 module에서 Spring stereotype의 존재와 runtime reachability는 별개다. current root는 `PersistenceJpaRootAutoConfiguration -> JpaAdapterComponentsConfig -> component scan` 체인을 통해 이를 해결한다. `TransactionPort` primitive는 다음으로 매핑된다. 특히 vendor default isolation에 맡기지 않고 READ_COMMITTED를 명시한다. `inRootWrite`는 REQUIRED이지만 일반 `inWrite`와 의미가 다르다. 시작 전에 `TransactionSynchronizationManager.isActualTransactionActive()`를 확인해 ambient physical transaction이 있으면 manager/action 호출 전에 거부한다. “root boundary”를 REQUIRED의 join semantics로 조용히 바꾸지 않는다. focused test는 실제…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — retry classification은 structured state로 제한한다
|
||
- slug: `adapter-outbound-persistence-jpa-c14`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L938`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `TransactionRetryClassifier`는 cause chain에서 SQLSTATE를 찾지만 automatic replay candidate는: `08007` 같은 transaction-resolution-unknown은 candidate가 아니다. `SpringPolicyTransactionPort`는 ordinary command에서 40001이 나더라도 `COMMAND_SERIALIZABLE_REPLAY_SAFE`가 아니면 retry하지 않는다. failure 종류뿐 아니라 **업무 side-effect가 replay-safe하다고 application policy가 선언했는가**가 함께 필요하다. 이것은 “DB가 retryable이라고 말하니 use case를 다시 실행”하는 구조와 다르다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — public JPA path: `SpringJpaTransactionExecutor`
|
||
- slug: `adapter-outbound-persistence-jpa-c15`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L953`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 이 executor는 한 번의 physical attempt만 담당한다. 자체 retry는 하지 않는다. attempt boundary에서 operation, attempt number, elapsed time, reconciliation key를 알고 있으므로 raw provider exception을 `JpaPersistenceException`으로 변환하는 위치로 사용된다. vendor translator가 조립되면 PostgreSQL 40001/40P01 같은 structured SQLSTATE가 stable exception으로 바뀌어 coordinator가 처리할 수 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `FullTransactionRetryCoordinator`: whole-use-case retry 의도
|
||
- slug: `adapter-outbound-persistence-jpa-c16`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L974`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: coordinator는 `JpaPersistenceException`만 catch하고, retry decision에 따라 **새 transaction / 새 persistence context에서 전체 work를 다시 호출**한다. 설계상 중요한 guard: completion unknown -> no retry irreversible side effect context -> no retry retry budget elapsed -> stop max attempts -> stop backoff interrupt -> stop retry listener는 observation only 이 모델 자체의 unit tests는 강하다. serialization/deadlock retry, exhaustion, completion unknown no-retry, interrupted sleep, irreversible side effect 등을 검증한다. 하지만 current implementation에는 public composition contract와 맞지 않는 별도 defect가 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — reconciliation record production path = 0
|
||
- slug: `adapter-outbound-persistence-jpa-c18`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1184`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `CompletionUnknownRecord`와 `CompletionUnknownRecorder`는 current production에서 자신들의 정의 외 consumer/implementation이 없다. 그런데 documentation은 훨씬 강한 계약을 선언한다. support matrix: 그리고 operator procedure는 그 record의 `transactionKey`를 사용하라고 한다. 현재 이 record를 실제로 쓰는 production channel은 확인되지 않았다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — zero-reference지만 dead가 아닌 `JpaTransactionConfig`
|
||
- slug: `adapter-outbound-persistence-jpa-c20`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1334`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 반대로 `JpaTransactionConfig`도 direct production reference는 거의 없다. 하지만 이 class는: 이고 `JpaAdapterComponentsConfig`가 transaction package를 component scan한다. 따라서 direct Java call/import가 0이어도 runtime reachability가 있다. 이 class source 자체도 historical reason을 기록한다. root `@ConfigurationPropertiesScan`에서 optional persistence tree 제외 JPA on 상태에서도 settings가 아무도 bind하지 않던 문제 발생 transaction port construction 실패 package-local configuration으로 JPA master switch 안에서만 settings enable 이 사례는 mandatory public-reachability probe가 필요한 이유를 잘 보여준다. static reference count만으로 dead code를 찾으면 Spring discovery path를 오탐한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 두 failure translator 계열은 현재 역할이 다르다
|
||
- slug: `adapter-outbound-persistence-jpa-c21`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1358`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 이 scope에는 이름이 비슷한 두 translation mechanism이 있다. 목적은 SQLSTATE/optimistic conflict를 retry/completion semantics에 필요한 stable persistence failure로 바꾸는 것이다. consumer는 adapter/application error boundary 쪽이다. 따라서 동일 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다. **transaction retry algebra와 platform operational error mapping이라는 서로 다른 output contract**를 가진다. PostgreSQL vendor translator와 exact SQLSTATE catalog correctness는 vendor sub-scope에서 계속 검증한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `failure.PersistenceExceptionTranslator`
|
||
- slug: `adapter-outbound-persistence-jpa-c22`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1384`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: consumer는 adapter/application error boundary 쪽이다. 따라서 동일 이름 영역을 다루지만 current evidence로는 competing duplicate implementation이 아니다. **transaction retry algebra와 platform operational error mapping이라는 서로 다른 output contract**를 가진다. PostgreSQL vendor translator와 exact SQLSTATE catalog correctness는 vendor sub-scope에서 계속 검증한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — runtime bean-factory-owned
|
||
- slug: `adapter-outbound-persistence-jpa-c23`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1416`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `JpaPlatformRuntimeAutoConfiguration`: `SpringJpaTransactionExecutor` — `PlatformTransactionManager`가 있을 때 `FullTransactionRetryCoordinator` — executor가 있을 때 default empty `RetryEventListener`
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — bulk DML과 StatelessSession은 일반 repository path와 다른 비용 모델을 명시한다
|
||
- slug: `adapter-outbound-persistence-jpa-c26`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1819`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `HibernateBulkDmlExecutor`는 arbitrary JPQL string을 아무 데서나 실행하는 helper가 아니다. operation name 등록 affected-row expectation persistence-context cleanup transaction requirement 를 contract로 둔다. bulk DML은 managed entity lifecycle을 우회하므로 ordinary entity save와 같은 audit/lifecycle guarantee를 기대하면 안 된다. support matrix도 이를 Advanced capability로 분리한다. 현재 production business consumer는 확인되지 않았고 PostgreSQL integration fixture에서 실제 behavior를 qualification한다. 따라서 “runtime에서 사용 중”이라고 주장하지 않는다. `HibernateStatelessSessionRunner`는 오히려 이 platform에서 transaction ownership 예외를 명시적으로 드러낸다. 일반 repository adapter: StatelessSession runner: 과거 review에서는 caller가 선언한 maxRows가 실제 affected rows와 연결되지 않는 문제가 있었다. 현재는 `StatelessWorkResult(value, affectedRows)`를 요구하고 cap 초과 시 commit 전에 rollback한다. 즉 과거의 “이름만 row cap” 문제는 현재 코드에서 수정돼 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Same-store inbox / polling outbox: 구현 계약은 강하지만 현재 미조립 candidate에 replay holes가 있다
|
||
- slug: `adapter-outbound-persistence-jpa-c37`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2734`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `PostgreSqlSameStoreInboxAdapter`와 `PostgreSqlPollingDeliveryAdapter`는 `application-core`의 owner-safe transition contract를 구현하지만, 현재 production composition에서 bean construction이나 stereotype은 확인되지 않았다. 따라서 아래 finding은 **현재 배포 기본 경로의 즉시 장애가 아니라, 이 candidate adapter를 채택할 때 활성화되는 latent defect**로 분리한다. `markProcessing()`은 같은 `START + operationId`를 발견하면 `classifyMismatch()`보다 먼저 `owner(row)`를 반환한다. 이 때문에 scope/operation id만 맞춘 forged owner로 replay하면 DB에 저장된 실제 owner token을 돌려받을 수 있다. 실제 PostgreSQL probe: 즉 duplicate handling이 owner capability recovery oracle처럼 동작한다. 채택 전에는 duplicate replay에서도 persisted owner tuple/revision과 supplied owner를 먼저 검증하도록 고쳐야 한다. `markRetryable`/`markDead`의 `retention`은 실제 SQL update에는 들어가지만 transition digest에는 들어가지 않는다. 동일 operation id로 retention만 바꾼 replay가 same-operation으로 흡수된다. retention은 terminal row 보존 기간을 결정하는 semantic argument이므로 digest에 canonical millis를 포함해야 한다. `markRetryable()`은 `nextAttemptAt`을 DB에 기록하지만 transition digest는 kind + operation + owner + errorCode만 포함한다. 재시도 시각은 delivery scheduling 자체를 바꾸는 semantic argument다. 동일 operation replay consisten…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Baseline composition을 먼저 분리해야 하는 이유
|
||
- slug: `adapter-outbound-persistence-jpa-c42`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2971`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `JpaAdapterComponentsConfig`는 adapter 전체를 넓게 scan하지 않고 다음 package만 명시적으로 component scan한다. `idempotency` `transaction` 따라서 같은 leaf 안에 있어도 reachability가 다르다. `OutboxStoreAdapter`는 baseline scan에 들어가고 `app-bootstrap`의 `OutboxConfig`가 `OutboxStorePort`로 사용한다. `DurableOperationStoreAdapter`, `JpaLiveEventReplayAdapter`는 현재 baseline component scan에 들어가지 않고 별도 production constructor/reference도 확인되지 않았다. `HibernateCacheGuard`, `HibernateEnversHistoryReader`와 Spring Data auditing candidate도 default composition에 들어가지 않는다. runtime-role verifier 자체는 app-bootstrap bean으로 구성되지만, policy를 적용하는 `requireSafe()` caller가 없다. 이 차이 때문에 아래 finding은 `production`, `conditional-production`, `latent`를 분리해 판정한다. 정적 composition snapshot은 `evidence/raw/072-baseline-capability-reachability.txt`에 남겼다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 현재 production composition은 Experimental을 실행하지 않지만 opt-in 경계는 완전히 구조적이지 않다
|
||
- slug: `adapter-outbound-persistence-jpa-c51`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3609`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 현재 repository 내부 production call graph에서는 `TenantDataSourceRegistry`, `TenantEntityManagerFactoryRegistry`, `SchemaMultiTenantConnectionProvider`, `ConsistencyAwareDataSourceRouter`, `RlsTenantSessionBinder`, `SchemaTenantMigrationOrchestrator` 등을 app-bootstrap이나 다른 production leaf가 조립하는 경로를 찾지 못했다. `backend.jpa.experimental.*` property도 production configuration에서 읽어 bean을 만드는 경로가 없고, 실제 문자열은 `ExperimentalFeature` enum의 property vocabulary에만 존재한다. 따라서 아래 semantic finding은 **현재 app-bootstrap runtime에서 즉시 활성화된 production defect가 아니라 latent experimental defect**로 분류한다. 이 구분은 중요하다. public API surface에 올라 있고 같은 artifact에 포함된 library code가 잘못된 것과, 현재 기본 애플리케이션이 그 code를 실제 실행하는 것은 다른 주장이다. 반면 structural opt-in은 완전히 닫혀 있지 않다. `PersistenceJpaConfig`의 Stable `@EntityScan`과 `@EnableJpaRepositories` 문자열 목록에는 이미 `dev.caskeleton.adapter.outbound.persistence.experimental`이 들어 있다. 현재 experimental package에는 `@Entity`, `@Repository`, `JpaRepository`, `@MappedSuperclass`가 없어서 당장 persistence unit에 들어오는 concrete JPA type은 없지만, 이후 experimental entity/repository 하나가 추가되면 별도 feature condition 없이 Stable pers…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실행 scope의 고정된 순서가 이 sub-scope의 중심이다
|
||
- slug: `adapter-outbound-persistence-mongo-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L586`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: `DefaultMongoImperativeExecutor.executeInternal(...)`은 순서를 고정한다 — collection profile 해석 → observation 개시 → consistency 바인딩 → callback 실행 → 실패 번역(최대 한 번) → observation 종료. javadoc이 이유를 적는다: "Fixing it here is what makes the invariants hold for operations nobody has written yet." 세 가지 방어가 눈에 띈다. 이미 번역된 `MongoPersistenceException`은 그대로 통과시킨다. 재번역하면 bulk partial failure나 guardrail 거절처럼 **그것을 던진 계층이 더 잘 아는** category를, driver 코드에서 유도한 일반 category로 덮어쓰게 된다. Spring이 감싼 driver 예외를 `unwrap(...)`으로 되꺼낸다. Spring의 번역은 error label을 잃는데, label이야말로 replayable transaction과 unknown commit을 가르는 값이다. `MongoCompletion.successOutcomeFor(operationType)`가 read와 write의 성공 outcome을 나눈다. 과거에는 두 executor 모두 성공을 `WRITE_CONFIRMED`로 기록해, "write가 acknowledge되고 있는가"를 답하는 지표가 read 트래픽의 함수가 됐다. `default` 분기가 `READ_CONFIRMED`로 떨어지는 것도 의도적이다 — "the honest answer is the one that claims least". `MongoCollectionProfileRegistry`가 "동적 collection 이름 금지"를 강제 가능하게 만드는 지점이다. 애플리케이션은 profile을 부르고 물리 이름은 이 registry만 안다. `ScopedAccess.collection(String)`은 요청된 collection이 scope의 것과 다르면 거부하고, `ScopedMongoOperations`의 어떤 메서드도 collect…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — transaction: framework vocabulary 대신 application semantic policy
|
||
- slug: `application-core-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L88`
|
||
- owning-module: `application-core`
|
||
- classification: `TransactionPort`는 `inWrite`, `inRootWrite`, `inRead`, `inNew` 네 개의 framework-neutral boundary를 노출한다. `PolicyTransactionPort`는 기존 surface를 깨지 않고 `TransactionRequest -> TransactionResult` 정책 기반 API를 추가한다. `TransactionPolicyId`는 Spring propagation 숫자가 아니라 `COMMAND_DEFAULT`, `COMMAND_SERIALIZABLE_REPLAY_SAFE`, `QUERY_PRIMARY`, `QUERY_REPLICA_ELIGIBLE`, `OUTBOX_APPEND`, `INBOX_AND_HANDLER`, `MAINTENANCE_NEW`처럼 application semantic ID를 노출한다. `TransactionRequest` constructor는 read policy의 consistency allowlist, non-read의 readConsistency 금지, operationId-required policy의 stable id 존재를 fail-fast한다. `TransactionResult`는 commit 결과를 다섯 상태로 분리한다. `Committed`: physical commit을 확인한 결과. `Participating`: outer transaction에 참여했지만 아직 commit을 주장할 수 없는 결과. `DeterminateRollback`: rollback이 확정된 실패. `Indeterminate`: commit 여부를 확정할 수 없는 결과. `CommittedWithPostCommitFailure`: commit은 됐지만 이후 operational cleanup이 실패한 결과. 이 algebra의 핵심은 “exception이 발생했다 = rollback”으로 단순화하지 않는 것이다. 특히 `Indeterminate`는 last observed transaction phase와 optional reconciliation reference를 보존하며, `CompletionResolution`은 `STILL_UNKNOWN`을…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 인터셉터 순서 계약
|
||
- slug: `grpc-server-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-server.md#L53`
|
||
- owning-module: `grpc-server`
|
||
- classification: 열 단계이고 선언 순서가 계약이다. 각 위치의 이유가 열거형 javadoc 에 있다. 예외 경계가 가장 바깥 — 이후 단계의 실패가 매핑되지 않은 상태로 새지 않는다 인증 → 행위자·소속 → 인가 — 각 단계가 앞 단계의 답을 필요로 한다 승인이 마감보다 먼저 — 부하 중 서버가 일을 쓰기 전에 흘려보낸다 멱등이 검증보다 먼저 — 재생된 요청이 이미 받아들인 본문을 다시 검증하지 않고 저장된 결과를 돌려준다 검증이 어댑터 직전 — 사용 사례는 믿을 수 있는 메시지를 받는다 필수가 아닌 단계는 멱등 하나다 — 상태 변경 키 메서드가 없는 서버에는 할 일이 없기 때문이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-inbox-jdbc-postgresql-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L57`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- classification: `messaging-reliability-api`의 `InboxRepository`·`IdempotentMessageHandler` 포트를 PostgreSQL로 구현한다. 이름이 기술을 드러낸다 — `docs/messaging/support-matrix.md`가 그 개명 이유를 적는다(MSG-023). **메커니즘 전체가 하나의 SQL 문장에 있다.** migration이 같은 사실을 반대편에서 적는다. `build.gradle` 주석이 테스트 전략을 명시한다. **그리고 실제로 실행된다** — `InboxPostgresIT` 6개가 기본 `test` 태스크에서 통과한다(§10).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-inbox-jdbc-postgresql-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L98`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `messaging-reliability-api`(api), `spring-jdbc`·`spring-tx`(implementation). 나가는 것: `messaging-spring-boot-starter`. **배선됨.** starter의 `MessagingReliabilityAutoConfiguration`이 셋을 만든다. `JdbcInboxRepository`는 그 목록에 없다 — `InboxRepository` bean을 누가 만드는지는 starter leaf가 답한다. Spring 타입을 두 곳에서 쓴다 — `DataSourceUtils`와 `TransactionSynchronizationManager`. 둘 다 `implementation` scope이고 public 시그니처에 나오지 않으므로 vendor `api` 규칙에 맞는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-inbox-jdbc-postgresql-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-inbox-jdbc-postgresql.md#L146`
|
||
- owning-module: `messaging-inbox-jdbc-postgresql`
|
||
- classification: 이 leaf에서 가장 중요한 안전 장치이고 이전 결함이 javadoc에 있다. **두 개의 오버로드가 있었고 호출되는 쪽이 틀린 쪽이었다.** 현재는 interface 메서드가 세 가지를 확인한다. 세 번째가 특히 정교하다 — **트랜잭션이 활성이어도 다른 DataSource에 묶여 있으면 거절한다.** 멀티 데이터소스 배포에서 실제로 발생하는 형태이고, 그 경우 예약과 부작용이 서로 다른 트랜잭션에 들어간다. 세 검사 전부 같은 코드 `INBOX_TRANSACTION_REQUIRED`를 쓴다 — 메시지만 다르다. **"the path nobody exercises before production"**가 이 leaf의 테스트 전략을 설명한다 — `InboxPostgresIT.aRolledBackTransactionLeavesNoReservationAndNoSideEffect`가 정확히 그 경로를 실 DB에서 돈다. `TransactionRunner`가 함수형 인터페이스이고 `<T> T inTransaction(Supplier<T> work)` 하나다. 즉 이 leaf는 Spring `@Transactional`에 의존하지 않고 **경계 제공을 호출자에게 위임**한다. `JdbcInboxRepository.requireActiveTransaction`이 그 위임이 지켜졌는지를 런타임에 확인한다 — **위임과 검증이 짝을 이룬다.** 중복이 정상 결과라는 것도 명시돼 있다 — "A duplicate is not an error. It is the expected consequence of at-least-once delivery, so the skip path is a normal outcome rather than an exception." 세 금지가 `messaging-reliability-api`의 `TransactionalMessageAction` javadoc이 구현자에게 요구한 것과 대칭이다 — 그쪽은 action에게, 이쪽은 handler에게. 예외 처리가 그 세 번째를 지킨다. `ActionFailedException`이 private `RuntimeException`이고, 바깥에서 잡아 `HandleResult.Retry`…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-outbox-jdbc-postgresql-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L66`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql`
|
||
- classification: **트랜잭셔널 아웃박스의 PostgreSQL 구현**이다. 비즈니스 트랜잭션이 쓰고 릴레이가 배출한다. 여기에 더해 `messaging-admin-api` 의 파괴적 작업 저널 구현도 같이 산다 — 그 이유가 build.gradle 에 적혀 있다. 이 리프의 축은 하나다: **"모르는 것을 실패로 취급하지 않는다."** 마지막 문장이 중요하다 — 이 리프가 자기 보장의 상한을 스스로 명시한다. 경계: 브로커를 모른다(`MessagePublisher` 포트만 안다). 스프링 컨텍스트를 모른다(`spring-jdbc`/`spring-tx` 는 `implementation` 이며 트랜잭션 동기화 조회에만 쓴다). 배선은 starter 몫이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-outbox-jdbc-postgresql-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L102`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql`
|
||
- classification: testcontainers 주석이 이 리프의 성격을 요약한다 — "신뢰성 패턴은 트랜잭션 경계와 유일성 제약에 대한 주장이고, 그것을 결판낼 수 있는 것은 실제 데이터베이스뿐이다." 그리고 그 레인이 **실제로 돈다**(§10). starter 가 만드는 빈(`EVD-312`): starter 가 만들지 **않는** 것: `JdbcOutboxRepository`, `OutboxEnvelopeFactory`, `JdbcAdminOperationJournal`. 셋 다 애플리케이션이 `DataSource`/`ProducerId` 를 알고 직접 등록해야 한다. `AdminOperationJournal` 의 기본값은 `InMemoryAdminOperationJournal` 이며, 프로덕션 프로파일에서는 `MessagingAdminDurabilityValidator` 가 그것을 거부한다(`analysis/messaging/messaging-admin-runtime.md` §4.4 참조).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-outbox-jdbc-postgresql-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L430`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql`
|
||
- classification: **쓰기** — 비즈니스 트랜잭션 → `append(record)` → 트랜잭션 3중 검사 → `DataSourceUtils.getConnection` → INSERT(22컬럼). **배출** — `MessagingOutboxRelayLifecycle` → `worker.start()` → `runPass()` → `relay.runOnce(now)` → 청구/발행/종결 → `scheduler.backoff(unproductive)` → 다음 패스 자기 스케줄링. **정리** — `OutboxCleanupJob.runOnce(now)` → `cutoff = now - retention` → `purgePublishedBefore(cutoff)` **무제한 오버로드** ×(최대 `maxBatches`, 실제로는 2회) → §12.1(a). **admin 저널** — `begin` → INSERT ON CONFLICT / TAKE_OVER → `checkpoint` × N → `complete` 또는 `fail`.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-outbox-jdbc-postgresql-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L767`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql`
|
||
- classification: SQL 마이그레이션과 javadoc 이 함께 이력을 이룬다. 여덟 개의 "이전에는 이랬다". 마지막 두 개(`OutboxRelay:117-123`, `OutboxRelayWorker:18-21`)가 이 저장소 전체에서 반복되는 결함 계열 — **"만들어졌지만 아무도 부르지 않는다"** — 을 명시적으로 이름 붙인 유일한 자리다. 그리고 이 리프에서는 그 둘이 실제로 고쳐졌다. §12.1(c)의 `requireExactlyOneRelay` 만 같은 상태로 남았다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 37 — delivery-and-settlement-models
|
||
|
||
> 배달과 정착 모델 — 발행·소비·재시도·배수.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.4) 문서/구현 드리프트 — 보고되는 HTTP 프로파일
|
||
- slug: `adapter-inbound-graphql-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L612`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `GraphQlPlatformConfigurationReport`(§8.1)가 `GraphQlHttpProfile.V1.name()`을 배포 상태의 일부로 보고한다. `GraphQlHttpProfile`은 autoconf=2로 참조되지만, 그 프로파일이 규정하는 전송 동작(상태 매핑 · Accept 협상 · 응답 형태)을 수행하는 코드는 미배선이다(§19.1). 그리고 그 보고서를 발행할 액추에이터 엔드포인트 자체도 등록되지 않는다(§8.1).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패: 재시도 가능성과 모호성이 배타로 강제된다
|
||
- slug: `adapter-outbound-cache-redis-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L332`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `RedisFailureMetadata`는 "Low-cardinality, payload-free description"이고, 불변식 하나가 이 SDK의 재시도 규칙 전체다. 그리고 팩토리 두 개가 그 규칙을 실제 상황에 적용한다. `notSent(...)`는 `retryable = readOperation`으로 유도한다 — 서버에 닿지 않은 읽기는 재시도해도 안전하다. `storedDataCorruption(...)`은 **일부러 `notSent`가 아니고**, javadoc이 그 이유를 적는다. 그 팩토리는 실제로 쓰인다 — `JsonEnvelopeFraming:202`, `VersionedJsonCodec:103` 두 곳(sub-scope 05 범위)이 디코딩 실패에서 호출한다. 예외 계층은 12종이고 전부 `RedisOperationException`을 상속한다. 메시지는 reason + `command=` 계열 + `mode=` + `ambiguous=`만 조립하고, javadoc이 경계를 적는다 — "keys, fields, members, values, arguments, and authentication material never appear."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — canonical digest가 길이 프레이밍이고, route token 충돌을 명시적으로 검사한다
|
||
- slug: `adapter-outbound-fileserver-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L300`
|
||
- owning-module: `adapter-outbound-fileserver`
|
||
- classification: `FilePublicationCanonicalDigests.digestOrderedValues`는 값 개수를 먼저 넣고, 값마다 **길이(4바이트) + 엄격 UTF-8 바이트**를 넣는다. 구분자를 쓰지 않으므로 값 안에 어떤 문자가 있어도 경계가 흐려지지 않는다. `FilePublishRequestFingerprint`도 같은 방식이다. `routeToken`은 정책 다이제스트의 앞 31자에 `r`을 붙인 것이라 **잘린 값**이다. 그래서 `FileserverBindingCompiler.deriveUniqueRouteTokens`가 컴파일 시점에 토큰 충돌을 검사하고, 충돌하면 두 destination 이름을 모두 담아 거부한다. 잘림이 만들 수 있는 유일한 문제를 그 자리에서 닫는다. 컴파일 후에도 `compiled.forEach`로 각 destination의 토큰이 레지스트리와 같은지 다시 확인한다. `CompiledFileDestination`의 compact 생성자는 넘겨받은 `effectivePolicyDigest`를 **다시 계산해 대조**하고, `routeToken`이 그 다이제스트에서 유도됐는지, `formatPolicyDigest`가 정본과 같은지도 확인한다. 값이 아니라 관계를 검증한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 실패를 "재시도 안전한가"로 분류한다
|
||
- slug: `adapter-outbound-fileserver-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L517`
|
||
- owning-module: `adapter-outbound-fileserver`
|
||
- classification: `AmbiguousFilesystemOperationDetector`는 `IOException`을 네 결과로 나눈다(`NOT_SENT` / `DEFINITELY_REJECTED` / `AMBIGUOUS_COMPLETION` / `RECONCILIATION_REQUIRED`). 기본값이 보수적이다 — 인식하지 못한 실패는 **변경 연산이면 ambiguous**다. javadoc이 비대칭을 적는다: "the cost of a wrong 'safe to retry' is a corrupted object, while the cost of a wrong 'ambiguous' is one reconciliation entry." `mutating` 인자로 순수 읽기는 결코 ambiguous가 되지 않게 하고, stale handle은 변경 연산일 때 `RECONCILIATION_REQUIRED`로 격상한다 — 에러만으로는 결과를 알 수 없으므로 물리 증거를 다시 읽어야 한다. 다만 `isStaleHandle`·`isLostResponse`와 `FilesystemFailureClassifier.isOutOfSpace`가 **메시지 텍스트 매칭**에 의존한다("stale file handle", "estale", "timed out", "No space left on device", "Disk quota exceeded"). 후자에는 주석이 붙어 있다 — "The JDK has no dedicated exception for this, so the reason text is the only available signal." 로케일이나 JDK 판본에 따라 문구가 달라지면 분류가 기본값으로 떨어지는데, 기본값이 보수적(변경 연산 → ambiguous)이므로 안전한 방향이다. 기록만 한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 두 발행 경로의 실패 정책이 정반대이고 그 이유가 적혀 있다
|
||
- slug: `adapter-outbound-messaging-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/12-adapter-outbound-messaging.md#L284`
|
||
- owning-module: `adapter-outbound-messaging`
|
||
- classification: `OutboundMessagePublisher.publish`에 이 저장소에서 반복해 본 종류의 수정 이력이 있다. 그리고 관측 자체가 결과를 바꾸지 못한다 — `observeQuietly`가 진단 예외를 흡수하며 "Diagnostics are non-authoritative. **An appender that is out of disk must not change what the caller believes about the broker.**" 비활성 sentinel 둘은 조용한 no-op이 아니라 `AdapterDisabledException`을 던지고, 서로 다른 클래스로 분리된 이유가 bean 조회 모호성이다(§2).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — provider가 준 `Retry-After`는 실제로 쓰이는가
|
||
- slug: `adapter-outbound-notification-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L582`
|
||
- owning-module: `adapter-outbound-notification`
|
||
- classification: `ProviderResults.retryAfter`가 파싱한 값이 종단까지 도달하는지 추적했다. 도달한다: 그리고 `RetryBackoff.delay`가 소비한다: 힌트는 계산값보다 **길 때만** 채택되고, 그 뒤 설정된 `max`(기본 5분)로 **상한이 걸린다**. javadoc의 주장 — "A provider-supplied `Retry-After` always wins over the computed value, but never over the configured maximum: a provider asking for an hour must not silently extend a delivery deadline" — 이 코드와 정확히 일치한다. 악의적 provider가 `Retry-After: 999999999`로 배달을 수십 년 뒤로 미루는 경로는 **없다**. §17.1의 `AccessContext`와 대조되는, 회로가 닫힌 사례다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Query API — pagination 비용과 trust boundary를 type shape로 제한
|
||
- slug: `adapter-outbound-persistence-jpa-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L302`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: offset/page number를 아예 표현하지 않으므로 keyset API를 사용하는 consumer가 실수로 large offset pagination으로 회귀하기 어렵다. `fetchSize()`는 요청 size + 1을 반환한다. 즉 별도 count query 없이 한 row를 더 읽어 `hasNext`를 판단하는 계약이다. hasNext=true -> nextCursor 필수 terminal slice -> nextCursor 금지 items defensive copy page number/total count가 없다는 것은 API omission이 아니라 의도된 성능 정책이다. “keyset을 쓰면서 매번 count(*)도 수행”하는 모순을 contract shape에서 제거한다. `QueryName`도 bounded registry key다. raw SQL을 metric/trace identity로 사용할 수 없다. `QueryObservation.start(QueryName)` → `QueryScope` 구조에서 scope는: failure(Throwable) 특히 `QueryScope.failure` 문서가 “throwable message를 log하지 말 것”을 직접 계약한다. Micrometer implementation이 이를 실제로 지키는지는 observation sub-scope에서 확인한다. `NoopQueryObservation`은 backend가 없을 때도 caller control flow가 갈라지지 않게 singleton no-op scope를 제공한다. app-bootstrap `JpaObservabilityAutoConfiguration`에서 actual fallback consumer가 존재한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 대부분의 optimization helper가 production에서 직접 소비되지 않는다는 사실은 이미 repository가 알고 있다
|
||
- slug: `adapter-outbound-persistence-jpa-c31`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2151`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: negative-space search에서 다음 implementation roots는 repository production consumer가 확인되지 않았다. `HibernateJpaBatchExecutor` `JpaBatchProfileRegistry` `HibernateBulkDmlExecutor` `HibernateStatelessSessionRunner` `FetchPlanApplier` `JpaKeysetQuerySupport` `JpaRepositoryFragmentSupport` `JpaStreamExecutor` `SpecificationPolicy` `QuerydslJpaSupport` 하지만 이것을 곧바로 “dead code가 대량 존재한다”라고 해석하면 안 된다. 이 repository의 기존 study/review 문서도 이미 JPA platform helper가 **구현/qualification되어 있지만 sample production path가 대부분 채택하지 않은 상태**라고 기록한다. 또한 batch/bulk/stateless helper는 real PostgreSQL integration tests에서 직접 실행된다. 따라서 현재 판단은 capability별로 나눈다. 이들은 library capability로 유지할 수 있다. `NamedStatementInspector`처럼 global Hibernate hook이 필요한 기능 이 경우는 “아무 use case가 안 쓴다”와 다르다. feature를 사용하려면 composition이 먼저 존재해야 한다. transaction scope의 `TransactionProfileRegistry`처럼 history를 통해 실제 residue로 판정해야 한다. 즉 `grep refs=0`은 finding의 시작점이지 결론이 아니다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실제 app-bootstrap consumer rule은 별도 allowlist를 다시 가진다
|
||
- slug: `adapter-outbound-persistence-jpa-c32`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2217`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `CleanArchitectureTest.BOOTSTRAP_USES_ONLY_THE_PERSISTENCE_EXPORT_SURFACE`는 또 다른 `EXPORTED` set을 정의한다. 여기에는 root composition이 vendor entry point를 import해야 하므로: 즉 두 목록은 이미 동일하지 않다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — leaf list 자체는 outside consumer를 검사하지 않는다
|
||
- slug: `adapter-outbound-persistence-jpa-c33`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L2230`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `JpaModuleBoundaryTest`의 local export test는: export package가 실제 존재하는지 새 top-level package가 governance 대상인지 를 보지만 repository의 outside consumer import를 직접 스캔하지 않는다. 실제 consumer restriction은 app-bootstrap의 별도 ArchUnit rule이 담당한다. 따라서 current architecture fitness function은: fresh architecture tests는 모두 통과했다. 이것은 현재 import graph가 각자의 rule을 만족한다는 뜻이지 **A와 B가 서로 drift하지 않는다는 증명은 아니다.** 우선순위: **P2/P3 architecture-governance hardening** 권장 방향은 exported package registry를 한 곳으로 옮기고 leaf package DAG와 consumer ArchUnit rule이 같은 데이터를 읽게 하는 것이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 이 sub-scope는 이 leaf에서 유일하게 "조립까지 된" 대형 서브시스템이다
|
||
- slug: `adapter-outbound-persistence-mongo-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1015`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: 앞선 sub-scope들과 다르다. `MongoPlatformAutoConfiguration`이 두 개의 bean을 실제로 만든다. `mongoChangeStreamSource`(209행) — `SpringReactiveChangeStreamSource`, 무조건. `reactiveMongoChangeStreamConsumer`(235행) — fork만 공급할 수 있는 5종(`MongoChangeStreamSubscription`, `MongoResumeCheckpointStore`, `MongoResumeTokenCodec`, `MongoChangeProjector`, `MongoChangeDeduplicationStore`)에 `@ConditionalOnBean`. pipeline·runner·recovery policy·invalidate recovery는 auto-configuration이 직접 `new`한다. 즉 fork가 설계가 요구하는 다섯 개를 그대로 제공하면 **완성된 소비자가 돈다**. 이 사실이 아래 §67의 심각도를 결정한다. 설계 자체는 이 leaf에서 가장 정교한 축에 속한다. **순서가 계약이다.** `MongoChangeStreamRunner`: 투영 먼저, checkpoint 나중. "Checkpointing first would mean a crash between the two loses the event permanently, with no trace." 그래서 중복을 택하고 중복을 제거한다. **claim은 3-state다.** 과거 `alreadyProjected` + `markProjected`(읽고-쓰기)는 동시에 `false`를 읽은 두 subscriber가 둘 다 투영했다 — "the deduplication that exists precisely because redelivery is guaranteed did not survive concurrency". 지금은 `CLAIMED`/`ALREADY_COMPLETED`/`BUSY`의 원자적 전이다. **빈 완료는 프로토콜 위반이다.** `Mono<Boolean>`이 empty로 완료되면 `flatMap`을 그냥 통과해 "투영도 checkpo…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — messaging과 realtime은 provider/transport vocabulary를 밖으로 밀어낸다
|
||
- slug: `application-core-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L174`
|
||
- owning-module: `application-core`
|
||
- classification: messaging application contract catalog는 contract id, logical destination, schema resource, ordering, payload/envelope bounds, sensitivity, retry/requeue horizon 등 semantic 정보만 가진다. Kafka topic/provider runtime type은 public contract에 없다. validated integration event는 partition key, schema/content hash, catalog/binding revision 같은 immutable evidence를 보존한다. strict `messagingApplicationContractQualificationTest`는 normal test source set의 세 required class를 no-skip 조건으로 실행한다. 처음 digest property 없이 실행했을 때 `prepareMessagingContractEvidence`가 fail-closed로 거부했다. current source/archive, current application-core JAR, exact profile file의 SHA-256을 공급한 재실행에서는 **15 tests, 0 skipped, BUILD SUCCESSFUL**이었다. 즉 qualification은 단순 테스트 이름이 아니라 evidence provenance property까지 요구한다. realtime contract는 durable fanout과 ephemeral fanout을 분리한다. durable은 accepted와 delivered를 동일시하지 않고 stream+position dedupe/replay를 모델링한다. stale cursor는 resnapshot 요구로 분리된다. presence는 non-authoritative이며 TTL/heartbeat failure 시 empty로 degrade할 뿐 security 판단에 사용하지 않는다. logical channel은 WebSocket/STOMP 같은 transport 명칭을 소유하지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — storage/file publication: legacy 경로와 semantic 경로가 공존한다
|
||
- slug: `application-core-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L182`
|
||
- owning-module: `application-core`
|
||
- classification: `application.storage.ObjectStoragePort`는 raw object key/whole-byte 방식의 legacy contract이며 `forRemoval` 표시가 있지만 실제 production consumer가 남아 있다. sample poster upload, adapter/config, characterization test에서 사용되므로 dead code로 분류할 수 없다. 제거 시점은 날짜가 아니라 실제 migration/zero usage로 판단하도록 문서화돼 있다. `fileexport` 역시 raw filesystem path를 반환하는 opt-in legacy capability이며 `FilesystemCsvExportAdapter`/configuration을 통해 조건부 활성화된다. 반대로 `filepublication`은 logical destination, operation/reference/version, schema, row streaming/checkpoint, durability semantic을 provider-neutral 계약으로 만든다. CSV formula injection(`=`, `+`, `-`, `@`, tab, CR)을 reject하는 정책이 테스트로 고정돼 있고, raw Path/SFTP/fileserver 타입이 receipt surface에 나오지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실제 production reachability와 legacy/dead-path 판정
|
||
- slug: `application-core-c11`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L270`
|
||
- owning-module: `application-core`
|
||
- classification: static production reference scan에서 주요 application package는 모두 외부 production consumer를 확인했다. 이 count는 “모든 type이 각각 호출된다”는 의미가 아니라 package-level runtime/repository reachability의 evidence다. 세부 파일은 `evidence/raw/013-application-core-reachability.txt`에 보존했다. legacy surface도 무조건 dead로 분류하지 않았다. `application.storage.ObjectStoragePort`, root notification `NotificationPort`, `NotificationVariablesCodecPort`, old idempotency-related exception 등은 adapter/config/characterization path에서 실제 reference가 남아 있다. 현재 상태는 dead code가 아니라 migration/compatibility surface다. 반대로 notification admin atomic `claim()`은 adapter 구현까지 존재하지만 application service consumer가 없는 **unwired corrective path**로 판정했다. 이것이 이번 scope의 가장 중요한 reachability finding이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Runtime reachability / wiring
|
||
- slug: `domain-core-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/01-domain-core.md#L146`
|
||
- owning-module: `domain-core`
|
||
- classification: `domain-core` 자체에는 Spring bean/configuration/entry point가 없다. Registry상 `app-bootstrap`, `sample-portfolio` 두 runtime composition에 membership이 있고, concrete consumers가 compile-time type/annotation으로 이 module을 참조한다. `ResourceId`: application-core messaging contract 및 sample IDs에서 참조 `IdFactory`: sample factory/use-case/identifier adapter에서 참조 `AggregateRoot`: sample aggregate에서 사용 `DomainEvent`: sample events와 websocket broadcaster qualification에서 사용 `ValueObject`: sample IDs/value objects에서 사용 따라서 major public abstraction이 완전히 dead/unwired인 상태는 아니다. 반대로 `domain-core`가 runtime service를 직접 수행한다는 근거도 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 건강 레지스트리 — 낙관에서 시작하지 않는다
|
||
- slug: `grpc-admin-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-admin.md#L50`
|
||
- owning-module: `grpc-admin`
|
||
- classification: 모든 등록 서비스가 `UNKNOWN` 에서 시작한다. 그리고 배수 중에는 `markServing`·`markNotServing` 이 무시된다. 전역 상태 계산은 세 단계다 — 임계 의존이 하나라도 불건강하면 `NOT_SERVING`, 아니면 하나라도 `NOT_SERVING` 이면 `NOT_SERVING`, 하나라도 `SERVING` 이면 `SERVING`, 그 밖에는 `UNKNOWN`.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 배수 순서
|
||
- slug: `grpc-admin-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-admin.md#L65`
|
||
- owning-module: `grpc-admin`
|
||
- classification: `beginDrain` 이 앞의 둘을 한 번에 수행하고, 그 전에 `rejectNewAdmission` 을 부르면 던진다. 조정자는 잠들지 않는다. 예산은 누적이다 — 스트림 신호 완료 판정이 `unaryDrainBudget + streamSignalBudget` 을 기준으로 한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — xDS 시작 가드
|
||
- slug: `grpc-advanced-resilience-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-resilience.md#L75`
|
||
- owning-module: `grpc-advanced-resilience`
|
||
- classification: 두 거절이 있고 javadoc 이 둘째를 더 중요하다고 적는다. 시작 차단 사유는 둘 — 능력이 사용 가능하지 않음, 그리고 애플리케이션이 재시도 정책을 함께 정의함. 부트스트랩 대조는 세 가지를 본다 — `xds_servers` 선언, 통제 평면 채널의 TLS, 프로파일의 자원 이름공간.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-admin-api-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L57`
|
||
- owning-module: `messaging-admin-api`
|
||
- classification: **되돌릴 수 없는 작업을 사람의 승인에 묶는 타입 집합**이다. 실행 코드는 하나도 없다 — 브로커를 만지는 것도, 메시지를 옮기는 것도 전부 `messaging-admin-runtime` 과 어댑터가 한다. 이 리프가 정의하는 것은 "무엇이 승인이고, 승인이 무엇을 인가하며, 인가되지 않은 것이 왜 컴파일되지 않는가" 다. 설계의 축은 하나다: **권한을 불리언이 아니라 타입으로 만든다.** 이 문단이 이 리프 전체의 요약이다. 그리고 그 위에 같은 기법이 한 층 더 쌓인다. `ReplayPlan` → `ApprovedReplayPlan` → 실행. 각 화살표가 타입 경계이고, 각 경계에서 검사가 **생성자 안에** 있어 우회 경로가 없다. 경계 밖: 이 리프는 브로커를 모른다(`DestinationTopology` 는 브로커가 보고한 값을 담는 record 일 뿐 조회하지 않는다), 저장소를 모른다(`AdminOperationJournal` 은 인터페이스), 스프링을 모른다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-admin-runtime-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L62`
|
||
- owning-module: `messaging-admin-runtime`
|
||
- classification: `messaging-admin-api` 가 정의한 타입들을 **실제로 실행하는 계층**이다. 계획을 세우고, 저널에 자리를 잡고, 옮기고, 결과를 보고한다. 1. **오케스트레이션** — `MessagingAdminService` / `DefaultMessagingAdminService`. 계획·승인·저널·실행을 잇는다. 2. **실행** — `ReplayService`, `RedriveService`. 각각 하나의 작업을 수행하며, 브로커 접촉은 SPI(`ReplayExecutor`, `RedriveSource`, `RedrivePublisher`)로 밀어낸다. 3. **토폴로지·저널** — `CompositeTopologyValidator`+`TopologyValidator`, `TopologyValidationRuntime`, `InMemoryAdminOperationJournal`. 경계 밖: 브로커 클라이언트가 없다. Kafka·Rabbit 어느 것도 import 하지 않고, 모든 브로커 접촉이 함수형 인터페이스 뒤에 있다. Spring 도 없다 — 배선은 전부 starter 몫이다. 읽고 나서 남는 인상은 두 가지로 갈린다. **개별 부품은 대단히 정교하다** — 저널의 펜싱 프로토콜, 리드라이브 루프의 per-item 경계, 토폴로지 severity 판정은 각각 실패 사례를 겪고 나온 코드로 보이며 그 근거가 주석에 있다. 반면 **부품을 잇는 층은 실행된 적이 없다** — §12.1 에서 보듯 `DefaultMessagingAdminService` 는 프로덕션에서도 테스트에서도 인스턴스화되지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-admin-runtime-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L440`
|
||
- owning-module: `messaging-admin-runtime`
|
||
- classification: **경로 A — 리드라이브 (설계상 의도된 흐름)** **경로 B — 토폴로지 검증** Stack A 는 `validateTopology()` 로 진입해 보고서를 돌려준다. 그 보고서로 `requireAcceptable()` 을 부르는 코드는 없다. Stack B 는 `validate(...)` 안에서 직접 던진다. 둘 다 프로덕션 진입점이 없다(`EVD-307`). **경로 C — 파괴적 작업** 없다. `DestructiveMessagingAdmin` 구현체가 0건이므로 `PURGE`·`OFFSET_RESET`·`DELETE_DESTINATION` 은 이 저장소에 실행 경로가 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-cloudevents-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L113`
|
||
- owning-module: `messaging-cloudevents`
|
||
- classification: **봉투 → CloudEvent** **CloudEvent → 봉투** 두 번째는 `messaging-core-api`의 `MessageEnvelope` javadoc과 정확히 짝을 이룬다 — "A null Kafka value is a tombstone, which is a distinct broker-native operation with different retention semantics." 봉투가 payload를 non-null로 강제한 이유가 여기서 실제 매핑 규칙으로 나타난다. `ProducerId`가 "deployment-independent service name, not a host, pod, or connection identity, so that it stays a bounded value safe for metric tags"라고 선언한 것과 같은 관심사다. **다만 이 방어는 완전하지 않다.** 마지막 세그먼트가 여전히 120 UTF-8 바이트를 넘거나 제어문자를 담을 수 있다. 그 경우 `ProducerId` 생성자가 `IllegalArgumentException`을 던진다 — §4.5. `urn:service:order-api` → `order-api`(테스트가 쓰는 형태). `https://a.example/very/long/path/x` → `x`. CloudEvents에는 `time` 하나뿐이므로 봉투의 두 시각(플랫폼이 봉투를 만든 때 / 사실이 일어난 때)을 구분할 수 없다. 같은 값을 넣는 것은 합리적 선택이지만 **정보 손실이 기록되지 않았다** — 왕복 후 `producedAt`은 원래 값이 아니다. 테스트의 왕복 검증(`roundTripsBackToAnEnvelopeWithoutInventingATombstone`)이 `messageId`·`messageType`·`schemaVersion`·`correlationId`·`tenantContext`·`payload`만 비교하고 `producedAt`은 비교하지 않는다. fixture에서 `producedAt`은 `09:15:01Z`, `occurredAt`은 `09:15:00Z`로 **일부러 다르게** 설정돼 있으므로, 비교했다면 실패했을 것…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-core-api-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L180`
|
||
- owning-module: `messaging-core-api`
|
||
- classification: 이 leaf의 실질은 여기 있다. **표현할 수 없는 상태를 생성자에서 거절하는 것**이 설계의 축이다. `PublishCompletion`은 boolean이 아니라 3상태다. enum javadoc이 왜 셋인지 적는다: "Collapsing 'the broker refused this' and 'we never learned what the broker did' into one failure is what produces duplicate orders"(`publish/PublishCompletion.java:6-8`). `PublishResult` 생성자(`publish/PublishResult.java:39-101`)가 거절하는 조합 12가지: 11번과 14번에는 코드 주석이 직접 달려 있다. record가 public이고 모든 adapter가 이것을 만들기 때문에 호출부를 믿지 않고 여기서 검증한다는 것도 javadoc에 적혀 있다(`PublishResult.java:18-20`). `PublishEvidence`(`publish/PublishEvidence.java`)는 `queuedLocally`, `transmission`, `brokerAccepted`, `confirmationLevel` 넷을 갖고, javadoc이 순서를 못 박는다 — "Evidence is recorded before a completion is chosen, not derived from it. That ordering is what lets an operator answer 'could the broker be holding this message?' from a stored result." `TransmissionEvidence`가 3상태(`NOT_TRANSMITTED` / `MAY_HAVE_BEEN_TRANSMITTED` / `TRANSMITTED`)인 것이 그 순서를 가능하게 한다. `SettlementResult`(`settlement/SettlementResult.java:23-36`)도 같은 형태다. `SETTLED`인데 `!brokerConfirmed` → 거절 `SETTLED`인데 `redeliveryPossible` → 거절 `!…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-core-api-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L419`
|
||
- owning-module: `messaging-core-api`
|
||
- classification: `MessagingException`(abstract) → 23개 구체 예외. 기반 타입이 `FailureDescriptor`를 갖고 `category()`·`retryable()`를 위임한다. javadoc이 목적을 적는다 — "a caller catching the base type can still classify and route the failure without matching on exception classes." **23개 중 12개가 leaf 밖에서 한 번도 참조되지 않는다**(`evidence/raw/269` §B, 12개 전부 `git grep` exit=1). §12.1에서 다룬다. `MessagingCapabilityUnavailableException` javadoc: "Downgrading replication evidence to a bare ack, or ordered delivery to unordered, produces a system that looks healthy right up to the moment the guarantee actually mattered." `MessageBackpressureException` javadoc: "Blocking the caller until a slot frees turns producer-side saturation into thread exhaustion in the calling application, which is a far worse failure than a fast rejection." 그리고 "Nothing was transmitted when this is thrown, so the message has no ambiguity."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 소비자 런타임 — 스레드 규율이 설계다
|
||
- slug: `messaging-kafka-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka.md#L69`
|
||
- owning-module: `messaging-kafka`
|
||
- classification: 공개 API 인 `pause`/`resume` 도 제어 큐를 통해 폴 스레드로 넘어가고, 반환된 단계는 **다음 폴 주기** 에 완료된다. `close()` 만 예외이고 그 예외에 근거가 붙어 있다 — 이후 폴 루프가 멈추므로 큐에 넣으면 영원히 배수되지 않는다. 이 규율은 실제로 지켜진다. 작업자 람다가 만지는 것은 `settlements`·`coordinator`·`shutdown`·`retries` 뿐이고 `consumer` 는 한 번도 없다. 통독으로 확인했다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-kafka-share-experimental-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L234`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- classification: **등록:** `registrar.register(profile, spec)` → `validator.validate(profile)` → 통과하면 `ShareRegistration(profile)` 반환 → **이후 아무 일도 일어나지 않는다** **pause:** `registration.pause(scope)` → 즉시 실패 stage 이 leaf에 메시지가 흐르는 경로가 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-kafka-share-experimental-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L244`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- classification: `MessagingCapabilityUnavailableException`의 javadoc이 이 leaf의 태도와 정확히 일치한다 — "Thrown instead of quietly degrading. Downgrading … ordered delivery to unordered, produces a system that looks healthy right up to the moment the guarantee actually mattered."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 능력 선언
|
||
- slug: `messaging-nats-experimental-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md#L82`
|
||
- owning-module: `messaging-nats-experimental`
|
||
- classification: `nativeDeadLetter=false` 의 근거가 클래스 javadoc 과 검증기 javadoc 양쪽에 있다 — 없는 큐를 찾아 나서게 만들지 않는다. `keyedOrdering=false` 도 검증기가 강제한다 — 키 순서를 요구하는 목적지를 거부한다. `deduplicatedPublish=true` 는 §17.1 이 다룬다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-observability-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L113`
|
||
- owning-module: `messaging-observability`
|
||
- classification: 여섯 차원: `broker`, `destinationProfile`, `operation`, `outcome`, `failureCategory`, `retryStage`. 없는 값은 `NONE = "none"`이다 — null도 빈 문자열도 아니고 명시적 sentinel이다. **두 factory의 차이가 §12.1의 핵심이 된다.** `asMap()`이 `LinkedHashMap`으로 순서를 고정하고 `Map.copyOf`로 불변화한다. 네 메서드와 네 상수(`PUBLISH`, `CONSUME`, `SETTLE`, `DEAD_LETTER`). `publish(...)`는 `PublishCompletion`과 `Optional<FailureCategory>`를 받아 **enum에서 문자열을 파생**한다 — 호출자가 철자를 정하지 않는다. 이 클래스는 소비자가 0이다(§12.1). 기본 상한 200/차원. **두 개의 이전 결함이 코드에 남아 있다.** 먼저 lock 없이 `values.contains(value)`로 빠른 경로를 두고, 새 값일 때만 `synchronized`로 들어가 다시 확인한다 — double-checked 패턴이다. 테스트가 경합을 직접 재현한다(`MessagingSecretLeakTest.concurrentAdmissionNeverExceedsTheLimit`). `wouldAdmit`으로 전수 사전 확인 후 `admit`으로 커밋한다. **사전 확인과 커밋 사이에 lock이 없으므로** 두 스레드가 동시에 통과할 수 있고, 그 경우 두 번째 `admit`이 false를 반환해 `admitted &= ...`가 false가 된다 — 상한은 지켜지고 결과만 거절이 된다. 안전한 방향이다. 27개 키. **두 범주**를 섞어 담는다. 두 메서드가 다른 목적을 갖는다. `isDenied`가 소문자 정규화 후 정확 일치다. **`messaging-core-api`의 `MessageHeaders.carriesACredential`은 세그먼트 매칭 + 인접 결합**(그쪽 §4.6)인데 이쪽은 정확 일치다 — 같은 저장소에서 같은 문제를 두 강도로 푼다(§12.3). `msg.id`가 목록에 리터럴로 들어 있다. `Rese…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-observability-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L334`
|
||
- owning-module: `messaging-observability`
|
||
- classification: **메트릭:** 호출자가 `MessagingTags`를 만들어 `MessagingObservation`의 다섯 메서드 중 하나를 호출 → `MessagingMetrics.admitted(tags)` → `guard.admit(tags)` → 통과하면 Micrometer `Tags`로 변환 후 미터 기록, 거절되면 `rejectedTagSets.increment()` **추적(발행):** `tracer.inject(context, headers)` → `traceparent` 없으면 그대로 반환 → 있으면 세 헤더를 `platform` factory로 추가 **추적(수신):** `tracer.extract(headers)` → `traceparent` 없으면 `TraceContext.none()` → 있으면 세 값으로 `TraceContext` 재구성(**core-api의 W3C 검증을 통과해야 함**) **감사:** 호출자가 `MessagingAuditEvent`를 만들어 sink에 `record`
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-observability-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L346`
|
||
- owning-module: `messaging-observability`
|
||
- classification: 이 leaf는 `MessagingException`을 하나도 던지지 않는다. 실패를 **값으로 표현**한다. `IllegalArgumentException`을 던지는 곳은 셋 — `CardinalityGuard` 생성자(`limitPerDimension < 1`), `MessagingMetrics.recordDelivery`(`attempt < 1`), `MessagingTracer.shouldLinkRatherThanContinue`(`batchSize < 1`), `MessagingAuditEvent` 생성자(빈 필드). 전부 호출자의 프로그래밍 오류다. **`extract`가 W3C 검증에 걸릴 수 있다.** `new TraceContext(traceparent, tracestate, baggage)`가 core-api의 정규식·바이트 상한·all-zero 검사를 돌리므로(그쪽 §4.11), 다른 시스템이 보낸 손상된 `traceparent`는 `IllegalArgumentException`이 된다. 그 예외는 `MessagingException`이 아니고 `extract`는 그것을 잡지 않는다. `messaging-cloudevents`의 id 파싱과 같은 형태다(`analysis/messaging/messaging-cloudevents.md` §17). §17.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-outbox-jdbc-postgresql-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-outbox-jdbc-postgresql.md#L177`
|
||
- owning-module: `messaging-outbox-jdbc-postgresql`
|
||
- classification: **V1** — `message_id` 를 대리키가 아니라 기본키로 삼는다. 인덱스도 근거가 있다. 부분 인덱스인 이유("PUBLISHED rows accumulate until the retention job removes them"), `IN_FLIGHT` 를 포함하는 이유("A relay that dies mid-publish leaves rows in that state ... omitting them here would strand those messages"). **V2** — 펜싱 토큰. 주석이 시나리오를 그대로 적는다. "확률을 낮추는 것과 데이터 제약으로 만드는 것은 다르다" — 이 리프에서 가장 좋은 한 줄이다. `EXHAUSTED` 상태 추가와 `next_attempt_at` 인덱스도 여기서 들어온다. **V3** — admin 저널. 복합 기본키 `(approval_ticket, plan_digest)` 의 근거가 `messaging-admin-api` 의 것과 동일하게 적혀 있다. **V4** — 정경 메타데이터 12컬럼. 왜 봉투 blob 이 아니라 컬럼인지가 명확하다. 그리고 밀반입 문제를 명시한다 — "smuggled through the header map under the reserved `msg.*` names ... a row whose header map contains `msg.id` overwrites another message's identity on the wire". DB 레벨 제약을 Java 와 이중으로 거는 이유도 적혀 있다. 마지막으로 **생성 컬럼**이 두 릴레이의 합의를 하나로 만든다. **이 수정이 배포되는 properties 파일에는 도달하지 않았다.** §12.4(a). 그리고 그것을 **강제**한다. 세 가지를 본다(`:228-245`): 활성 트랜잭션이 있는가 / 읽기 전용이 아닌가 / **이 DataSource 에 바인딩되어 있는가**. 세 번째가 특히 좋다 — 다른 DataSource 의 트랜잭션 안에서 append 하면 둘이 독립적으로 커밋된다. `append(Connection, OutboxRecord)` 가 package-private 으로 내려간 이력도 적혀 …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-policy-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L84`
|
||
- owning-module: `messaging-policy`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api). 둘 다 `api`인 이유는 `DestinationProfile`이 `DeliveryGuarantee`·`OrderingScope`·`DestinationKind`·`DestinationName`(core-api)와 `SchemaCompatibility`(schema-api)를 필드로 갖기 때문이다. 나가는 것: `messaging-transport-spi`, `messaging-runtime-core`, `messaging-kafka`, `messaging-kafka-share-experimental`, `messaging-rabbit`, `messaging-outbox-jdbc-postgresql`, `messaging-admin-api`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-cloud-stream-bridge`, `messaging-spring-boot-starter`, `messaging-testkit`. **실제 배선 지점 넷**(전부 `messaging-spring-boot-starter/MessagingCoreAutoConfiguration`): 이 leaf 자체는 Spring 주석을 갖지 않는다 — bean 정의는 전부 starter 쪽에 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-policy-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L411`
|
||
- owning-module: `messaging-policy`
|
||
- classification: **시작:** `MessagingCoreAutoConfiguration:134` → `validateAll(registered)` → 프로파일별 15검사 + 중복 이름 + 사이클 그래프 → 실패 시 `IllegalArgumentException`으로 부팅 중단 **발행:** `DefaultMessagePublisher` → `admission.admit(destination, bytes)` → 크기 → 종료 여부 → 목적지 슬롯 → 프로세스 permit → (발행) → `admission.complete(destination)` **재시도 판단:** `RetryContext(profile, deliveryMetadata, failure, capabilities, ...)` → `engine.decide(...)` → `RetryDecision` 5종 중 하나 — **이 경로는 출하 컨텍스트에서 호출되지 않는다**(§12.1) **DLQ:** `orchestrator.deadLetter(profile, delivery, failure, settlement)` → 헤더 6개 추가 → 발행 → CONFIRMED면 원본 정산 — **이 경로도 호출되지 않는다**(§12.1)
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — E. control: the publish path IS constructed in production
|
||
- slug: `messaging-policy-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L603`
|
||
- owning-module: `messaging-policy`
|
||
- classification: DefaultMessagePublisher MessagingCoreAutoConfiguration.java:446 TransportMessagingRuntime MessagingCoreAutoConfiguration.java:476 DefaultRetryDecisionEngine MessagingCoreAutoConfiguration.java:168 DeadLetterOrchestrator MessagingCoreAutoConfiguration.java:180
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — G. every file that acts on a RetryDecision variant
|
||
- slug: `messaging-policy-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L617`
|
||
- owning-module: `messaging-policy`
|
||
- classification: messaging-kafka/.../KafkaRetryExecutor.java (생성되지 않음) messaging-policy/.../DefaultRetryDecisionEngine.java (생산자) messaging-policy/.../RetryDecision.java (선언) messaging-policy/.../RetryDecisionEngineTest.java (테스트)
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 소비·정착·죽은 편지의 세 규율
|
||
- slug: `messaging-rabbit-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-rabbit.md#L91`
|
||
- owning-module: `messaging-rabbit`
|
||
- classification: **좁은 catch.** `RabbitConsumerRegistrar.onMessage` 가 디코딩만 감싸는 안쪽 `try` 를 따로 둔다. **정착하지 않은 핸들러.** 완료했는데 정착하지 않으면 대신 ack 하지 않고 requeue 한다 — "acknowledging on its behalf would silently drop it". **네이티브 죽은 편지.** `RabbitNativeDeadLetterCapability` 가 두 조건을 모두 요구한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-runtime-core-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L55`
|
||
- owning-module: `messaging-runtime-core`
|
||
- classification: **이 leaf는 조립 결함 하나를 고치기 위해 만들어졌다.** 여섯 파일 중 다섯의 javadoc이 "X was an interface with no implementation" 형태로 시작한다. `build.gradle`이 그 사정을 파일 맨 위에 적는다. 이 진단의 마지막 문장이 핵심이다 — **컨텍스트 테스트를 통과하면서 아무것도 실행하지 않는 조립**이 가능했다는 것. 이 저장소가 반복해서 만나는 형태다. six 파일이 메운 구멍: 여섯 중 다섯은 배선됐고 마지막 하나(`DefaultDeliveryProcessor`)는 배선되지 않았다(§12.1).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-runtime-core-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L86`
|
||
- owning-module: `messaging-runtime-core`
|
||
- classification: 들어오는 것: 여섯 project 의존, 전부 `api`. `DefaultMessagePublisher` 한 클래스가 그중 다섯을 생성자로 받으므로 `api`가 맞다. 나가는 것: `messaging-spring-boot-starter`만. **배선 지점 다섯**(전부 `MessagingCoreAutoConfiguration`): 446의 인자가 **여섯 개**라는 것이 §12.1의 관측 지점이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-runtime-core-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L128`
|
||
- owning-module: `messaging-runtime-core`
|
||
- classification: 실제 순서 여덟 단계: **1–7은 전부 `REJECTED`, 8만 `AMBIGUOUS`다.** 그 경계가 정확히 "바이트가 프로세스를 떠났는가"다. `messaging-core-api`의 3상태(§4.1)가 여기서 실제 분기가 된다. 그리고 `rejected(...)`가 만드는 `PublishResult`는 `PublishEvidence.notTransmitted()`를 쓰므로 `PublishResult` 생성자의 14가지 금지 조합 검증을 자연히 통과한다. **3번이 4번보다 먼저인 이유**가 인라인 주석에 있다. `remainingBudget`이 `timeout - elapsedSince(startedAt)`이고, 0 이하면 전송 전에 `REJECTED`로 끝낸다 — "Sending anyway would start a message the caller has already stopped waiting for." `.copy()`가 핵심이다. `orTimeout`을 원본에 걸면 만료가 어댑터의 stage를 완료시켜 어댑터의 자기 정리가 깨진다. 복사본에 걸면 만료는 이쪽 경로만 끝내고 어댑터는 자기 in-flight를 계속 소유한다. 그 대가도 명시돼 있다 — permit과 lease는 **복사본이 완료될 때** 반납되므로, 브로커가 나중에 응답해도 이미 반납된 상태다. 그것이 의도다("holding them until a stalled broker answers is how a rotation waits forever"). `handle`은 `whenComplete`와 달리 실패를 삼키고 값을 반환하므로 두 경우가 한 블록에서 처리된다. `lease.close()`는 `MessagingRuntimeLease` 계약상 멱등이고(`transport-spi` §4.1), `admission.complete`도 미보유 목적지에 대해 무해하다(`messaging-policy` §4.3). **한 가지 비대칭.** 6번(`admit`)이 예외를 던지면 그 예외가 그대로 호출자에게 전파된다 — `try` 블록 밖이다. 다른 모든 실패는 `PublishResult`로 정규화되는데 admission 실패만 예외다. `MessageTooLarge…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-runtime-core-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L397`
|
||
- owning-module: `messaging-runtime-core`
|
||
- classification: `DefaultMessagePublisher`가 만드는 결과: 같은 코드 `PUBLISH_DEADLINE_EXCEEDED`가 **두 completion에 쓰인다.** 전송 전이면 `REJECTED`, 후면 `AMBIGUOUS`다. 코드만 보는 대시보드는 두 경우를 구분할 수 없다 — completion을 함께 봐야 한다. §17. `sanitized(Throwable)`가 메시지가 아니라 **타입 이름만** 남긴다. `messaging-core-api`의 `FailureDescriptor` javadoc("no payload, no stack trace, no credential")과 같은 관심사다. `isDeadline`과 `sanitized` 둘 다 `CompletionException`을 한 겹 벗긴다 — 비동기 경로에서 원인이 감싸지기 때문이다. `DefaultDeliveryProcessor`는 예외를 던지지 않는다. 이중 정산만 `failedFuture`로 보고한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-schema-json-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L46`
|
||
- owning-module: `messaging-schema-json`
|
||
- classification: Stable JSON codec 하나. `MessageCodec`(schema-api)을 구현하고 Jackson 3(`tools.jackson.*` 네임스페이스)을 쓴다. javadoc이 "기본 codec으로 노출해도 안전한 이유" 셋을 명시한다. 세 번째가 `messaging-core-api`의 3상태 발행 결과와 직접 연결된다 — 크기 초과를 브로커가 아니라 여기서 잡으면 `NOT_TRANSMITTED` 증거가 붙은 `REJECTED`가 되고, 브로커에서 잡히면 `AMBIGUOUS`가 된다. 전자는 버려도 안전하고 후자는 아니다. Jackson 의존성은 `implementation`이다 — public 시그니처에 Jackson 타입이 없기 때문이다. 형제 leaf(`schema-avro`, `schema-protobuf`, `cloudevents`)는 vendor 타입이 public 시그니처에 나오므로 `api`로 선언했고 build.gradle에 그 이유를 주석으로 적었다. `src/messaging/CLAUDE.md:40-43`의 게이트가 이 구분을 강제한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-schema-json-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L246`
|
||
- owning-module: `messaging-schema-json`
|
||
- classification: 전부 `retryable = false`다 — `PERMANENT_BUSINESS`와 `DESERIALIZATION` 카테고리다. 같은 바이트를 다시 디코딩해도 같은 결과이므로 일관적이다. **진단 손실 하나.** 파서 강화가 잡는 여섯 가지(깊이, 중복 키, trailing token, 미지 필드, 문서 길이, 토큰 길이)가 전부 하나의 코드 `JSON_DECODE_FAILED`로 접힌다. 운영자는 "JSON 디코딩 실패"만 보고 원인 여섯 갈래를 구분할 수 없다. 원인 예외가 `cause`로 붙지만 `FailureDescriptor`는 `exceptionType`을 `Optional.empty()`로 둔다(`MessageSerializationException`의 3인자 생성자 경로). §17 참조.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-schema-protobuf-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L112`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- classification: 이 leaf에서 가장 밀도 높은 결정이다. 증명 방법이 영리하다. proto3에서 모든 필드가 wire상 optional이므로 **빈 바이트는 항상 유효한 메시지**다. 그것을 파싱하면 default instance가 나오고 그 클래스가 곧 parser의 산출 타입이다. 별도 리플렉션 없이 짝을 확인한다. 에러 메시지가 실패 지점을 명시한다 — "a mismatched pairing fails at decode time on a broker thread, not here". 즉 **여기서 실패하는 것이 목적**임을 메시지가 스스로 말한다. 두 코드가 다르다: `PROTOBUF_CONTRACT_UNUSABLE`(파싱 자체 실패)과 `PROTOBUF_CONTRACT_MISMATCH`(파싱은 되는데 타입이 다름). 카테고리는 둘 다 `CONFIGURATION`이다. 테스트가 이 성질을 붙든다 — `aParserThatDoesNotProduceTheDeclaredClassIsRejectedAtConstruction`, `as("the mismatch used to surface as a ClassCastException on a broker thread")`. 주석이 이유를 적는다. **세 codec 중 유일하게 사전 거절이 가능한 포맷이다.** `BoundedByteSink.requireFits`가 이 leaf를 위해 존재하고, schema-api의 javadoc이 그것을 명시한다 — "Protobuf knows its serialized size exactly, so the whole encode can be refused before the first byte is written." 그리고 사전 검사가 사후 경계를 대체하지 않는다 — `writeTo(sink)`가 여전히 sink를 통과하므로 이중 방어다. schema-api javadoc: "this is a cheaper refusal, not a replacement for the bound." `Message`인지와 등록된 클래스의 인스턴스인지를 함께 본다. 후자만으로 충분해 보이지만 전자가 `writeTo`를 부를 수 있음을 보장한다. JSON codec과 같은 비대칭이다 — e…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-security-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L339`
|
||
- owning-module: `messaging-security`
|
||
- classification: **보안 판정이 두 예외 계층으로 나뉜다.** `BrokerTlsPolicy`는 안정 코드가 붙은 `MessagingConfigurationException`을 쓰고, `MessageSecurityValidator`는 코드 없는 `IllegalArgumentException`을 쓴다. 둘이 같은 두 검사(TLS·hostname)를 공유하는데도 그렇다 — §12.3, §17.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 설정이 프로파일이 된다
|
||
- slug: `messaging-spring-boot-starter-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L111`
|
||
- owning-module: `messaging-spring-boot-starter`
|
||
- classification: `MessagingConfigurationCompiler` 가 닫는 것은 기능이 아니라 바인더의 부재다. 컴파일과 검증을 나눈 이유도 적혀 있다. 컴파일은 객체 모델이 표현할 수 없는 것만 본다 — 목적지의 브로커가 존재하는지, 사후 처리 목적지가 선언되었는지, 보안 항목이 실재하는 브로커를 지키는지. 프로파일이 자체로 정합한지는 `DestinationProfileValidator` 의 질문이고 레지스트리 전체에 대해 던져진다. 그래서 설정으로 만든 프로파일과 빈으로 선언한 프로파일이 **같은 규칙**을 받는다. 그리고 모든 거부가 키를 부른다. 타입을 부르는 오류는 운영자가 고칠 줄을 알려 주지 않기 때문이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 종료 순서가 두 수명 주기의 phase 로 표현된다
|
||
- slug: `messaging-spring-boot-starter-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L155`
|
||
- owning-module: `messaging-spring-boot-starter`
|
||
- classification: `SmartLifecycle` 은 내림차순으로 멈추므로 중계가 먼저, 승인 차단과 배수가 다음이다. 두 클래스의 javadoc 이 서로를 근거로 든다 — 중계가 발행 중일 때 승인을 닫으면 그 회차의 행이 모호해지고, 그 모호함이야말로 배수가 없애려는 것이다. 그리고 브로커 연결을 쥔 빈(`@Bean(destroyMethod = "close")` 인 생산자)은 `Lifecycle` 이 아니므로 컨텍스트가 `destroyBeans()` 에 도달할 때, 즉 두 수명 주기가 모두 끝난 뒤에 닫힌다. 순서가 맞는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-spring-cloud-stream-bridge-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L86`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- classification: **선언된 것과 쓰이는 것이 다르다.** `grep -rn 'import dev.caskeleton.messaging.transport\|import org.springframework'` → exit 1. **Spring Cloud Stream 브리지가 Spring을 import하지 않는다.** 바인더 접촉면 전체가 두 함수형 인터페이스로 추상화돼 있다 — `SpringCloudStreamPublisherBridge.ChannelSend`와 `SpringCloudStreamConsumerBridge.BridgedHandler`. javadoc이 그 목적을 적는다 — "isolated so the bridge is testable without a binder". 즉 **`spring-context` 의존은 실제 통합 코드가 있어야 필요했을 것**인데 그 코드가 없다. §12.4. 나가는 것: 없다. 어떤 leaf의 `allowed_dependencies`에도 이 leaf가 없고 starter 목록에도 없다. 런타임 배선: 없음. `runtime_memberships: []`. bean 없음. **소비자 0 · membership `[]` · 조립 0의 삼중 정합** — `messaging-kafka-share-experimental`·`messaging-schema-avro`와 같은 상태다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-spring-cloud-stream-bridge-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L309`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- classification: **검증:** `validator.validate(profile, bindingName, extendedProperties, enabled)` → guard 4검사 → 이름 → 속성 8개 → production → `BindingCapabilityReport.bridged(...)` **발행:** `bridge.bindPublisher(dest, binding)` → `bridge.publish(dest, payload, headers)` → `send.send(...)` → true면 `AMBIGUOUS`, false면 `REJECTED` **수신:** `consumerBridge.register(dest, binding, handler)` → 바인더가 `dispatch(binding, payload, headers)` → `handler.handle(...)` (예외 그대로 전파)
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-spring-cloud-stream-bridge-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L319`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- classification: **아홉 개의 구성 실패가 전부 `MessagingConfigurationException` + 안정 코드다.** 이 저장소 messaging family에서 예외 어휘가 가장 일관된 leaf다 — `messaging-security`(두 계층 혼용)·`messaging-kafka-share-experimental`(두 계층 혼용)·`messaging-policy`(검증기가 `IllegalArgumentException`)와 대비된다. 발행 결과 둘은 예외가 아니라 값이다 — `messaging-core-api`의 설계를 그대로 따른다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-testkit-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L68`
|
||
- owning-module: `messaging-testkit`
|
||
- classification: 이 리프는 **"지원한다(supported)"라는 단어의 정의를 코드로 못 박는 곳**이다. 플랫폼의 다른 어떤 리프도 "Kafka 는 Stable 이다" 를 주장하지 않는다. 그 주장은 여기에만 있고, 여기서만 검증된다. 세 개의 층으로 되어 있다. 1. **공유 계약** (`MessagingAdapterContract` + `MessagingAdapterHarness` + `ContractMessage`/`ContractAssertions`/`ObservedDelivery`/`HandleOutcome`/`FaultController`) — 브로커가 무엇이든 똑같이 답해야 하는 7가지 행동. 2. **결함 시나리오와 그 증거** (`NetworkFaultScenario` + `BrokerCertificationEvidence` + `CertifiedEvidence` + `BrokerFailureMatrix`) — 어떤 장애를 실제로 돌려 봤는가. 3. **지원 등급** (`CompatibilityMatrix`) — 위 두 층의 결과로 어댑터가 얻는 등급. 경계는 명확하다. 이 리프는 어댑터를 **구현하지 않고**, 어댑터를 **실행하지도 않는다**. 하니스 구현은 각 어댑터 리프의 `src/test` 에 있다(`KafkaContractHarness`, `RabbitContractHarness`). 이 리프가 가진 유일한 하니스는 `InMemoryMessagingHarness` 이며 `src/test` 에 있고, 그 javadoc 이 스스로 선을 긋는다. `final` + package-private + `private` 생성자 + 정적 팩토리. "프로덕션 어댑터가 되어서는 안 된다" 는 문장이 접근 제어자로도 강제되어 있다. `src/main` 이 아니라 `src/test` 에 둔 것도 같은 결정이다 — 다른 리프의 test 클래스패스에 올라가는 것은 `src/main` 뿐이므로, 이 하니스는 물리적으로 이 리프 밖으로 나갈 수 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-testkit-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L99`
|
||
- owning-module: `messaging-testkit`
|
||
- classification: 여섯 개가 전부 `api` 다. `implementation` 이 하나도 없다. 이것은 이 리프에서 옳은 선택이다: `MessagingAdapterContract` 는 `@Test` 를 **자기 시그니처에** 달고 있고(`MessagingAdapterContract.java:28`), `ContractAssertions` 는 AssertJ 를 반환 타입 없이 쓰지만 상속받는 쪽이 같은 AssertJ 를 봐야 하며, `MessagingAdapterHarness.publish` 는 `PublishResult`(core-api)를, `ContractMessage` 는 `EncodedMessage`(schema-api)를 **공개 시그니처에** 노출한다. 상속하는 쪽이 컴파일되려면 전부 전이되어야 한다. `runtime_memberships: []` 이지만 §12.1 의 판정은 다른 `[]` 리프들과 정반대다. 추가로 `messaging-kafka/build.gradle:91` 이 이 리프의 **리소스 파일 경로를 문자열로 참조**한다(§4.3). `allowed_dependencies` 4개 중 실제 import 여부:
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-transport-spi-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-transport-spi.md#L90`
|
||
- owning-module: `messaging-transport-spi`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `messaging-policy`(api). 셋 다 `api`인 이유는 세 leaf의 타입이 이 leaf의 public 시그니처에 직접 등장하기 때문이다 — `TransportPublishRequest`가 `DestinationProfile`(policy)·`MessageEnvelope`(core-api)·`EncodedMessage`(schema-api)를 필드로 갖는다. 나가는 것: `messaging-runtime-core`, `messaging-kafka`, `messaging-kafka-share-experimental`, `messaging-rabbit`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-admin-runtime`, `messaging-spring-cloud-stream-bridge`, `messaging-spring-boot-starter`, `messaging-testkit`. 런타임 편입은 starter closure를 통해서다. 이 leaf 자체는 bean을 만들지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 계약과 불변식
|
||
- slug: `shared-contract-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/02-shared-contract.md#L51`
|
||
- owning-module: `shared-contract`
|
||
- classification: `ApiErrorCode`는 code/category/httpStatus/retryable의 최소 표면을 제공하고 `OperationalError`가 registry mirror 역할을 한다. `Category`는 VALIDATION, AUTH, AUTHZ, NOT_FOUND, CONFLICT, RATE_LIMIT, TRANSIENT_DEPENDENCY, PERMANENT_DEPENDENCY, DATA_INTEGRITY, INTERNAL의 10개 값으로 고정되어 있으며 테스트가 정확한 vocabulary를 pin 한다. `OperationalErrorTest`는 단순 enum 존재보다 category × retryable 의미를 강하게 검증한다. deterministic VALIDATION/AUTHZ/NOT_FOUND는 retryable=false이고, transient INTERNAL은 기본적으로 retryable=true이되 deploy-time/configuration/terminal 상태인 일부 code는 명시적 예외로 false다. `AUTH_KID_UNKNOWN`은 key rotation 중 JWKS refresh 가능성을 이유로 AUTH 중 유일한 retryable case로 pin 되어 있다. upstream 4xx 전체를 permanent/non-retryable로 분류하면서 408/429의 의미 차이가 남는다는 점은 source comment와 README가 이미 known edge로 기록한다. `DependencyFailureException`과 `PersistenceFailureException`은 `ApiErrorCarrier`를 통해 transport adapter에 stable error code를 전달하면서 raw cause/diagnostic message를 server-side 정보로 남긴다. `AdapterDisabledException`은 carrier를 구현하지 않고 별도 mapping 대상이다. `Envelope`, `BulkEnvelope`, `ResponseMeta`, `PageMeta`, `Operation`은 framework-neutral record/factory로 API shape를 …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 38 — schema-and-wire-models
|
||
|
||
> 스키마와 와이어 모델 — 코덱·봉투·버전·마이그레이션.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 스키마 해시의 생산자와 소비자
|
||
- slug: `adapter-inbound-graphql-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L221`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `GraphQlSchemaHash`의 유일한 생산 경로는 `GraphQlSchemaAssemblyResult.schemaHash()`(`:94-95`)이고, 그 결과 타입은 `GraphQlSchemaAssembler.assemble(...)`만 만든다. 둘 다 프로덕션 호출자가 없다(§7.1). 소비 쪽은 `GraphQlPlatformActuatorEndpoint`가 생성자로 받는다. 그 클래스의 저장소 전체 참조는: `GraphQlPlatformAutoConfiguration`의 39개 `@Bean` 중 이것을 만드는 것이 없다. §8.1. *(이 파일은 `autoconfigure` 패키지에 있어 sub-scope 01의 분모에 포함된다. 스키마 해시 사슬의 소비 쪽이므로 여기서 함께 다룬다.)*
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 실행 전 실패의 매퍼
|
||
- slug: `adapter-inbound-graphql-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L604`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `GraphQlRequestErrorMapper`의 javadoc이 자기 존재 이유를 적는다: 진단이 정확하고, 배선된 것은 리졸버 쪽(`GraphQlExceptionResolver`, autoconf=4)뿐이다. 파싱·검증 실패의 와이어 형식을 이 플랫폼이 정하지 않는다는 뜻이다 — 다만 `runtime/GraphQlWireErrorMapper`(autoconf=4)와 `runtime/GraphQlPlatformRejectionMapper`(main_other=3)가 배선돼 있어 플랫폼이 거부하는 실패(익명 연산, 복잡도 초과 등)는 안정 코드로 매핑된다. 덮이지 않는 것은 graphql-java 자신이 만드는 구문/검증 오류다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 커서 서명 키의 두 소비처
|
||
- slug: `adapter-inbound-graphql-c11`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L725`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `backend.graphql.cursor.key-ids`를 읽는 프로덕션 코드는 둘이다: **서명하는 코드는 없다.** `HmacGraphQlCursorCodec`과 `GraphQlCursorKeyRing`은 autoconf=0 · main_other=0이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — 라이브러리 타입의 소비자
|
||
- slug: `adapter-inbound-web-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L843`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: 다섯 패키지 중 소비 모듈이 실제로 부르는 것은 `ETags` 하나다 — `sample-portfolio`의 `WorkLogController`가 세 곳에서 쓴다(`:144` `If-None-Match` 비교, `:213` 버전에서 약한 ETag 생성, `:226` `If-Match` 검사). 나머지는 전부 테스트 전용이다. 라이브러리이므로 그 자체가 결함은 아니지만, 페이지네이션 어휘 19개 파일·버전 관리 7개 파일이 **한 번도 컨트롤러에 붙어 본 적이 없다**는 사실은 기록해 둘 값이 있다 — 이 저장소가 다른 곳에서 "타입은 있고 호출자가 없다"를 반복해서 결함으로 취급했기 때문이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 커서 코덱도 두 벌
|
||
- slug: `adapter-inbound-web-c10`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L889`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `pagination/WebCursorCodec`(인터페이스) + `pagination/HmacWebCursorCodec`(135, 서명된 구현) + `pagination/WebCursorPayload` + `pagination/WebCursorKeyRing`, 그리고 별도로 `cursor/CursorCodec`(100) + `cursor/CursorException`. `WebStableModule`은 둘을 다른 모듈로 선언한다(`CURSOR` = "Opaque keyset cursor encoding and its failure type", `PAGINATION`). 둘 다 프로덕션 소비자가 없어 어느 쪽이 정본인지 코드로는 판정할 수 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — `OpenApiCustomizer` 가 두 개다
|
||
- slug: `adapter-inbound-web-c11`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L978`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: 배선된 것 — `config/OpenApiContractConfig`(37줄, `@Configuration`)가 익명 람다 `OpenApiCustomizer` 하나를 빈으로 등록한다. 하는 일은 `ApiError.details` 스키마를 `ObjectSchema`로 되돌리는 것 한 가지다. 배선되지 않은 것 — `openapi/WebOpenApiCustomizer`(74줄)와 그것이 쓰는 `ProblemSchemaContributor`(88) · `CursorSchemaContributor`(47) · `WebOpenApiProfile`(72) · `WebOpenApiBreakingPolicy`(187) · `WebOpenApiReleaseGate`(100) · `WebOpenApiDiffResult`(39). 합 607줄. 빈으로 등록하는 코드가 main·app-bootstrap에 없고, 참조는 자기들끼리와 테스트뿐이다. 즉 springdoc이 생성하는 문서에는 RFC 9457 problem 스키마도 커서 스키마도 기여되지 않는다 — 그 기여자들이 커스터마이저에 도달하지 않기 때문이다. SS2에서 확인한 "problem 계약이 문서에 없다"(§7.3)와 같은 방향의 사실이 스키마 쪽에서도 성립한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.4) 카운트 — `WebSocketFailureCategory`
|
||
- slug: `adapter-inbound-websocket-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/17-adapter-inbound-websocket.md#L267`
|
||
- owning-module: `adapter-inbound-websocket`
|
||
- classification: `error` 패키지의 `WebSocketFailureCategory`(main_other=10)가 이 sub-scope에서 가장 널리 참조되는 타입이고, `WebSocketErrorMessage`(75, main 참조 0)와 `WebSocketErrorTransport`가 그것을 전송으로 옮긴다. 실제 STOMP 오류는 `stomp/SafeStompSubProtocolErrorHandler`(32)가 만든다 — 세 번째 오류 형식이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 재개 토큰 서명
|
||
- slug: `adapter-inbound-websocket-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/17-adapter-inbound-websocket.md#L381`
|
||
- owning-module: `adapter-inbound-websocket`
|
||
- classification: `ResumeTokenCodec`(217) + `ResumeTokenKeyRing`(87)이 서명된 재개 토큰을 만든다. 이 조합은 graphql §24.1의 `HmacGraphQlCursorCodec` + `GraphQlCursorKeyRing`과 같은 형태다. **차이는 이쪽에는 그 키를 요구하는 시작 검증기가 없다는 것** — 즉 "키를 요구하고 서명하지 않는" 잘못된 확인 신호가 없다. 면책 목록에 replay/resume이 있으므로 문서·코드·검증이 일치한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 명령 기술: 정책 파일과 서버 메타데이터의 접합점
|
||
- slug: `adapter-outbound-cache-redis-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L350`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `RedisCommandDescriptor`는 "the join between official server metadata and organization policy"이고, 그 아래를 못박는다 — "Nothing downstream of the guard is allowed to re-derive risk, access, or timeout from a command name." 생성자가 그 접합의 모순 네 가지를 거부한다. 마지막 하나가 §23의 런타임 불변식과 같은 규칙의 **선언 시점** 짝이다 — 하나는 정책 파일이 거짓말하지 못하게 하고, 하나는 실패 객체가 거짓말하지 못하게 한다. `KeySpec`은 공식 Redis 규약(1-based, 음수 lastKey는 뒤에서부터, `movable`은 정적 유도 불가)을 그대로 따르고, `movable`이면 `resolvePositions`가 던진다 — "movable key specification must be resolved by the server". `CommandId`는 항상 대문자로 정규화해 "a policy file, a server metadata reply, and an SDK call site cannot disagree because of casing." permit 세 종은 인터페이스이고 javadoc이 경계를 명확히 한다 — "Application code may implement this interface, but a self-made instance never passes `RedisPermitVerifier`… The final enforcement boundary remains the Redis ACL account, which a permit never widens." `PersistentKeyPermit`은 한 줄 더 붙인다: "Cache, session, lock, idempotency, and rate-limit APIs never accept this permit." `OperationBudget`도 규칙을 문서로 못박는다 — "Every R2 API requires a budget. The budget is never optional and n…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 정책 문서를 일반 YAML 파서로 읽지 않는다
|
||
- slug: `adapter-outbound-cache-redis-c11`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L579`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `RedisCommandPolicyLoader`의 javadoc이 이유를 적는다. 파서는 그만큼 좁다 — 탭 금지, 들여쓰기 0/2/4만 허용, `commands:` 루트 정확히 하나, 명령 블록 중복 금지, 필드 이름 allowlist(12종) 밖이면 거부, 빈 값 거부, 필드 중복 거부. test가 `rejectsUnknownFieldsEnumsAndDuplicates`로 잡는다. `RedisCommandPolicy`/`RedisCommandDescriptor`의 교차 필드 불변식(§24)이 로딩 시점에 적용되므로, "R4인데 BLOCKED이 아닌" 정책 파일은 **읽히지 않는다**. test가 `everyDestructiveCommandIsBlockedAndUnreachable`·`deprecatedCommandNamesAreNotReachable`·`arbitraryScriptSourceExecutionIsBlocked`·`theCatalogFailsClosedForAnUnclassifiedCommand`로 그 집합을 고정한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — codec이 "canonical"을 왕복으로 강제한다
|
||
- slug: `adapter-outbound-fileserver-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L168`
|
||
- owning-module: `adapter-outbound-fileserver`
|
||
- classification: `FileserverControlRecordCodec`은 세 레코드와 receipt snapshot에 대해 **decode 직후 재encode해 바이트를 비교**한다(`requireCanonical(bytes, encodeOperation(record))`). 그래서 "파싱은 되지만 우리가 쓰지 않았을 형태"가 전부 거부된다 — 공백, 필드 재배열, `A` 같은 이스케이프, `-0`/선행 0 같은 숫자 표기, 후행 콘텐츠. 파서 자체도 좁다. 필드 집합을 **정확히 일치**시킨다(`values.keySet().equals(allowedFields)`) — 누락도 미지 필드도 거부. 중복 키를 거부한다(`putIfAbsent`). UTF-8 디코딩이 `REPORT` 모드라 malformed 바이트가 대체문자로 조용히 바뀌지 않는다. `\b \f \n \r \t` 이스케이프를 **문법 수준에서 거부**한다("control characters are forbidden") — 제어문자가 이스케이프로 밀입되는 경로를 닫는다. 짝 없는 서로게이트를 거부한다(`requireWellFormedUnicode`). `Instant.parse` 후 `result.toString().equals(value)`로 **canonical UTC 표기**만 받는다. receipt snapshot은 `rsv1.` 접두사 + unpadded base64url이고, 디코딩 후 **재인코딩 문자열 비교**로 alias(후행 비트가 0이 아닌 변형)를 거부한다. test가 그 하나하나를 이름으로 고정한다 — `canonicalDecoderRejectsWhitespaceReorderingEscapesNumbersUtf8AndTrailingContent`, `receiptSnapshotRejectsBase64urlAliasWithNonZeroTrailingBits`, `canonicalCodecRoundTripsSupplementaryUnicodeInOpaqueText`, `formulaMitigationCountCannotExceedCellsAndUsesOverflowSafeBounds`. 마지막 것은 코드에서도 확인된다. `requireFormulaCountWithinCe…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `ObjectBody`의 재생 가능성 판정 — 값의 성질이지 코덱의 성질이 아니다
|
||
- slug: `adapter-outbound-httpclient-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L237`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: 이 sub-scope에서 가장 신중한 코드다. 과거 동작과 그 결과가 적혀 있다. 지금은 `deeplyImmutable(value)`가 구조적으로 판정한다 — 문자열·숫자·불리언·문자·enum·UUID·`Temporal`은 통과, 컬렉션과 맵은 **JDK의 불변 뷰인지 이름으로 확인**하고 원소까지 재귀, record는 모든 성분을 반사로 재귀 확인, 그 외는 전부 `ONE_SHOT`. 반사가 실패하면 "A component the platform cannot inspect cannot be certified, and an uncertified body is one-shot rather than optimistically replayable." 컬렉션 판정이 이름 기반인 이유도 적혀 있다 — "`List.of(...)` and `Collections.unmodifiableList(...)` return package-private classes with no shared marker interface. An ordinary `ArrayList` the caller still holds is exactly the case this must not accept." test 넷이 네 갈래를 고정한다 — `anImmutableRecordReplays`, `aMutableValueIsOneShot`, `aRecordWrappingMutableStateIsOneShot`, `anArbitraryBeanIsOneShot`.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 두 예산, 두 계층, 그리고 읽는 도중의 강제
|
||
- slug: `adapter-outbound-httpclient-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L403`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: `ResponseSizeLimiter`가 **와이어 바이트와 디코드 바이트를 따로** 센다 — "a compressed payload passes a wire check and then expands, so a single limit either rejects legitimate traffic or lets a **decompression bomb** through." 그리고 `CountingBoundedInputStream`이 상한을 **읽는 도중에** 적용한다 — "a response that is discovered to be too large only once it is fully buffered has already cost the memory the limit exists to protect." `BoundedErrorBody`는 오류 본문을 RFC 9457 문서를 해독할 만큼만 읽고, "The bytes never reach an exception message or a log." `toString()`은 길이와 truncated 여부만 낸다. `RemoteProblemDecoder`의 규칙 한 줄이 이 계층의 성격을 요약한다 — "**The wire status wins.** A remote `status` member is read and discarded, because trusting it would let an upstream **relabel a 503 as a 400 and change our retry behaviour from its own body.**" 확장 속성도 allowlist로 걸러 "an upstream cannot inject unbounded attributes into our telemetry." test 넷이 그 갈래를 고정한다(`mapsProblemJsonWithoutTrustingBodyStatus`·`dropsExtensionsThatAreNotAllowlisted`·`treatsANonProblemContentTypeAsAnEmptyProblem`·`survivesAnUnparseableProblemDocument`).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 레지스트리가 "닫혀 있다"는 것의 의미
|
||
- slug: `adapter-outbound-messaging-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/12-adapter-outbound-messaging.md#L174`
|
||
- owning-module: `adapter-outbound-messaging`
|
||
- classification: `LocalJsonSchemaRegistry`의 한 줄 요약이 계약이다 — "Immutable, startup-compiled Draft 2020-12 registry backed **only by explicitly supplied bytes**. Every reference is checked before NetworkNT compilation. After construction this type exposes **no loader, URL, file or classpath fetch operation**." 닫힘이 네 겹으로 표현된다. 1. **어휘 allowlist** — `KNOWN_VOCABULARIES` 8종(core·applicator·unevaluated·validation·meta-data·format-annotation·format-assertion·content) 밖의 `$vocabulary` 항목은 거부된다. 2. **키워드 부분집합** — `$anchor`·`$dynamicRef`·`$dynamicAnchor`·`$recursiveRef`·`$recursiveAnchor` 다섯이 `UNSUPPORTED_CLOSED_SUBSET_KEYWORDS`로 **문서 어디에서든** 거부된다(test `rejectsDynamicRecursiveAndAnchorKeywordsEverywhereInTheClosedSubset`). 3. **참조 사전 검사** — `validateAllReferences`가 NetworkNT 컴파일 **전에** 모든 `$ref`를 확인하고, 원격 참조와 설정된 깊이를 넘는 참조 그래프를 거부한다. 4. **핀 고정된 메타스키마 권위** — 9개 Draft 2020-12 메타 문서를 리소스로 동봉하고 `authority.sha256` 매니페스트로 해시를 고정하며, 도메인 분리 상수(`ca-skeleton.messaging.draft-2020-12-authority.v1`)를 섞는다. 매니페스트는 UTF-8 디코딩을 `REPORT` 모드로 읽어 잘못된 바이트를 조용히 대체하지 않는다. **실행 probe로 매니페스트를 검증했다** — 동봉된 9개 파일의 SHA-256이 `authority.sha256…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 봉투 작성이 파서를 거치지 않는다
|
||
- slug: `adapter-outbound-messaging-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/12-adapter-outbound-messaging.md#L189`
|
||
- owning-module: `adapter-outbound-messaging`
|
||
- classification: `DeterministicEnvelopeWriter`는 페이로드를 **선언된 shape을 따라 스냅샷**한 뒤 그 정확한 바이트를 봉투에 끼워 넣는다 — "those exact trusted bytes are then embedded in the envelope **without any raw JSON parser or generator API**." `embedExactPayload`가 `,"payload":` 리터럴로 이어 붙이는 방식이다. 입력 검증이 촘촘하다 — draft의 페이로드가 **정확히 등록된 final record 클래스**여야 하고(`exactPayloadClassIsRequiredAndNoAssignableTypeSearchOccurs`), contractId와 payloadVersion이 컴파일된 계약과 같아야 하며, 레코드 성분 수·문자열 UTF-8 길이·배열/객체 크기·깊이가 모두 `EnvelopeAdmissionLimits`로 유계다. 그리고 **쓰는 도중에** 출력 크기를 본다(`boundsJsonOutputDuringWritesInsteadOfOnlyInspectingTheCompletedBuffer`). 가변 페이로드 처리도 명시적이다 — `snapshotsStatefulMutablePayloadAccessorsOnceAndEmbedsThoseExactBytes`. 접근자를 한 번만 부르고 그 바이트를 고정하므로, httpclient의 `ObjectBody` 문제(같은 키로 다른 바이트)가 여기서는 구조적으로 불가능하다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 계열이 닫혀 있고 스키마가 fail-closed다
|
||
- slug: `adapter-outbound-objectstorage-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L173`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: `ObjectControlRecord`는 열한 개 구현만 허용하는 `sealed interface`이고, javadoc이 규칙을 적는다 — "Unknown families and schemas fail closed." codec의 `payload(...)`가 그 계열에 대해 **exhaustive switch**를 쓰므로, 새 레코드를 추가하면 컴파일이 강제로 codec을 갱신하게 만든다. 스키마 버전은 `ControlRecordSupport.header`가 `schemaVersion != 1`을 거부한다 — "only control schema version 1 is writable". 더 새로운 스키마를 만나면 덮어쓰지 않고 `UnsupportedObjectControlSchemaException`으로 격리한다("A newer or unknown durable schema that must be quarantined rather than overwritten"). `objectstorage.control` 패키지를 leaf 밖에서 참조하는 코드는 **0**이다(`151-...` §8.1, exit=1). CLAUDE.md의 "control-record types leaking into application-core" 금지가 가시성으로 성립한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 모든 키가 단일 인코더에서 나오고 route를 벗어날 수 없다
|
||
- slug: `adapter-outbound-objectstorage-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L273`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: `ObjectControlKeyCodec`과 `ObjectDataKeyCodec`이 각각 "Sole encoder"를 자칭하고, 저장소에서 `"control/v1/"`·`"data/v1/"` 리터럴은 이 두 파일에만 있다(`152-...` §8.3). 키 형태는 `control/v1/<family>/<route>/<shard>/<identity>`이고 shard는 identity의 SHA-256 앞 두 자리다. route 격리가 구조적이다 — `requireMatchingRoute(route, routedIdentity)`가 routed identity의 route 구획을 파싱해 현재 route와 다르면 거부한다("routed identity belongs to a different route"). reference·session·stage handle 키 모두 이 검사를 지난다. 핸들 계열도 접두사로 분리된다 — `osh1`(stage), `osu1`(direct upload), `osm1`(multipart), `osv1`(version), `osr1`(reference). `ObjectNamespaceCodecTest.referenceAndHandleFamiliesRemainSeparated`가 그 분리를 고정하고, `dataKeyApiHasNoRawNameStringParameter`는 **API 서명 자체에 raw 이름 문자열이 없음**을 단언한다. `CrockfordBase32`는 소문자 정규 알파벳(`0123456789abcdefghjkmnpqrstvwxyz` — I·L·O·U 제외)을 쓰고, 인코딩 후 남은 값이 있으면 거부한다("base32 output length is too small") — 잘림을 조용히 넘기지 않는다. test에 property-based 검사가 있다 — `routeParserRejectsArbitraryNonCanonicalText(@ForAll String candidate)`(jqwik), `namespaceRejectsAliasesAndTraversalInputs`, 그리고 fingerprint codec에는 **golden vector**가 고정돼 있다(`canonicalIntentHas…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `SignedJsonCursorCodec`: 좋은 trust-boundary 설계와 경계값 결함이 동시에 존재
|
||
- slug: `adapter-outbound-persistence-jpa-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L358`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: codec은 다음 token을 만든다. 확인한 방어는 다음과 같다. signing key 최소 32 bytes URL-safe Base64 / no padding version까지 MAC input에 포함 token 전체 길이 4096-character cap payload 2048-byte cap presented MAC 32-byte exact length 확인 `MessageDigest.isEqual` constant-time comparison MAC 검증 전에 application payload decoder를 호출하지 않음 oversized public input을 substring/decode/MAC allocation 전에 거부하려는 선행 check 기존 dedicated tests도 tampering, foreign key, unknown version, short key, oversized token, wrong-length MAC, oversized payload 등을 폭넓게 검증한다. 문제는 decoded payload size를 decode **전에** 추정하는 helper다. 이 함수는 “최대 decoded size”를 빠르게 계산하려는 의도로 commit `2f5d2fc`에서 hostile-input bounds와 함께 추가됐다. 그러나 codec은 **unpadded Base64URL**을 사용한다. 실제 self-round-trip probe: 즉 현재 accepted encode domain과 accepted decode domain이 다르다. 이건 hostile token을 더 엄격히 거부하는 정도가 아니다. **codec의 자기 round-trip contract를 깨는 boundary defect**다. 실행 evidence: `evidence/raw/035a-jpa-cursor-boundary-probe.java` `evidence/raw/035-jpa-cursor-boundary-probe.txt` 현재 `SignedJsonCursorCodecTest`는: ordinary round-trip 2049-byte encode rejection decode 쪽 arbitrary oversized pa…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Hibernate provider policy는 declared baseline과 실제 runtime을 분리한다
|
||
- slug: `adapter-outbound-persistence-jpa-c25`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1643`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `HibernateProviderPolicy`는 상수로 선언된 Stable provider baseline과 실제 classpath에서 읽은 runtime version을 구분한다. 이 설계가 필요한 이유는 repository가 과거 “7.4를 Stable baseline이라고 문서화하면서 실제 Spring Boot BOM은 7.1.x를 resolve”한 상태를 경험했기 때문이다. declared baseline: policy constant runtime provider: `org.hibernate.Version`에서 읽음 drift 여부: `driftsFromDeclaredBaseline()` app-bootstrap capability/report가 runtime value를 사용 즉 “문서 상수와 같은 상수를 assert해서 green”인 self-fulfilling test는 피한다. 이 sub-scope에서 outside-leaf production consumer가 명확히 존재하는 핵심 Hibernate type도 `HibernateProviderPolicy`다. app-bootstrap이 이를 composition/report에 사용한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Fileserver composition과 schema lifecycle
|
||
- slug: `adapter-outbound-persistence-jpa-c45`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3166`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: Fileserver persistence는 latent helper가 아니라 실제 opt-in production capability다. `PersistenceJpaRootAutoConfiguration`이 `FileserverJpaPersistenceConfig`를 import한다. `app.fileserver-platform.enabled=true`이면 Fileserver entity/repository/component scan이 열린다. `FileserverStorageConfiguration.fileserverSchemaActivation()`은 `JdbcOperations`가 있으면 startup에서 `requireActive()`를 호출한다. 따라서 schema activation, quota, cleanup, recovery adapter는 Fileserver capability가 켜진 배포에서 production-reachable하다. V1은 registry에 `jpa-fileserver-metadata-v1`, `feature_revision=1`, `INSTALLED_INACTIVE`를 기록하고, V2는 recovery schema를 추가한 뒤 revision을 2로 올린다. 이후 V3는 fenced cleanup lease column을, V4는 upload terminal lifecycle column을 추가하지만 registry revision은 더 이상 갱신하지 않는다. 이 차이는 아래 startup fail-open finding의 직접 원인이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — crypto envelope와 contact-point secret protection
|
||
- slug: `adapter-outbound-persistence-jpa-c49`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3561`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: request variable payload는 `NotificationPayloadProtection`을 필수 collaborator로 받아 보호된 envelope를 저장하고, contact point는 ciphertext/nonce/lookup HMAC/key id로 분리된다. V10은 plaintext-looking request envelope를 DB constraint로도 거부한다. 이번 완독에서 이 경계 자체를 우회하는 production write path는 확인하지 못했다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — mapping의 나머지는 manifest를 실제로 강제한다
|
||
- slug: `adapter-outbound-persistence-mongo-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L493`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: P1과 별개로, 이 package의 나머지는 api manifest를 말이 아니라 코드로 만든다. `MongoCustomConversionsFactory.converters(...)`가 변환기를 **명시적 List 순서로** 조립한다. 이유가 주석에 있다 — Spring의 conversion service는 첫 매칭 변환기를 쓰므로 `Set`이나 classpath 스캔에서 조립하면 JVM 실행마다 다른 변환기가 선택될 수 있다. `fingerprint(manifest)`가 manifest fingerprint에 변환기 클래스 이름을 이어 붙여 golden BSON snapshot이 비교할 identity를 만든다. 같은 factory가 `requireEveryAxisImplemented(...)`로 `LOCAL_DATE_TIME_WITH_REGISTERED_CONVERTER`를 startup에서 거부한다. enum 상수 자신이 "selecting this without registering the named converter is a startup failure"라고 적어 둔 규칙을 실제로 집행하는 지점이다. `BigIntegerRepresentationConverters.forRepresentation(...)`은 manifest의 BigInteger 축을 세 변환기 쌍으로 컴파일한다. 주석이 과거 상태를 기록한다 — 이 축은 선언만 있고 컴파일되지 않아 `STRING`과 `DECIMAL128`이 동일한 document를 만들었고, 하나는 사전식으로 다른 하나는 수치로 정렬된다. `LocalDateTimeMappingGuard`는 `MongoMappingConfiguration`이 **실제 등록된 변환기**로 만든다. javadoc이 이전 결함을 적는다 — guard를 `withoutConverters()`로 만들고 manifest를 검증하게 해서, 명명된 변환기를 등록한 배포와 등록하지 않은 배포를 똑같이 거부했다. `BigDecimalToDecimal128Converter`는 driver 호출 전에 34 유효숫자·지수 범위를 검사한다. `Decimal128`은 초과 정밀도를 조용히 반올림하므로, 검사가 없으면 금액이 다른 값으로 …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 이 sub-scope는 정책과 값 객체이고, 배선된 것은 하나뿐이다
|
||
- slug: `adapter-outbound-persistence-mongo-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L709`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: auto-configuration이 이 sub-scope에서 만드는 bean은 **`MongoBudgetEnforcer` 하나**다(`132-...` §8.1). `MongoQueryPolicy`·`PolicyAwareMongoQueryBuilder`·`MongoRegexPolicy`·`MongoBudgetPolicyRegistry`·`MongoKeysetCursorCodec`·`PolicyAwareMongoAggregationExecutor`는 bean도 아니고 `main` 안에 소비자도 없다(§8.1 세 번째 검색 exit=1). 그 하나조차 짝이 없다. `MongoBudgetEnforcer`의 유일한 production 소비자는 `PolicyAwareMongoAggregationExecutor`인데 그것이 미배선이므로, 배선된 enforcer는 현재 아무도 호출하지 않는다. `MongoKeysetCursorCodec`은 32바이트 이상 서명 키를 요구하는데 그 키를 공급하는 production 코드가 없다 — 생성자 호출은 test 3곳뿐이다. 이것 자체는 결함이 아니다. 이 leaf는 가짜 도메인을 두지 않고 collection profile·field descriptor·budget을 fork가 선언하도록 설계돼 있으며, CLAUDE.md가 "Real forks add their own document, repository, mapper"라고 명시한다. 기록하는 이유는 두 가지다. (a) README의 D1/D2 표는 "typed query, mapping manifest, atomic update, optimistic revision"을 노출 계층의 내용으로 제시하는데, 그중 typed query 계열은 배선 없이 fork가 조립해야 한다는 사실이 그 표에 없다. (b) §41의 다음 항목이 그 조립 시점에만 문제가 된다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 분류 불변식이 실제로 성립한다
|
||
- slug: `adapter-outbound-persistence-mongo-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1269`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: 세어 봤다(`137-...` §8.1b·§8.1c). `@MongoAdvancedEntryPoint` **7개**: `MongoChangeMessagingBridge`(CHANGE_STREAM), `MongoCsfleClientFactory`(CSFLE), `MongoQueryableEncryptionCollectionManager`(QUERYABLE_ENCRYPTION), `MongoGridFsMigrationJob`(GRIDFS_COMPATIBILITY), `MongoShardingAdminGateway`(SHARDING), `MongoTenantClientRegistry`·`MongoTenantMigrationCoordinator`(DATABASE_PER_TENANT). `@MongoAdvancedPolicy` **11개**. 어느 쪽도 아닌 구체 클래스 **1개**: `MongoAdvancedConfiguration`. 이것은 누락이 아니다 — `MongoAdvancedRules.concreteClass()`가 `@Configuration`을 명시적으로 제외하며 이유를 적는다: "A `@Configuration` class is the package's composition root: it builds entry points through the guard rather than being one, and **gating it would gate the thing that supplies the guard**." interface·enum·record·익명·private 중첩·abstract도 같은 방식으로 제외되고 각각 근거가 붙어 있다. 즉 §83이 말하는 불변식은 문서가 아니라 코드로 서 있다. 이 leaf에서 "문서가 주장하고 코드가 지키지 않는다"를 여러 번 본 뒤라, 여기서는 그 반대가 성립한다는 것을 명시해 둘 가치가 있다. 또 하나의 confirmed: **`throw new UnsupportedOperationException`만 하는 public 메서드를 값으로 바꾼 수리**가 두 곳에서 같은 형태로 이루어졌다. `MongoTimeSeriesCapabilityValidator`는 네 개의 던지기만 하는 메서드를 …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — public API와 secret boundary
|
||
- slug: `application-core-c10`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L230`
|
||
- owning-module: `application-core`
|
||
- classification: public contract는 arbitrary `Object`/`Map<String,Object>`를 허용하지 않고 sealed `NotificationVariable` algebra를 사용한다. 과거 mutable/arbitrary variable 때문에 serialization/fingerprint drift와 `toString` collision이 가능했던 것이 변경 근거다. structural test는 public API에 arbitrary Object가 다시 들어오지 않는지 검사하며, 과거 잘못된 test root로 vacuous pass했던 문제도 regression guard로 남아 있다. `NotificationPlan`은 exact template version을 pin한다. recipient/metadata/variable count/depth가 bounded되어 있고 receipt는 “durable logical acceptance”이지 provider delivery를 의미하지 않는다. contact point는 encrypted value + keyed fingerprint로 분리되고 protected contact rendering은 원문을 노출하지 않는다. template variable 자체에 reset token 같은 secret이 들어갈 수 있어 payload protection이 존재하며 decrypt 실패를 빈 값으로 degrade하지 않는다. contact lookup/provider request/callback fingerprint는 HMAC purpose를 분리해 동일 secret-purpose reuse를 피한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 재개 토큰 — 서명하고, 구분자를 봉인한다
|
||
- slug: `grpc-policy-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L73`
|
||
- owning-module: `grpc-policy`
|
||
- classification: `GrpcResumeToken` 의 아홉 성분 각각이 왜 필요한지가 javadoc 에 있다 — 스냅숏 판본 없이는 사라진 뷰의 위치에서 재개하고, 만료 없이는 이력이 사라진 커서에서 재개하고, 필터 지문 없이는 남의 필터를 자기 위치에서 재개해 요청하지 않은 행을 받는다. 그리고 문자열 성분이 구분자를 담지 못하게 생성자가 거부한다. `GrpcResumeTokenCodec` 의 검증이 세 성질을 지킨다 — 상수 시간 비교(`MessageDigest.isEqual`), 알 수 없는 키 식별자 거부, 세 실패의 구분 불가.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-admin-api-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L185`
|
||
- owning-module: `messaging-admin-api`
|
||
- classification: 8개 필드. 그중 `requestedBy` 가 생성자에서 4-eyes 를 강제한다. "우회할 수 없는 자리에 표현했다" — 검사기가 아니라 **record 생성자**에 두었으므로, 서명 대상 객체가 존재하는 것 자체가 4-eyes 통과를 뜻한다. `canonicalForm()` 은 길이 접두 인코딩이다. 버전 접두 `"v1"` 이 앞에 있어 형식 교체 여지를 남긴 것도 의도적으로 보인다. 이 규칙이 계획 다이제스트 쪽에는 적용되지 않았다 — §12.3(b). 세 가지가 코드로 지켜진다. 검증 순서도 의미가 있다: **다이제스트 대조 → 윈도우 → 16진 파싱 → HMAC**. 다이제스트를 먼저 보는 이유가 주석에 있다. `sign(...)` 이 같은 클래스에 public 으로 있고, javadoc 이 그 위험을 스스로 명시한다. 이 문장과 대칭키 선택이 만드는 구조적 결과가 §17 의 한 항목이다: **검증하는 쪽은 반드시 서명할 수도 있다.** 실제 분기는 여섯이다. 5번에는 별도 인라인 주석이 있어 의도된 검사임이 분명하다. "검사는 있는데 클래스 javadoc 이 세지 않는" 두 항목이, 동시에 **어떤 테스트에도 도달하지 않는** 두 항목이다(`EVD-303`, §12.1). "dry run 은 항상 허용" 의 근거도 적혀 있다 — "the way to make operators plan before they act is to make planning free". 이것은 보안 완화가 아니라 행동 설계다. `ApprovedReplayPlan` 생성자(`:19-52`): `requireExecutable(now, currentTopologyVersion)`(`:54-85`): 토폴로지를 **두 번** 보는 이유가 주석에 있다. 승인이 서명된 토폴로지와 계획이 계산된 토폴로지가 다를 수 있으므로 둘 다 현재와 대조한다. `ApprovedRedrivePlan` 은 같은 네 검사에 더해 `loopAcknowledged` 를 별도 필드로 갖는다. 그리고 `requireExecutable` 의 마지막 분기가 그것을 강제한다. "진행처럼 보이는 루프" 는 이 리프에서 반복되는 관점이다 — 대시보드에서 옳아 보이는 실패를 타입으로 막는다. 키를…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-cloudevents-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L48`
|
||
- owning-module: `messaging-cloudevents`
|
||
- classification: 플랫폼 봉투와 CloudEvents 1.0.2 사이의 양방향 매퍼. 적용 범위를 인터페이스 javadoc이 한정한다. 의존성 선언에 이 저장소에서 가장 자세한 근거 주석이 붙어 있다. **둘의 scope가 다른 것이 정확하다.** `cloudevents-api`(`CloudEvent`, `CloudEventData`)는 public 시그니처에 나오므로 `api`, `cloudevents-core`(`CloudEventBuilder`, `BytesCloudEventData`)는 구현 안에서만 쓰이므로 `implementation`이다. `src/messaging/CLAUDE.md:40-43`의 게이트가 잡는 구분을 두 좌표로 나눠 지켰다. **이 leaf의 위치가 형제들과 다르다.** `runtime_memberships`가 `["app-bootstrap"]`이다 — 배포 아티팩트가 싣는다. 그런데 소비자가 하나도 없다(§12.1). Avro·Protobuf는 "싣지도 않고 쓰지도 않는다"로 정합하지만, 이 leaf는 **싣고 쓰지 않는다.**
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-cloudevents-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L80`
|
||
- owning-module: `messaging-cloudevents`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `cloudevents-api:4.0.1`(api), `cloudevents-core:4.0.1`(implementation). 나가는 것: `messaging-spring-boot-starter`의 `allowed_dependencies`에 포함된다. 그래서 `app-bootstrap` → starter → 이 leaf 경로로 런타임 classpath에 오른다. **그러나 어떤 코드도 이 leaf의 타입을 부르지 않는다.** starter의 어느 `@Bean`도 `CloudEventMapper`를 만들지 않고, 어느 클래스도 import하지 않는다(§12.1). bean 없음(Spring 주석 0개).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 패키지/컴포넌트 지도
|
||
- slug: `messaging-cloudevents-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L92`
|
||
- owning-module: `messaging-cloudevents`
|
||
- classification: `CloudEventExtensions`의 javadoc이 이름이 봉투 필드명과 다른 이유를 적는다 — "CloudEvents requires extension names to be lowercase alphanumeric".
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-cloudevents-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L290`
|
||
- owning-module: `messaging-cloudevents`
|
||
- classification: **나가는 방향:** `occurredAt` 확인(없으면 거절) → `CloudEventBuilder.v1()`에 id·source·type·time·datacontenttype·schemaversion → 선택 확장 셋 → payload 종류 판정 → `dataschema`(있을 때) → `build()` **들어오는 방향:** `time` 확인(없으면 거절) → `datacontenttype`(기본 `application/json`) → `data`(없으면 빈 배열) → `MessageId`·`MessageType`·`SchemaVersion`·`ProducerId`·확장 셋 → `MessageEnvelope` 조립
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-cloudevents-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-cloudevents.md#L298`
|
||
- owning-module: `messaging-cloudevents`
|
||
- classification: **분류된 실패 넷, 분류되지 않은 실패 여덟.** 매퍼가 직접 던지는 것은 전부 `MessageValidationException`이지만, 값 객체 생성자에 위임한 검증은 전부 raw `IllegalArgumentException`이다. `fromCloudEvent`는 **외부에서 온 데이터**를 다루는 유일한 진입점인데, 그 진입점의 실패 대부분이 플랫폼 실패 어휘 밖이다. `messaging-core-api`의 `FailureDescriptor` 설계 전체가 "예외 클래스로 분기하지 말고 선언된 분류로 판단하라"였다. 이 경로는 그 분류를 만들지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-core-api-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L101`
|
||
- owning-module: `messaging-core-api`
|
||
- classification: 들어오는 것: 없음. registry `allowed_dependencies: []`이고 `build.gradle`에 선언이 없다. 나가는 것(이 leaf를 의존하는 messaging leaf, registry 기준): `messaging-schema-api`, `messaging-schema-json`, `messaging-schema-avro`, `messaging-schema-protobuf`, `messaging-cloudevents`, `messaging-policy`, `messaging-transport-spi`, `messaging-runtime-core`, `messaging-observability`, `messaging-security`, `messaging-kafka`, `messaging-kafka-share-experimental`, `messaging-rabbit`, `messaging-reliability-api`, `messaging-outbox-jdbc-postgresql`, `messaging-inbox-jdbc-postgresql`, `messaging-claim-check`, `messaging-admin-api`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-cloud-stream-bridge`, `messaging-spring-boot-starter`, `messaging-testkit` — messaging family의 나머지 **24개 전부**. `runtime_memberships: ["app-bootstrap"]`이고, 그 편입은 직접 선언이 아니라 **전이(transitive)**로 일어난다. `src/app-bootstrap/build.gradle:87`이 선언하는 것은 하나다: starter의 `allowed_dependencies`가 17개 leaf를 끌고 오고 그 closure에 `messaging-core-api`가 있다. 즉 **배포 아티팩트가 이 leaf를 싣는다.** 실행 여부는 별개이고 master switch `app.…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-core-api-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-core-api.md#L406`
|
||
- owning-module: `messaging-core-api`
|
||
- classification: 이 leaf에는 실행 경로가 거의 없다. 실제로 코드가 도는 지점은 넷이다. 1. **봉투 생성** — `new MessageEnvelope<>(...)` → 14개 non-null 검사 + partitionKey/orderingKey wire 검사 2. **헤더 생성** — `MessageHeaders.application/platform(Map)` → 개수(≤64) → 이름별 예약/자격증명/중복 검사 → 총 바이트(≤32,768) 3. **식별자 생성** — `MessageId.newId()` → `UuidV7.next()` → `AtomicLong.updateAndGet(advance)` 4. **결과 조립** — `new PublishResult(...)` / `new SettlementResult(...)` → 조합 검증 나머지는 전부 인터페이스 선언이고, 구현은 `messaging-runtime-core`·`messaging-kafka`·`messaging-rabbit` 등 다른 leaf가 소유한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-schema-api-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L79`
|
||
- owning-module: `messaging-schema-api`
|
||
- classification: 들어오는 것: `messaging-core-api`(api 노출). 나가는 것: `messaging-schema-json`, `messaging-schema-avro`, `messaging-schema-protobuf`, `messaging-cloudevents`, `messaging-policy`, `messaging-transport-spi`, `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`, `messaging-testkit`. 런타임 편입은 `messaging-core-api`와 같은 경로다 — `app-bootstrap`이 `messaging-spring-boot-starter`를 선언하고 그 closure가 이 leaf를 끌어온다. 이 leaf는 bean을 만들지 않는다. Spring 주석 0개.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-schema-api-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L113`
|
||
- owning-module: `messaging-schema-api`
|
||
- classification: 이 leaf에서 가장 밀도 높은 javadoc이다. 핵심은 "Nothing failed"다. 타입만으로 키를 잡으면 실패가 발생하지 않고 **잘못된 성공**이 발생한다. 그리고 그 결과에는 등록된 적 없는 버전 라벨이 붙어 하위 감사 기록까지 오염된다. 이 결정은 세 codec에 전부 반영돼 있다 — `JacksonMessageCodec.requireRegistered`, `AvroMessageCodec.schemaFor`, `ProtobufMessageCodec.requireRegistered`가 모두 "타입은 아는데 버전을 모른다"와 "타입 자체를 모른다"를 **다른 에러 코드**로 구분한다(`SCHEMA_VERSION_NOT_REGISTERED` vs `UNKNOWN_MESSAGE_TYPE`). 그 구분이 있어야 운영자가 "등록을 빠뜨렸다"와 "오타다"를 나눌 수 있다. 세 가지 설계 결정이 붙어 있다. 1. **버퍼를 한도로 미리 잡지 않는다.** `new ByteArrayOutputStream(Math.min(maxBytes, 8_192))` — 주석: "a 1 GiB bound must not pre-allocate 1 GiB." 2. **codec의 에러 코드를 그대로 던진다.** `errorCode`가 생성자 인자다. 그래서 Avro는 `AVRO_PAYLOAD_TOO_LARGE`, JSON은 `PAYLOAD_TOO_LARGE`가 나온다. 테스트가 이 성질을 직접 단언한다(`BoundedByteSinkTest.java:69-77`, `as("the sink reports the codec's own code, not a generic one")`). 3. **`requireFits(size)`는 예산을 소비하지 않는다.** Protobuf는 직렬화 크기를 미리 알므로 첫 바이트 전에 거절할 수 있다. 그리고 그 뒤의 쓰기도 여전히 경계 안이다 — 주석: "this is a cheaper refusal, not a replacement for the bound." `refuseIfBeyondLimit`가 `size > maxBytes - written`으로 비교하는 것도 의도적이다. `written + size > maxBy…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-schema-api-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-api.md#L255`
|
||
- owning-module: `messaging-schema-api`
|
||
- classification: 1. **경계 있는 인코딩** — codec이 `BoundedByteSink.of(maxBytes, code)`를 만들고 → 포맷 라이브러리가 sink에 쓰고 → 한도를 넘는 write에서 `MessageTooLargeException` → 아니면 `sink.toByteArray()`로 `EncodedMessage` 조립 2. **계약 조회** — `new MessageContractKey(type, version)` → registry lookup → 미스면 "타입 미등록" vs "버전 미등록" 구분 3. **진화 검사** — `registry.compatibilityOf(subject)` → `versionsToCheck` → (포맷별 게이트가 실제 비교) 3번은 이 저장소에서 실행되지 않는다(§12.1).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-schema-avro-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L49`
|
||
- owning-module: `messaging-schema-avro`
|
||
- classification: 선택적(optional) Avro codec. Stable이 아니고 registry membership이 비어 있다 — **build-only / incubating**이며, `docs/messaging/support-matrix.md`의 등급과는 다른 축이다. Avro를 `api`로 선언한 이유가 build.gradle 주석에 있다. `src/messaging/CLAUDE.md:40-43`이 기술하는 게이트 — public/protected 시그니처에 나오는 vendor 라이브러리가 `api`로 선언됐는지 대조 — 를 이 leaf가 통과한다. 형제 `messaging-schema-json`은 Jackson 타입이 시그니처에 없으므로 `implementation`이고, 그 판정 차이가 규칙이 실제로 작동한다는 증거다. **클래스 둘의 실행 시점이 다르다.** 게이트의 javadoc이 그 이유를 적는다 — "By the time a producer has published one incompatible record, the damage is durable: the record sits in a retained log that every current and future consumer must be able to read."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-schema-avro-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L75`
|
||
- owning-module: `messaging-schema-avro`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `avro:1.12.0`(api). 나가는 것: **없다.** 어떤 leaf의 `allowed_dependencies`에도 `messaging-schema-avro`가 없다. `messaging-spring-boot-starter`의 17개 의존 목록에도 없다. 런타임 배선: 없음. `runtime_memberships: []`이므로 배포 아티팩트에 실리지 않는다. bean도 없다(Spring 주석 0개). **소비자 없음과 membership 없음이 일치한다.** 이것이 정합적인 incubating 상태다 — `messaging-cloudevents`와 대비된다(그쪽은 membership이 있고 소비자가 없다).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-schema-avro-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L106`
|
||
- owning-module: `messaging-schema-avro`
|
||
- classification: "does not fail — it produces plausible garbage"가 이 leaf의 모든 방어의 전제다. JSON이나 Protobuf와 달리 Avro는 잘못된 스키마로 디코딩해도 예외를 던지지 않는 경우가 있다. single-object encoding에 헤더를 붙이지 않는 것도 명시적 결정이다 — "The framing that would carry a schema fingerprint belongs to the transport headers, where the platform already carries schema identity for every format, rather than being duplicated inside the Avro payload for this one format." 생성자가 받는 것은 중첩 맵 `Map<MessageType, Map<SchemaVersion, Schema>>`이고, `Map.copyOf`는 **바깥 레벨만** 복사한다. 이 결함이 위험한 이유는 §4.1과 곱해진다 — 스키마가 바뀌어도 디코딩이 실패하지 않고 그럴듯한 쓰레기를 낸다. `AvroRegistryBoundsTest.mutatingTheCallersMapAfterConstructionChangesNothing`이 세 가지를 한 번에 확인한다: 생성 후 추가한 버전은 미등록, 생성 후 추가한 타입도 미등록, 원래 등록한 스키마는 그대로. 평탄화가 `MessageContractKey`(schema-api)를 키로 쓰므로 §4.5의 2단 에러 구분도 자연히 따라온다. `BoundedByteSink`(schema-api)의 경계가 실제로 작동하려면 인코더가 증분적으로 써야 한다. `EncoderFactory.get().binaryEncoder(...)`는 버퍼링하므로 sink가 첫 write를 보기 전에 큰 레코드가 이미 할당된다. 즉 **schema-api의 방어가 이 한 줄에 의존한다.** payload가 `GenericRecord`인가 → `AVRO_PAYLOAD_NOT_A_RECORD` `schema.equals(record.getSchema())`인가 → `AVRO_SCHEMA_MISMATCH` 두 번째는 …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-schema-avro-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L254`
|
||
- owning-module: `messaging-schema-avro`
|
||
- classification: **encode:** `schemaFor` → `GenericRecord` 확인 → 스키마 동일성 확인 → `BoundedByteSink` + direct encoder → `writer.write` + `flush` → `EncodedMessage(bytes, AVRO, SchemaReference)` **decode(동일 버전):** `requireWithinLimit` → `schemaFor` → 대상 타입이 `GenericRecord` 계열인지 → `boundedReader(writer, writer)` → `reader.read` **decodeEvolved:** `requireWithinLimit` → `schemaFor(writer)` + `schemaFor(reader)` → `boundedReader(writer, reader)` → `reader.read` **CI 게이트:** `check(candidate, history, mode)` → 모드에 따라 비교 대상 선정 → 방향별 `checkReaderWriterCompatibility`
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-schema-avro-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L266`
|
||
- owning-module: `messaging-schema-avro`
|
||
- classification: **예외 재던지기 패턴이 세 곳에 반복된다.** `BoundedByteSink`가 던지는 `MessageTooLargeException`은 `RuntimeException`이므로 catch에 걸린다. 그것을 그대로 통과시키지 않으면 크기 실패가 인코딩 실패로 접힌다 — JSON codec의 `unwrapTooLarge`와 같은 문제를 다른 방식(원인 사슬 탐색이 아니라 즉시 `instanceof`)으로 푼다. §12.3. `AvroHostileInputTest.aCountBeyondIntRangeFailsWhileReadingRatherThanWhileReserving`가 흥미로운 경계를 잡는다 — 2³²을 주장하면 int로 잘려 무해한 값이 되고, 그 다음 읽기가 입력 부족으로 실패해 `MessageSerializationException`이 된다. 즉 `newArray` 방어를 우회하는 값이 존재하지만 그 우회는 할당이 아니라 읽기 실패로 끝난다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-schema-json-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L71`
|
||
- owning-module: `messaging-schema-json`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `messaging-schema-api`(api), `jackson-databind`(implementation). 나가는 것: `messaging-spring-boot-starter`(registry `allowed_dependencies`에 포함). **실제 배선 지점이 하나 있다** — 이 플랫폼에서 유일하게 조립되는 codec이다. `RegisteredMessageCodecs.of(defaultCodec, codecs...)`의 varargs 자리가 비어 있다. 즉 **출하 구성의 codec registry에는 JSON 하나만 들어간다.** Avro·Protobuf·raw bytes는 등록되지 않는다. 두 번째 배선 지점은 상수 참조다. payload 정책의 상한이 **JSON codec의 상수에서 파생된다.** 포맷 중립이어야 할 admission 정책이 한 포맷의 클래스 상수를 참조한다 — §17에서 다룬다. `contracts.getIfAvailable(MessageContracts::none)`이 기본값이므로, 애플리케이션이 `MessageContracts` bean을 내놓지 않으면 **빈 registry**로 codec이 만들어진다. 그 codec은 모든 `encode`/`decode`를 `UNKNOWN_MESSAGE_TYPE`으로 거절한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-schema-json-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L124`
|
||
- owning-module: `messaging-schema-json`
|
||
- classification: 여섯 가지 방어가 한 곳에 있다. 그리고 **polymorphic default typing을 켜지 않는다.** javadoc이 그것이 대부분의 JSON gadget chain의 기반이라고 적는다. `maxDocumentLength`가 `maxBytes`와 같다는 점이 중요하다 — 인코딩 상한과 디코딩 파서 상한이 하나의 값에서 나온다. 따로 두면 둘이 갈라진다. 주석이 이유를 적는다 — "Jackson writes incrementally, so a payload whose serialized form is far larger than the limit stops at the limit instead of after the whole graph has been rendered into a buffer nobody bounded." `unwrapTooLarge`가 필요한 이유도 명시돼 있다. `for (Throwable cause = exception; cause != null; cause = cause.getCause())` — 원인 사슬을 끝까지 훑어 `MessageTooLargeException`을 찾는다. 못 찾으면 `MessageSerializationException("JSON_ENCODE_FAILED")`. `SCHEMA_VERSION_NOT_REGISTERED` 메시지에는 `registeredVersions(type)`가 정렬되어 포함된다. 테스트가 그 내용을 직접 단언한다 — `hasMessageContaining("order.created v999").hasMessageContaining("[1, 2]")`(`JsonContractRegistryTest.java:58-61`). 운영자가 "1과 2는 있고 999는 없다"를 에러 메시지만으로 알 수 있다. 비대칭이 합리적이다. 인코딩에서 `OrderCreated`의 하위 타입을 넘기면 Jackson이 등록된 형태로 직렬화한다. 디코딩에서 하위 타입을 허용하면 등록된 계약과 다른 클래스로 역직렬화되므로 정확 일치여야 한다. 다만 이 비대칭은 주석으로 설명되지 않았다 — §15의 추론 항목이다. 명시 검사 하나(`encoded.length`)와 파서 내부 검사 하나(`max…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-schema-json-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-json.md#L238`
|
||
- owning-module: `messaging-schema-json`
|
||
- classification: **encode:** `requireRegistered(type, version)` → payload가 등록 타입의 인스턴스인지 → `BoundedByteSink` 생성 → `mapper.writeValue(sink, payload)` → 실패 시 `unwrapTooLarge` → `EncodedMessage(bytes, JSON, SchemaReference)` **decode:** `requireRegistered(type, version)` → 요청 클래스가 등록 클래스와 정확히 같은지 → `encoded.length` 상한 → `mapper.readValue` → `JacksonException`이면 `JSON_DECODE_FAILED`
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-schema-protobuf-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L49`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- classification: 선택적 Protobuf codec. `runtime_memberships: []`이고 starter의 codec registry에도 등록되지 않는다 — build-only / incubating. protobuf를 `api`로 선언한 이유가 build.gradle 주석에 있다. `src/messaging/CLAUDE.md:40-43`의 vendor `api` 게이트를 통과한다 — `ProtobufMessageContract(Class<? extends Message>, Parser<? extends Message>)`가 public record이므로 소비자가 그 타입을 이름 부르지 않고는 계약을 등록할 수 없다. **이 leaf의 핵심 문제 인식**은 클래스 javadoc이 한 문장으로 적는다. `messaging-schema-avro`의 "does not fail — it produces plausible garbage"와 같은 성질이다. **JSON은 틀린 스키마로 디코딩하면 대개 실패하고, Avro와 Protobuf는 실패하지 않는다.** 그래서 두 leaf 모두 registry를 계약의 중심에 둔다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-schema-protobuf-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L229`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- classification: **계약 등록:** `new ProtobufMessageContract(class, parser)` → 빈 입력 파싱 → 클래스 일치 확인 → 실패 시 `MessagingConfigurationException` **encode:** `requireRegistered` → `Message`이고 등록 클래스인지 → `requireFits(getSerializedSize())` → `writeTo(sink)` → `EncodedMessage(bytes, PROTOBUF, SchemaReference)` **decode:** `requireRegistered` → 요청 클래스 정확 일치 → `encoded.length` 상한 → `parser.parseFrom`
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-schema-protobuf-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L239`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- classification: **Avro와 다른 점 하나.** Avro는 `catch (IOException | RuntimeException)` 안에서 `MessageTooLargeException`을 `instanceof`로 통과시킨다. Protobuf는 `catch (IOException failure)`만 잡으므로 sink가 던지는 `MessageTooLargeException`(`RuntimeException`)이 그대로 전파된다. 별도 통과 로직이 필요 없다 — protobuf-java가 예외를 감싸지 않기 때문이다. 세 codec이 같은 문제를 세 가지로 푸는데(JSON은 원인 사슬 탐색, Avro는 즉시 `instanceof`, Protobuf는 아무것도 안 함) 각각 라이브러리 동작에 맞는 최소 해법이다. 다만 그 이유가 코드에 적혀 있지 않다. **계약 위반은 `CONFIGURATION`이고 메시지 실패가 아니다.** `ProtobufMessageContract` 생성 실패는 registry를 조립하는 시점, 즉 시작 시점에 난다. `MessagingConfigurationException` javadoc이 그 의도를 적는다 — "Raised at startup wherever possible."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-schema-protobuf-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-protobuf.md#L458`
|
||
- owning-module: `messaging-schema-protobuf`
|
||
- classification: `ProtobufMessageContract` javadoc이 두 결함을 보존한다. 두 번째가 특히 이 저장소의 반복 주제다 — **실패의 종류가 바뀌면 운영자가 읽을 정보가 사라진다.** `messaging-core-api`의 `FailureDescriptor` 설계, `MessageContractKey`의 2단 에러, JSON codec의 `unwrapTooLarge`가 전부 같은 관심사다. `.proto` 파일의 주석도 설계 이유를 남긴다 — "Field numbers are the contract, not the field names ... Tags are never reused, and removed fields are reserved so that a later edit cannot take the number back." 이 규칙 셋 중 둘(개명 안전, 태그 재사용 위험)이 테스트로 증명되고 하나(reserved)는 증명되지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 패키지/컴포넌트 지도
|
||
- slug: `messaging-testkit-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L140`
|
||
- owning-module: `messaging-testkit`
|
||
- classification: 단일 패키지 `dev.caskeleton.messaging.testkit`. 세 소스셋이 같은 패키지를 공유하므로 `InMemoryMessagingHarness`(test)가 `FaultController`(main)를 package-private 없이 구현할 수 있고, `EnvelopeCodecBenchmark`(jmh)도 같은 패키지에 있다. 데이터 흐름은 한 방향이다. 핵심은 **화살표 방향이 한 번도 역전되지 않는다**는 것이다. 등급이 증거를 만들지 않고 증거가 등급을 만든다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Messaging envelope schema
|
||
- slug: `shared-contract-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/02-shared-contract.md#L114`
|
||
- owning-module: `shared-contract`
|
||
- classification: `contracts/messaging/envelope/v1.schema.json`은 Draft 2020-12 schema resource이며 envelopeVersion/eventId/contractId/payloadVersion/logicalDestination/aggregate/occurredAt/correlationId/contentType/payload를 required로 고정하고 top-level/aggregate에 `unevaluatedProperties:false`를 둔다. checked-in SHA-256은 `bf6f2e13fafe01b8ef4cbb73d7ba3f5703bfc68d145bdfe43190bf606dbd00b1`이다. `MessagingEnvelopeSchemaResourceTest`는 schema text 자체, identifier regex parity, Java int/long 경계 vector, strict UTF-8, exact digest를 JDK API로 검증한다. 이 테스트는 resource drift와 digest mismatch를 강하게 막지만 README가 명시하듯 실제 Draft 2020-12 validator interoperability나 broker runtime discovery를 증명하지는 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 39 — admission-budget-and-backpressure
|
||
|
||
> 승인·예산·백프레셔 — 무엇이 들어오고 무엇이 거절되는가.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 예산 계층
|
||
- slug: `adapter-inbound-graphql-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L354`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: 설계 §10이 다섯 계층을 정의하고 `GraphQlDeadlinePropagator`가 그 파생을 담는다. 실제 강제 상태: `GraphQlTimeoutPolicy`와 `GraphQlResolverBudget`의 main 참조자를 전수하면 전부 `execution` 패키지 안(그리고 미배선 클러스터 안)이다:
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.4) 문서/구현 드리프트 — 취소 경로
|
||
- slug: `adapter-inbound-graphql-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L376`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `GraphQlCancellation`(93)은 `cost/GraphQlRuntimeBudgetTracker` · `advanced/incremental` · `advanced/subscription` 세 곳에서 쓰인다. 요청 데드라인이 실제로 실행을 끊는 경로가 존재한다는 뜻이고, `GraphQlRequestContext.withDeadline`의 단조 조이기와 함께 요청 계층은 완결돼 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.4) 게이트 프로퍼티가 존재하는가
|
||
- slug: `adapter-inbound-web-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L666`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: 자바 한 줄뿐이다. 어떤 `application.yml`에도 `backend.web.budgets`가 없고 `matchIfMissing`도 없으므로 이 핸들러는 **기본 꺼짐**이다. 그리고 켜더라도 그 생성자가 요구하는 `BudgetProblemMapper` 빈을 선언하는 코드가 main·app-bootstrap 어디에도 없다 — 참조자는 두 필터와 이 핸들러 자신뿐이고, 셋 다 빈 정의가 아니다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — forwarded 헤더 신뢰 정책
|
||
- slug: `adapter-inbound-web-c14`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1098`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `proxy` 패키지 421 LOC이 프로덕션 조립에 들어가지 않는다. 실제로 forwarded 헤더를 해석하는 것은 Spring Boot의 `server.forward-headers-strategy=framework`(app-bootstrap `application.yml:321` 기본값)가 등록하는 `ForwardedHeaderFilter`/`ForwardedHeaderTransformer`이고, 그것은 **피어가 신뢰된 프록시인지 검사하지 않는다**. §32.2.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — 시작 검증과 조립
|
||
- slug: `adapter-inbound-web-c18`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1336`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `FileserverPlatformAutoConfiguration`이 `DefaultNginxInternalUriMapper`(`:215-216`) · `NginxDownloadStrategy`(`:221-223`) · `FileserverRequestContextFactory`(`:159-161`)를 만든다. 회로 닫힘.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 두 프로그래밍 모델의 대칭이 기계 검사되고, 검사기 자신도 검사된다
|
||
- slug: `adapter-outbound-cache-redis-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L245`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `ReactiveRedisOperations`는 "Mirrors `RedisOperations` method for method"라고 주장한다. `ApiParityTest`가 그것을 반사로 강제한다 — `PAIRS` 맵에 14쌍의 sync/reactive 인터페이스를 놓고 `everySyncOperationHasReactiveCounterpart`, `everyTypedSurfaceIsInParity`, `theTwoEntryPointsExposeTheSameStructureAccessors`, `everyReactiveMethodReturnsAPublisher`를 돌린다. 두 facade의 접근자 12개는 실제로 동일하다(diff 공백). 두 가지가 특히 좋다. 첫째, **예외가 이유와 함께 목록에서 빠져 있다** — Pub/Sub은 sync가 핸들러+closeable subscription이고 reactive는 publisher 자신이 전달하며 취소로 구독을 끊으므로 "different shapes on purpose, so mechanical parity would be the wrong check for them". 둘째, `theInspectorDetectsADivergentReturnShape`라는 **검사기에 대한 메타 test**가 있다 — 대칭 검사기가 고장 나 항상 통과하는 상태를 잡는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 키: 렌더된 문자열을 받는 API가 존재하지 않는다
|
||
- slug: `adapter-outbound-cache-redis-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L324`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `QualifiedRedisKey`의 javadoc이 이 계층의 규칙이다 — "This is the only key shape the SDK accepts. **There is no API that takes an already rendered key string**, so namespace, slot, and size rules cannot be bypassed." 구조가 그것을 강제한다. `RedisTypedKey`는 9종만 허용하는 sealed interface고(`ValueKey`·`HashKey`·`ListKey`·`SetKey`·`SortedSetKey`·`BitmapKey`·`HyperLogLogKey`·`GeoKey`·`StreamKey`), 전부 `QualifiedRedisKey` + 코덱으로 구성된다. `QualifiedRedisKey`는 `RedisNamespace`(토큰 3개) + `RedisKeyName`(entity 토큰 + identifier) + 선택적 `RedisSlotTag`다. 그리고 `RedisKeyRenderer`가 **중괄호를 쓰는 유일한 장소**라서 Cluster 해시 태그가 "the tag and nothing else"를 덮는다. `RedisKeyRules`의 자기 한정도 정직하다 — 규칙은 "mechanical"이며 "Values that are indistinguishable from an ordinary surrogate identifier, such as a bare digit string, cannot be rejected here; those must be fingerprinted by the caller before they become a key part." 즉 이 검사가 PII 방지의 완결이 아님을 명시한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — raw gateway — "escape hatch"가 두 겹의 사전 승인으로 닫혀 있다
|
||
- slug: `adapter-outbound-cache-redis-c12`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L684`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `RedisRawGateway`의 javadoc이 존재 이유와 한계를 함께 적는다 — "There is no `execute(String, byte[]...)` here or anywhere else in the SDK. The escape hatch exists because **some commands genuinely have no typed form worth building**, not because arbitrary command execution is acceptable; every one of them is named, bounded, and audited before it can be sent." 승인이 **두 개의 독립된 문**을 모두 통과해야 한다(`RawCommandApprovals`). 1. 명령이 정책 카탈로그에서 `RAW_ONLY`로 분류돼 있어야 한다 — "the organization's decision about which commands may ever leave through this door" 2. 배포가 그 명령에 대한 승인(`ApprovedRawCommand`)을 등록해야 한다 "Neither alone is enough, and neither is decided at request time." 그리고 R3/R4는 어느 쪽이든 거부된다. `ApprovedRawCommand`는 **배포 산출물**이다 — 명령 identity, 최대 인자 수, 요청/응답 바이트 상한, 타임아웃, 응답 디코더를 프로세스 시작 전에 고정한다. 토큰은 `RawCommandApprovals`만 발급하고, 검증은 (a) 토큰 타입이 내부 record인지, (b) **발급 레지스트리 인스턴스가 같은지**(`issued.origin != this`), (c) 정책 id가 일치하는지, (d) 제시된 승인이 등록된 것과 같은지 넷을 본다. **`RawMovableKeys`가 이 패키지에서 가장 흥미롭다.** movable key spec(예: `SORT`)은 키 위치를 인자 목록이 결정하므로 정적으로 알 수 없고, 그러면 네임스페이스 검사를 할 수 없다. 기본은 여전히 거부다. 예외로 `SORT`/`SORT_RO` 파서 하나가 등…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 인가와 감사가 정보를 흘리지 않는다
|
||
- slug: `adapter-outbound-fileserver-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L507`
|
||
- owning-module: `adapter-outbound-fileserver`
|
||
- classification: `RoleBasedFileAccessPolicy`는 열 개 연산을 READ/WRITE/ADMIN 세 계층으로 접는다. 근거가 적혀 있다 — 연산별 역할 맵은 `COPY`를 주고 `CREATE`를 안 주는 조합을 허용하는데 "a copy creates a file"이므로 제한처럼 보이고 제한이 아니다. **admin은 write를 상속하지 않는다** — 삭제할 수 있다는 이유로 force-delete까지 되면 감사되는 관리 평면이 일반 데이터 평면으로 도달 가능해진다. 빈 admin 역할 집합은 생성자가 거부한다("would leave the management plane unreachable rather than protected"). 거부 메시지는 필요한 역할도 주체의 역할도 말하지 않는다 — "a denial that reported what was missing would turn every 403 into a readable description of the role model". `UnenforcedFileAccessPolicy`의 설계도 기록할 만하다. 이름 자체가 장치다 — composition root가 **타입 이름으로 매치해** production startup을 거부한다. "A permissive default that looked like a real policy would ship as one." 실패 메시지 위생도 일관된다. `LocalStorageFailures`의 어떤 메시지도 경로·마운트·루트를 담지 않고, 감사 어댑터가 쓰는 필드는 전부 지문·코드·불투명 식별자다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `ClientProfileValidator` — 34개 위반 코드가 각각 과거 사고를 적는다
|
||
- slug: `adapter-outbound-httpclient-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L90`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: 이 저장소에서 본 가장 조밀한 설정 검증기다. `validate(profile, environment)`가 11개 검사 그룹을 돌리고 결과를 정렬해 "a configuration error reports deterministically across runs and machines"를 보장한다. 특히 이 leaf에서만 보이는 태도가 하나 있다 — **바인딩은 되지만 어떤 전송에도 닿지 않는 설정을 무시하지 않고 거부한다.** 기본값은 통과시키므로 "only a deliberate, unmet request fails"다. 앞선 열 개 모듈에서 반복해 발견한 "선언되었으나 아무것도 하지 않는 설정" 패턴을, 이 모듈은 **명시적 거부로 처리한다.** 같은 논리가 관측 설정에도 적용된다 — `full-url-recording`은 아무도 읽지 않았고 `body-logging`은 actuator 보고에만 닿았다. "Leaving them that way is the worse of the two failure modes — an operator who set them believed the platform was recording full URLs or bodies, and an operator who left them false had no assurance that it was not." 지금은 production에서 둘 다 거부된다. 나머지 검사도 각각 구체적인 다운그레이드를 막는다. **`REACTIVE_REDIRECT_UNSUPPORTED`** — 엔진 리다이렉트는 모든 전송에서 꺼져 있고 hop별 재검증을 하는 coordinator는 블로킹 스택에만 있다. 리액티브 프로파일이 redirect를 켜면 "the caller received the 302 as an ordinary response and read its empty body as the answer." 거부가 정직한 결과다 — "a configured guarantee that silently does nothing is worse than one the platform declines to offer." **`HTTP2_REQUIRED_TRANSPORT_UNSUPPORTE…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 가드 순서와 그 근거
|
||
- slug: `adapter-outbound-httpclient-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L323`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: `AttemptResiliencePipeline`이 물리 시도마다 **Circuit Breaker → Rate Limiter → Bulkhead → HTTP 호출**을 고정 순서로 적용하고 역순으로 해제한다. 그리고 브레이커가 무엇을 보는지에 대한 수정 이력이 하나 더 있다 — 이전에는 원시 전송만 파이프라인 안에서 돌고 응답→예외 매핑이 밖에서 일어나서 "a 503 completed the call normally, the breaker recorded a success, and **an upstream that answered nothing but 503 never opened its circuit. The thing the breaker is for was the one thing it could not see.**" 지금은 `remoteFailure` 분류기가 반환값을 보고 브레이커에 알린다. test 47개가 이 규칙들을 촘촘히 덮는다 — `appliesCircuitThenRateLimiterThenBulkheadPerAttempt`, `openCircuitDoesNotConsumeRateOrBulkheadPermit`, `bulkheadRejectionReleasesTheRateLimiterAndIsNotACircuitError`, `answeredStatusesDoNotRetryANonIdempotentOperation`, `deniesOneShotBodyEvenForPut`, `honorsRetryAfterOnlyInsideDeadline`, `protocolProofOfNonProcessingWinsOverEverything`, `streamAfterGoAwayLastIdIsPeerNotProcessed` 등. `Http2ProtocolEvidence`는 프로토콜 수준 증거를 다룬다 — `REFUSED_STREAM`과 GOAWAY의 last-stream-id보다 큰 스트림 id는 **피어가 처리하지 않았음의 증명**이라 `NOT_SENT`로 승격되고, 그 이하 id의 리셋은 여전히 모호하다(`streamAtOrBelowGoAwayLastIdStaysAmbiguous`, `aBareStreamResetProvesNothi…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 전송은 능력을 선언하고, 프로파일보다 약하면 startup이 실패한다
|
||
- slug: `adapter-outbound-httpclient-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L573`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: `TransportCapabilityValidator`가 프로파일이 요구하는 것과 전송이 선언한 것을 대조해 부족분을 이름으로 모아 거부한다 — 프로토콜, route pool, 유계 pending 큐, proxy, mutual TLS, 동적 대상 안정성. 메시지는 "profile settings and capability names only — never a URL, address, or secret." 능력 레코드가 그 선언을 데이터로 만든다. `ReactiveTransportCapabilities.reactorNetty()`는 9개 능력을 전부 `true`로, `jettyHttp3Experimental()`은 route pool·유계 큐·DNS 핀·동적 안정성을 `false`로 선언한다. HTTP/3는 `compileOnly` 의존이라 클래스가 없으면 `Http3CapabilityReport`가 전송을 거부한다 — "the failure mode is a startup error rather than a `NoClassDefFoundError` mid-call"(§0). testkit이 별도 source set인 것도 이 sub-scope의 성격이다 — 계약을 담은 클래스 35개(`BlockingTransportContract`·`ReactiveTransportContract`·`RetrySafetyContract`·`ResourceLifecycleContract`·`ObservabilityContract`·`DynamicTargetSecurityContract`)를 test·performance·jmh 세 lane이 공유한다. `NettyLeakDetectionExtension`은 leak detector 레벨을 **믿지 않고 확인한다** — "asserts the level rather than trusting the flag reached the forked JVM"(§0). 성능 lane 7개는 자원 상한을 검증한다 — `PoolSaturationPerformanceTest`·`RetryStormBudgetTest`·`RuntimeRotationDrainTest`·`OAuthRefreshContentionTest`·`Larg…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — legacy가 세 겹으로 격리돼 있다
|
||
- slug: `adapter-outbound-objectstorage-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L92`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: 폐기 경로가 셋인데 서로 다른 스위치를 쓰고 서로를 배제한다. `ObjectStorageBindingCompiler.rejectLegacyOverlap`가 legacy filesystem 루트와 canonical provider 루트가 **어느 방향으로든 포함 관계**면 거부한다. `LegacyObjectAdoptionSettings`는 `APPLY` 모드일 때 검토된 manifest 경로와 64자리 SHA-256을 요구하고, batch size 1–1000, timeout 5분 이내를 강제한다. legacy runtime은 `AutoCloseable` holder로 감싸 S3 client 수명을 정확히 소유하고, `@Bean(destroyMethod = "close")`로 등록된다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — objectstorage: staged lifecycle, opaque identity, privilege separation
|
||
- slug: `application-core-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L190`
|
||
- owning-module: `application-core`
|
||
- classification: semantic objectstorage API는 provider/filesystem type을 노출하지 않는다. object identity/reference는 prefix + check digit를 포함한 opaque routed representation이고 redacted rendering을 제공한다. tampered/cross-prefix reference를 거부한다. content I/O는 bounded pull/push callback context와 budget/cancellation/chunk contract를 사용하며 callback lifetime 밖에서 context를 재사용할 수 없다. zero-progress가 무한 loop로 이어지지 않도록 bounded 후 실패한다. lifecycle은 staged -> verified -> published를 분리한다. scanner verdict는 exact stage/version/operation/policy revision에 결합되고 publish/cleanup mutation은 exact-version/fencing을 요구한다. scanner 권한과 purge 권한은 분리돼 검증 주체가 임의 삭제까지 할 수 없게 한다. transient bearer grant는 URI/header를 redaction하고 TTL은 최대 24시간으로 제한한다. multipart part count는 1..10000이고 completion은 expected content identity를 요구한다. `FullContentIdentity`는 SHA-256 기반으로 ETag를 content identity로 오인하지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 능력 15종과 등급 4종
|
||
- slug: `grpc-advanced-bootstrap-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md#L53`
|
||
- owning-module: `grpc-advanced-bootstrap`
|
||
- classification: 능력을 하나씩 등급 매기는 것이 설계다. 기본 등급 분포는 `ADVANCED_STABLE` 11, `EXPERIMENTAL` 3(`HEDGING`·`CUSTOM_LOAD_BALANCER`·`XDS`), `WATCH` 1(`EDITION_2026`)이다. `EXPERIMENTAL` 에 두 번째 승인을 요구하는 근거가 적혀 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 수동 흐름 제어
|
||
- slug: `grpc-advanced-streaming-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-streaming.md#L86`
|
||
- owning-module: `grpc-advanced-streaming`
|
||
- classification: 승인이 record 의 필드이고 거짓이면 생성자가 거부한다. 수요 상한과 교착 감시가 필수다 — 상한 없는 `request(n)` 은 단계만 늘린 무제한 버퍼링이다. 감시견은 잠들지 않고 두 시각을 비교한다 — 마지막으로 수요를 요청한 때와 마지막으로 메시지가 움직인 때. 둘 다 시간 제한만큼 멈춰 있으면 교착이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-admin-api-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L493`
|
||
- owning-module: `messaging-admin-api`
|
||
- classification: **경로 A — 계획 (승인 불필요, dry run 무료)** **경로 B — 승인 발급 (이 리프 밖, 변경관리 시스템)** **경로 C — 실행** 경로 C 에서 검사가 세 지점(verify / Approved*Plan / guard)에 걸쳐 겹친다. `ApprovalVerifier` javadoc 이 그 이유를 설명한다. 즉 서명 검증이 통과하면 나머지는 이미 보장되지만, `ApprovedReplayPlan` 생성자와 guard 가 같은 것을 다시 본다. 방어적 중복이며 §12.3(a) 에서 다시 다룬다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-policy-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-policy.md#L423`
|
||
- owning-module: `messaging-policy`
|
||
- classification: **배치 상한이 두 축인 이유**가 적혀 있다. `checkBatch`가 각 항목에 대해 `checkPayload`도 부르므로 **개별 상한 · 개수 상한 · 총합 상한** 셋이 함께 적용된다. 프로파일 검증 실패는 `IllegalArgumentException`이다 — `MessagingException` 계층 밖이다. 시작 시점의 구성 오류이지 메시지 실패가 아니므로 일관적이다. 다만 `MessagingConfigurationException`("Raised at startup wherever possible")이 존재하는데 쓰이지 않는다 — §17의 P3.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-schema-avro-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-schema-avro.md#L457`
|
||
- owning-module: `messaging-schema-avro`
|
||
- classification: 테스트 클래스 javadoc이 세 결함을 보존한다. 세 번째가 형태상 가장 흥미롭다 — **바이트 상한이라는 올바른 도구가 잘못된 공격에 적용되어 있었다.** 테스트 javadoc이 그것을 한 문장으로 적는다: "The byte limit is the wrong instrument for this attack and was the only one in place."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식·상태 모델
|
||
- slug: `messaging-spring-cloud-stream-bridge-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L146`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- classification: **세 거절이 `DestinationProfile`의 세 필드를 직접 본다.** 즉 **`messaging-policy`가 정의한 세 보장 각각에 대해 "이것을 선언했으면 브리지를 쓸 수 없다"**를 강제한다. 세 코드 전부 `MessagingConfigurationException`이고 안정 코드를 갖는다 — `messaging-kafka-share-experimental`이 두 거절에 다른 예외 타입을 쓴 것(그쪽 §17)과 대비된다. `!enabled`도 같은 예외 타입이다 — 일관적이다. 에러 메시지가 **두 선택지를 명시한다** — "remove it from the binding or move the destination to the native adapter". 무엇을 하라고만 하지 않고 어느 쪽을 포기할지를 준다. **production 목적지는 무조건 거절한다.** guard의 세 조건을 통과한 목적지(순서 없음·재시도 없음·DLQ 없음)라도 production이면 막는다. **네 번째 게이트**다. 바인딩 이름 패턴 `[a-zA-Z][a-zA-Z0-9-]{0,63}` — 언더스코어와 점을 배제한다. **"nothing at runtime will show it"**이 이 record가 존재하는 이유다. 네 boolean과 두 factory: `gaps()`가 각 `false`마다 **문장 하나**를 만든다. **각 문장이 결과까지 적는다** — "indistinguishable", "loses the message". 상태 플래그가 아니라 운영자가 읽는 진술이다. `isFullyGuaranteed()`가 `gaps().isEmpty()`다 — 매 호출마다 네 문장을 다시 만든다. 성능 문제는 아니지만 순수 조회가 문자열을 할당한다. `accepted == true`일 때의 결과: **`messaging-core-api`의 `PublishResult` 14개 금지 조합을 전부 통과하도록 정확히 구성돼 있다** — `AMBIGUOUS`는 `confirmationLevel == NONE`, `brokerAccepted == false`, `transmission != NOT_TRANSMITTED`, `routingOutco…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Permission
|
||
- slug: `shared-contract-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/02-shared-contract.md#L69`
|
||
- owning-module: `shared-contract`
|
||
- classification: `Permission`은 정확히 한 개의 colon으로 `resource:action`을 분리하고 trim/lowercase normalization을 수행한다. 테스트는 mixed case, surrounding whitespace, blank component, 0/2+ colon을 검증한다. 다만 source는 component 내부 character set을 제한하지 않는다. 즉 "lowercase colon-delimited"는 normalization 결과이지 `[a-z0-9-]+` 같은 strict grammar는 아니다. 현재 test 역시 이를 요구하지 않으므로 observed contract로만 기록한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 40 — security-and-trust-boundaries
|
||
|
||
> 보안과 신뢰 경계 — 인가·서명·비밀·격리.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — off 계약의 두 절반
|
||
- slug: `adapter-inbound-graphql-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L130`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: 두 번째 테스트가 특히 정교하다. 리터럴 404를 단언하지 않고, 매핑된 적 없는 경로의 상태 코드와 **같은지**를 본다 — 주석이 그 이유를 적는다: "Asserting a literal 404 would have been wrong: the security filter chain runs before ...". 그리고 그 테스트는 이 leaf가 아니라 app-bootstrap에 있다. off 계약은 출하 조립에서만 검증할 수 있으므로 옳은 위치다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — 신원 모델의 프로덕션 참조 수
|
||
- slug: `adapter-inbound-web-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L426`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `AuthenticationView`를 만드는 코드도 테스트뿐이다: 즉 `security` 패키지 전체가 **자기 안에서만 서로를 부르는 닫힌 섬**이고, 바깥에서 들어오는 화살표가 없다. 교차 테넌트 가드 `rejectTenantInput`은 그 섬 안에만 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.3) 필터 체인 순서 — `publicPaths` 대 `RestrictedPathRule`
|
||
- slug: `adapter-inbound-web-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L473`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: Spring Security는 첫 일치가 이긴다. 주석은 "Ordered before the authenticated catch-all: **a management path must be refused at the transport**"라고 하는데, 그 순서는 `anyRequest()`에 대해서만 성립하고 `publicPaths`에 대해서는 반대다. §12.3. 프로덕션 `RestrictedPathRule` 생산자는 하나다 — `FileserverAdminPlaneConfiguration:36`이 fileserver 관리 경로를 등록한다. `publicPaths`의 기본값은 `${SECURITY_PUBLIC_PATHS:${PRESENTATION_API_BASE_PATH:/v1}/healthcheck}`로, 환경변수 하나로 전체가 대체된다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.4) 지문 정규화가 길이 프레이밍인가
|
||
- slug: `adapter-inbound-web-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L785`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `SemanticRequestFingerprintFactory`는 U+001F 한 글자를 구분자로 쓰고, 경로 변수는 `SEP + name + "=" + value`, 헤더는 `SEP + name + ":" + value`로 이어붙인다. 값에 대한 이스케이프나 길이 접두사가 없다. 같은 저장소의 다른 다이제스트들(notification `NotificationCatalogException.update`, messaging의 도메인 분리 상수)은 **4바이트 길이 프레이밍**을 쓰고, 그 이유를 "인접 필드 연결로 인한 충돌이 구조적으로 불가능"으로 적는다. §20.3.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 목적지 정책 — 절대 URI를 정화하지 않고 거부한다
|
||
- slug: `adapter-outbound-httpclient-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L474`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: `TrustedTargetPolicy`의 규칙 — "An absolute URI is **rejected here rather than sanitised**: H2 exists to vary method, relative path, query, approved headers, and body — not the destination. Changing the destination is what H3 is for, and H3 has its own policy, credentials, and DNS validation." `requireRelativeTemplate`가 빈 템플릿, `//` 시작, `://` 포함, `/`로 시작하지 않음을 거부한다. 확장은 문자열 연결이 아니라 Spring `DefaultUriBuilderFactory`의 `TEMPLATE_AND_VALUES` 인코딩이라 "a value containing `/`, `?`, or `#` cannot change the shape of the request." 그리고 확장 **후에** `requireAllowedOrigin`이 host/port allowlist를 다시 본다. 멱등성 키 처리에 수정 이력 둘이 붙어 있다. 그리고 리다이렉트 hop에 allowlist를 다시 적용하는 `requireAllowedTarget`이 public인 이유도 적혀 있다 — 조정자가 이전에는 리다이렉트 정책만 보고 프로파일 allowlist를 보지 않아 "An upstream could therefore redirect a trusted profile to any origin the redirect policy tolerated, including one the operator had explicitly excluded."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 교정 — 영구 TLS 실패의 `CONNECT` 분류는 분류기 결함이 아니라 픽스처의 듀얼스택 호스트명이다
|
||
- slug: `adapter-outbound-httpclient-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/11-adapter-outbound-httpclient.md#L627`
|
||
- owning-module: `adapter-outbound-httpclient`
|
||
- classification: 이 절은 이전 사이클이 여기에 적었던 **P1 진단을 철회하고 교체한다**. 관측된 실패는 그대로 재현되지만, 그 원인으로 지목했던 기전은 측정으로 반증되었다. 근거는 `EVD-332`다. `:adapter:outbound:httpclient:test` 는 HEAD 에서도 **283 중 3건 실패**한다. 세 건 모두 `MutualTlsHandshakeContractTest.java:168` — `assertThat(classified.stage()).isEqualTo(TLS_HANDSHAKE)` 다. 바로 앞줄인 167행(`evidence == NOT_SENT`)은 통과한다. 이전 사이클의 주장은 이랬다. **틀렸다.** 그 진단은 `ApacheFailureClassifier.recognize`의 분기 순서(pool → DNS → CONNECT → TLS)를 읽고 사슬의 모양을 추론한 것이지, 사슬을 실제로 떠본 것이 아니다. 잡힌 예외를 그대로 출력하면 이렇다. 사슬에 `SSLHandshakeException`이 **없다**. 그림자에 가려진 것이 아니라 애초에 도착하지 않았다. 분류기는 자기가 받은 것을 정확히 분류했다. 동일한 서버 객체, 동일한 클라이언트 신뢰재료. `baseUrl`의 호스트 문자열만 바꿨다. 이 컨테이너의 `/etc/hosts`는 `localhost`를 두 패밀리에 준다. `MockWebServer`는 IPv4 루프백에만 바인딩하고, `MockHttpServer.uri()`는 호스트명 `localhost`를 돌려준다 (`MockHttpServer.java:70-72`). Apache HttpClient 5의 연결 오퍼레이터는 해석된 주소를 순회하면서 **마지막이 아닌 주소의 실패를 삼킨다**. 호출자에게 도달하는 유일한 예외는 두 번째 주소의 연결 거부다. 핸드셰이크가 **성공하는** test가 통과하는 이유도 같은 루프다. 127.0.0.1 에서 성공하면 루프가 즉시 반환하므로 `::1`을 시도하지 않는다. 따라서 처음 눈에 띄었던 `startTls(..., true/false)` 차이는 원인이 아니라 상관관계였다 — 실패하는 케이스가 곧 두 번째 주소까지 가는 케이스다. `startTls(..., fals…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.4) 문서/카운트 드리프트 — 어떤 상태가 unhealthy인가
|
||
- slug: `adapter-outbound-notification-c04`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L619`
|
||
- owning-module: `adapter-outbound-notification`
|
||
- classification: `NotificationHealthReporter.snapshot()`이 `healthy = false`로 넘어가는 조건은 넷이다: 세 번째 조건에는 이 저장소 특유의 자기고발이 붙어 있다: "A platform with providers but no route accepts every request and delivers none. It was reported healthy because every runtime was healthy — **which was true and beside the point**." 여기서 눈에 띄는 것은 **`DRAINING`이 목록에 없다**는 점이다. 로테이션 중 드레인은 정상 운영이므로 그 자체로는 옳다. 그러나 §13의 P2와 겹치면 부작용이 하나 더 생긴다 — 아래 §21.2.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 후보로 본 unguarded split은 값 타입이 막고 있다
|
||
- slug: `adapter-outbound-objectstorage-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L135`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: `RoutingObjectReadAdapter.load`가 `reference.canonicalText().split("\\.", -1)[1]`로 route token을 꺼낸다. 인덱스 검사가 없어 처음에는 `ArrayIndexOutOfBoundsException` 후보로 봤다. `ObjectReference`를 확인한 결과 생성자가 `ObjectIdentitySupport.requireRouted(canonicalText, "osr1")`로 형태를 강제하므로, 유효하게 만들어진 참조에는 항상 route 구획이 있다(`150-...` §8.4d). 결함이 아니다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — tenant repository/listener guard가 곧 production isolation이라는 주장
|
||
- slug: `adapter-outbound-persistence-jpa-c53`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3784`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `TenantAwareRepositoryGuard`와 `TenantEntityListenerGuard`의 local behavior는 fail-closed지만 현재 production repository/entity에 연결된 caller/listener registration은 없다. 따라서 이 type들이 존재한다는 이유만으로 현재 application의 tenant isolation이 보장된다고 쓰지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — global masking도 이 보장을 복구하지 않는다
|
||
- slug: `adapter-outbound-support-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L207`
|
||
- owning-module: `adapter-outbound-support`
|
||
- classification: `app-bootstrap`의 `LogMaskingPatterns`는 방어 심층화로 다음과 같은 secret 형태를 mask한다. password/secret/token/api-key 계열 key=value Authorization credentials standalone Bearer token 그러나 arbitrary email address나 free-form body PII를 일반적으로 제거하는 규칙은 없다. app-bootstrap README 자체도 regex masking을 **보증이 아니라 defence-in-depth**라고 설명한다. 따라서 현재 “logger signature 때문에 PII가 들어올 수 없다”는 1차 방어선 설명은 사실과 맞지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — outbound peer isolation
|
||
- slug: `adapter-outbound-support-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L368`
|
||
- owning-module: `adapter-outbound-support`
|
||
- classification: `CleanArchitectureTest.OUTBOUND_ADAPTERS_ARE_PEERS_SHARING_ONLY_SUPPORT`는 outbound adapter family를 slice로 나누고 서로 직접 의존하지 못하게 한다. 유일한 shared-code 예외는 target package가: 인 dependency다. 따라서 messaging → notification 같은 peer coupling은 금지하지만 messaging → support는 허용한다. fresh `CleanArchitectureTest --rerun-tasks`도 통과했다. 이 구조는 support 모듈이 단순 편의 library가 아니라 **outbound family에서 sanctioned shared dependency point**라는 점을 build-time fitness function으로 고정한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — 레지스트리 계약이 실제 레지스트리 파일을 읽는가
|
||
- slug: `app-bootstrap-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/18-app-bootstrap.md#L460`
|
||
- owning-module: `app-bootstrap`
|
||
- classification: `MasterSwitchRegistryContractTest`가 `docs/registries/env-keys.yaml`과 `src/app-bootstrap/src/main/resources/application.yml`을 실제로 읽어 대조한다(§2). `ErrorCodeRegistryMappingTest`·`SecretsClassificationRegistryTest`도 `docs/registries/` 아래 파일을 읽는다. 파일 기반 SSOT가 테스트로 고정돼 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — authorization: permission과 object access를 분리한다
|
||
- slug: `application-core-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L78`
|
||
- owning-module: `application-core`
|
||
- classification: `AuthorizationPort`는 principal의 raw role/permission을 기준으로 “이 종류의 작업을 수행할 수 있는가”를 판정하는 framework-free PEP다. `AuthorizationPrincipal`은 role set을 defensive copy + unmodifiable로 만들고 null roles는 empty set으로 정규화한다. `AuthorizationDeniedException`은 Spring `AccessDeniedException` 대신 application-owned failure를 사용한다. object-level access는 별도 `ObjectAccessPolicy`가 담당한다. 같은 permission을 가진 사용자라도 ownership, membership, workflow state에 따라 특정 object 접근 결과가 달라질 수 있기 때문이다. `ObjectAccessDecision`은 denial에 stable code를 요구하고 `hideExistence`를 별도 boolean으로 보존해 transport가 403/404 disclosure 정책을 추측하지 않게 한다. **Historical evidence.** `ObjectAccessPolicyTest`에는 이 계약이 과거 inbound GraphQL adapter에 있었고 GraphQL request context를 signature에 포함해 application-core가 구현하려면 transport에 역의존해야 했던 문제가 기록돼 있다. 현재 regression test는 policy/request/decision signature에 `dev.caskeleton.adapter.*` 타입이 다시 등장하면 실패한다. 이 프로젝트에서 “여러 호출자가 공유해야 하는 계약을 inbound adapter가 소유하면 Core가 Adapter에 의존하게 된다”는 문제가 실제로 있었던 근거다. `decideAll()`의 default는 요청 순서를 보존하지만 object마다 `decide()`를 호출한다. set-based authorization을 제공하는 구현체가 override하지 않으면 batched loading 안에…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 승격 게이트
|
||
- slug: `grpc-advanced-bootstrap-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md#L89`
|
||
- owning-module: `grpc-advanced-bootstrap`
|
||
- classification: 증거는 능력마다 따로 기록된다. 일곱 항목(호환성·보안 검토·고장·성능·ADR·런북·실환경 테스트)과 담금 기간을 본다. 임계값이 둘이다. 그리고 `WATCH` 는 `EXPERIMENTAL` 을 먼저 거쳐야 한다. `GrpcAdvancedSupportMatrix.apply` 는 결정의 시작 등급이 현재 등급과 다르면 거부한다 — 두 승격이 경합했거나 하나가 재생된 경우다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체
|
||
- slug: `grpc-advanced-diagnostics-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-diagnostics.md#L41`
|
||
- owning-module: `grpc-advanced-diagnostics`
|
||
- classification: 진단 표면(Channelz·CSDS)과 그것을 게시 가능하게 만드는 편집기, 그리고 고급 능력이 무엇을 상대로 검증되어야 하는지를 이름 짓는 테스트킷 계약을 담는다. 편집기 javadoc 이 왜 이것이 필요한지 적는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `grpc-observability-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-observability.md#L46`
|
||
- owning-module: `grpc-observability`
|
||
- classification: 세 층위를 구별한다 — 논리 RPC, 물리 시도, 스트림 수명주기. Micrometer 를 `api` 로 노출하는 이유도 build.gradle 에 적혀 있다 — "the observation convention's public signatures name Micrometer types, so wiring it requires naming them." 실제로 `GrpcObservationConvention` 의 생성자와 `boundedTags` 반환형이 Micrometer 타입(`MeterRegistry`, `Tags`)이므로 그 서술은 코드와 일치한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 격리 규칙
|
||
- slug: `grpc-spring-boot-starter-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-spring-boot-starter.md#L42`
|
||
- owning-module: `grpc-spring-boot-starter`
|
||
- classification: 격리 규칙은 세 겹이다 — 레지스트리, 빌드 검증 태스크, 그리고 자바 쪽 단언. 세 번째는 `validateAdvancedIsolation` 이 `GrpcStableBuildInvariant.requireNoAdvancedDependency` 를 부르는 형태다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-admin-api-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L98`
|
||
- owning-module: `messaging-admin-api`
|
||
- classification: `messaging-core-api` 에서 쓰는 것: `DestinationName`, `MessageAuthorizationException`, `MessagingConfigurationException`. `messaging-policy` 는 import 0건이다(§12.4). leaf 밖 소비자 21개 파일 / 4개 모듈: 부팅된 애플리케이션에서 이 리프의 타입 중 실제로 살아나는 것은 **둘뿐**이다(`EVD-302`, `EVD-303`). `MessagingAdminService` 빈은 없고 `ApprovalVerifier` 빈도 없다. 이는 명시된 설계다. 그러나 이 스탠스가 **토폴로지 검증까지 덮지는 않는다** — §12.1 과 §17 의 첫 항목이 그것이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-admin-api-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-api.md#L543`
|
||
- owning-module: `messaging-admin-api`
|
||
- classification: 전부 `MessageAuthorizationException` 또는 `MessagingConfigurationException` 이고, 코드가 붙어 있다. 메시지가 전부 "무엇이 왜 거절되었는가" 를 서술형으로 쓴다. 예: `IllegalArgumentException` 은 **구조적으로 불가능한 값**에만 쓴다 — 음수 카운터, 빈 문자열, 역전된 윈도우, 4-eyes 위반. 인가 실패와 프로그래밍 오류가 예외 타입으로 갈린다. `toString()` 하나가 로그 위생을 명시적으로 다룬다. `ApprovalGrant` 는 record 라 기본 `toString()` 이 전 필드를 찍는다는 점은 대비된다 — 다만 `ApprovalGrant` 자체가 로그에 닿는 경로는 확인되지 않았다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 자격증명은 연결 시도마다 해석된다
|
||
- slug: `messaging-rabbit-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-rabbit.md#L106`
|
||
- owning-module: `messaging-rabbit`
|
||
- classification: `AmqpCredentials` 가 record 가 아니라 class 인 이유도 적혀 있다 — 비밀을 지우려면 가변이어야 하고, record 가 `char[]` 를 동등성에 쓰면 같은 자재를 가진 둘이 서로 다르다고 판정된다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 41 — capability-and-disclosure-models
|
||
|
||
> 능력과 공시 모델 — 무엇을 할 수 있다고 말하고 그것을 무엇이 뒷받침하는가.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 두 자동설정의 게이트
|
||
- slug: `adapter-inbound-web-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L116`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: 둘 다 `matchIfMissing = true` — **기본 켜짐**이다. notification·messaging·cache-redis가 전부 `matchIfMissing = false`(옵트인)인 것과 반대인데, 이유가 다르다: 저쪽은 선택적 능력이고 이쪽은 웹 애플리케이션의 본체다. 상호배타성은 프로퍼티가 아니라 `@ConditionalOnWebApplication`의 타입 수준에서 온다 — "a reactive application cannot accidentally activate the servlet filters even if both artifacts are on the classpath." 두 자동설정이 등록하는 빈은 MVC 12개, WebFlux 11개다. `AutoConfiguration.imports`에는 이 둘만 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 하나의 스위치가 두 능력을 켠다
|
||
- slug: `adapter-inbound-web-c17`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1255`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `NDJSON`과 `JSON_SEQUENCE`는 enum에서 서로 다른 상수이고 각자 프로퍼티 이름을 갖는데, 실제로는 `ndjson` 스위치 하나가 둘을 함께 켠다. `WebAdvancedFeature`의 javadoc이 금지한 형태다 — "A single switch would make those one decision."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — "컴파일이 먼저, 생성은 나중"이 실제 순서다
|
||
- slug: `adapter-outbound-objectstorage-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L63`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: `ObjectStorageProviderContribution`이 두 메서드의 계약을 나눈다. `ObjectStorageCapabilityAssembler.assemble`이 그 순서를 지킨다 — `compiler.compile(settings)`가 **전부** 끝난 뒤(`:25`)에야 선택된 destination을 돌며 `contribution.create(provider)`를 부른다(`:48`). 그리고 도중에 실패하면 이미 만든 것을 **역순으로** 닫는다(`:53–56`). `AssembledCapability.close()`도 역순이고 `AtomicBoolean`으로 정확히 한 번만 실행된다. README의 "Settings compile fully before any selected provider creates a directory, client, thread, scheduler, or credential lookup"이 코드 구조로 성립한다. 컴파일러 자체가 fail-closed다. 비활성이면 빈 바인딩을 돌려주고, 활성인데 provider·destination·default destination 중 하나라도 비면 거부한다. provider마다 `describe`가 돌려준 서술자와 설정을 **대조**한다 — providerType 일치, version 일치, `maximumObjectBytes`가 서술자 상한 이하, `chunkBytes`가 서술자 상한 이하. chunk는 추가로 `1 ≤ chunk ≤ min(maxObject, 16 MiB)`이고 `Integer.MAX_VALUE`를 넘지 못한다. destination은 route token 중복을 거부하고, 요구한 capability를 provider가 `SUPPORTED`로 신고하지 않으면 거부하며, `SCAN_CLEAN`을 요구하는데 scanner seam이 없으면 이름을 대며 거부한다. 식별자 검증도 좁다 — `canonicalId`는 64자 이내, `[a-z0-9][a-z0-9_-]*`, 소문자, 그리고 **0x20–0x7e 밖 문자를 전부 거부**한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — §41 보강 — 레지스트리는 문서 주장을 얼어붙히지만 런타임 설정 경로는 덮지 않는다
|
||
- slug: `adapter-outbound-objectstorage-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/09-adapter-outbound-objectstorage.md#L560`
|
||
- owning-module: `adapter-outbound-objectstorage`
|
||
- classification: §41에서 "R0 경계가 문서에만 있다"고 적었다. §47을 반영해 정확히 다시 말한다. R0 경계는 **문서 주장에 대해서는** 기계 검사된다(§47). 그러나 그 검사의 대상은 `docs/registries/object-storage-readiness.yaml`이고, `KNOWN_PROVIDERS`는 `filesystem-local-dev` 하나다. 운영자가 `app.object-storage` 설정에 AWS provider용 qualification profile을 쓰면서 `DIRECT_UPLOAD` capability를 주장하는 경로는 이 레지스트리를 **거치지 않는다**. `S3ProviderBinding.compileProfiles`가 그 주장을 MinIO에 대해서만 거부하므로, AWS + DIRECT_* 조합은 여전히 compile을 통과하고 presigner를 할당한다(§41). 따라서 §41의 판정은 유지되고 오히려 선명해진다 — 이 저장소에는 "이 카드는 R0"를 강제하는 장치가 이미 있는데, 런타임 설정 경로가 그 장치의 사정권 밖에 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 이 sub-scope는 하나의 query framework가 아니라 세 단계의 정책층이다
|
||
- slug: `adapter-outbound-persistence-jpa-c24`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1610`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: 현재 code shape는 대략 다음처럼 읽는 것이 맞다. 중요한 점은 `springdata`와 `querydsl`이 application-core의 repository contract를 대체하는 generic CRUD layer가 아니라는 것이다. `JpaRepositoryFragmentSupport`에는 범용 `save/findAll/delete`가 없고, domain-owned adapter가 필요한 query mechanism만 조합하게 설계돼 있다. 이 방향은 support matrix의 “platform-owned generic CRUD repository는 unsupported”와 일치한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 세 개의 환경 검증기
|
||
- slug: `app-bootstrap-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/18-app-bootstrap.md#L179`
|
||
- owning-module: `app-bootstrap`
|
||
- classification: `MasterSwitchEnvironmentPostProcessor`(스위치 값 문법) · `RuntimeEnvironmentProfileValidator`(93, 프로파일) · `CapabilityDependencyEnvironmentValidator`(62 → `CapabilityDependencyValidator` 156, 능력 간 의존). 셋 다 `EnvironmentPostProcessor`이고 관심사가 다르다. 중복 아님.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — — 다섯 어댑터 범위는 런타임 멤버십 레지스트리와 일치한다 (결함 아님)
|
||
- slug: `app-bootstrap-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/18-app-bootstrap.md#L185`
|
||
- owning-module: `app-bootstrap`
|
||
- classification: §3.1의 표를 처음에는 "출하되는 스위치가 다섯보다 많다"는 결함으로 기록했다. 그 판정은 **틀렸다**. `src/config/architecture/modules.json`의 `runtime_memberships`가 결정적이다: **gRPC와 WebSocket은 build-only leaf다** — 어떤 런타임에도 올라가지 않는다. 그리고 그 사실이 기계로 강제된다: 그 테스트의 javadoc이 세 전송의 등급을 나눈다: 따라서 두 어댑터가 `MasterSwitch`·`env-keys.yaml`·`AdapterActivationReport`에 없는 것은 누락이 아니라 **일관성**이다. 런타임에 오르지 않는 어댑터에는 운영자용 활성화 스위치가 필요하지 않다. **남는 것은 `backend.web.*` 하나다.** `adapter-inbound-web`은 두 런타임에 올라가고(`["app-bootstrap","sample-portfolio"]`), 그 스위치들 — `backend.web.mvc.enabled` · `backend.web.webflux.enabled`(둘 다 `matchIfMissing=true`, **기본 켜짐**) · `backend.web.budgets.enabled` · `app.web-platform.durable-operations.enabled` — 은 `MasterSwitch`에도 `env-keys.yaml` 341개 키에도 없다. §4.1b.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.4) 카운트 — `.imports` 여섯 줄과 다섯 능력
|
||
- slug: `app-bootstrap-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/18-app-bootstrap.md#L264`
|
||
- owning-module: `app-bootstrap`
|
||
- classification: `.imports`의 여섯 항목 중 다섯이 능력 루트이고 하나(`AdapterActivationAutoConfiguration`)가 자기 액추에이터다. `MasterSwitch`의 다섯과 일치한다 — 단 `PERSISTENCE_MONGO`는 `.imports`에 루트가 없고 컴포넌트 스캔 제외 정규식(`adapter\.outbound\.mongo\..*`)으로만 관리된다. §7.1.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 두 겹의 게이트
|
||
- slug: `grpc-advanced-diagnostics-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-diagnostics.md#L51`
|
||
- owning-module: `grpc-advanced-diagnostics`
|
||
- classification: `GrpcChannelDiagnosticsPolicy` 는 네트워크와 역할 두 게이트를 모두 요구하고, 하나라도 비면 생성자가 거부한다. 그리고 등록 판정이 능력 깃발에 걸려 있다. CSDS 는 Channelz 가 켜져 있고 xDS 도 켜져 있을 때만 등록된다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-kafka-share-experimental-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L399`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- classification: 이 leaf의 javadoc에 **이전 결함 서술이 없다.** 다른 messaging leaf 대부분이 "X used to …" 형태의 기록을 갖는 것과 대비된다. 대신 **막으려는 것**을 셋 적는다. 세 번째가 이 leaf에서 가장 성숙한 판단이다 — **거절이 무시보다 낫다**는 원칙이고, `messaging-core-api`의 `MessagingCapabilityUnavailableException` javadoc과 같은 계열이다. 역설적으로 **그 원칙이 `register(...)`에는 적용되지 않았다** — spec을 받아 무시하고 성공을 반환한다(§17).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 주요 실행 경로
|
||
- slug: `messaging-testkit-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-testkit.md#L452`
|
||
- owning-module: `messaging-testkit`
|
||
- classification: **경로 A — 어댑터 계약 실행 (컨테이너 불필요, 항상 실행)** `MessagingAdapterHarness extends AutoCloseable` 이고 `close()` 가 checked exception 을 던지지 않도록 재선언되어 있다(`MessagingAdapterHarness.java:71-72`). 7개 테스트 전부 `try (…)` 로 감싸므로 하니스 누수 경로가 없다. **경로 B — 인증 증거 생산 (컨테이너 필요, `test` 에서 제외)** **경로 C — 등급 판정 (컨테이너 불필요, 매 빌드)** 경로 C 가 경로 B 없이도 돌고, 경로 B 가 없으면 매니페스트가 비어 등급 주장이 무너진다는 것이 설계의 핵심이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 42 — query-and-pagination-models
|
||
|
||
> 질의와 페이지네이션 모델 — 비용과 신뢰 경계를 타입으로 제한하기.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.1) 도달성 — 네 패키지의 배선 상태
|
||
- slug: `adapter-inbound-graphql-c10`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L719`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: 58개 main 파일 중 자동설정이 참조하는 것은 **둘**이다 — `GraphQlBatchPolicyRegistry`(4) · `GraphQlDataLoaderFactory`(4). 나머지 56개는 autoconf=0이다. `dataloader`는 `runtime/GraphQlBatchLoaderRegistrar`를 통해 도달하므로 배선돼 있다. `fetch`·`pagination`·`mutation` 41개 파일은 어떤 배선 경로에도 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.2) durable-operation HTTP 표면의 두 게이트
|
||
- slug: `adapter-inbound-web-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L762`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `app.web-platform.durable-operations` 문자열은 저장소의 어떤 yaml에도 없다. 그리고 켜더라도 생성자가 요구하는 `OperationQueryService` 빈을 선언하는 코드가 main·app-bootstrap에 없다(testkit에만 생성). §16.3과 같은 형태 — 게이트를 켜면 부팅이 실패한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Spring Data repository support는 generic CRUD보다 query execution policy에 가깝다
|
||
- slug: `adapter-outbound-persistence-jpa-c27`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1860`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `JpaRepositoryFragmentSupport`는 domain-specific repository adapter가 사용할 공통 실행 support다. `EntityManager` access query name context fetch plan application bounded query observation scope 이고 범용 business repository contract는 제공하지 않는다. 이 구조는 Clean Architecture 관점에서 의미가 있다. application-core가 `JpaRepository`, `EntityManager`, `Specification`을 알 필요가 없고, 실제 domain repository port를 구현하는 outbound adapter 내부에서만 Spring Data/JPA mechanics를 사용한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — keyset predicate는 mixed type / mixed direction을 표현하도록 진화했다
|
||
- slug: `adapter-outbound-persistence-jpa-c28`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1933`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `KeysetPredicateBuilder`는 conjunction이 아니라 lexicographic predicate를 만든다. 예를 들어 `(createdAt ASC, id DESC)`라면 cursor 뒤는 개념적으로: 현재 `KeysetTerm<T>`는 각 term마다: cursor value 그래서 `(Instant, UUID)`처럼 term type이 다르고 direction도 다른 ordering을 표현할 수 있다. source history에는 과거 one-type/one-direction API가 mixed order에서 rows를 skip/repeat했던 이유가 주석으로 남아 있고, 현재 code/test는 이를 보완했다. builder는 “마지막 term이 unique tie-breaker여야 한다”고 문서화하지만 runtime에서 uniqueness를 증명할 metadata는 받지 않는다. 검사할 수 있는 것은: 따라서 uniqueness는 caller/registry contract다. 현재 evidence만으로 이를 defect라 단정하지 않는다. platform이 이를 fail-closed invariant로 승격하려면 unique-key metadata까지 contract에 포함해야 한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — keyset execution은 `size + 1`로 hasNext를 판정하고 count query를 제거한다
|
||
- slug: `adapter-outbound-persistence-jpa-c29`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L1973`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `JpaKeysetQuerySupport`는: 반환은 최대 `size`개이고 추가 1개로 `hasNext`를 판단한다. 이 path에는 `COUNT(*)`가 없다. 즉 keyset을 도입해 OFFSET full-walk 비용을 줄여 놓고 total count로 다시 full-work를 추가하는 구조를 피한다. 실제 PostgreSQL readiness query도 `(occurred_at,id) > (?,?) ORDER BY ... LIMIT ?` 형태와 representative index 사용을 별도 integration lane에서 검증한다. 해당 entire integration lane 자체는 later sub-scope 11의 denominator이므로 여기서는 cross-scope evidence로만 사용한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈 경계와 빌드 의존성
|
||
- slug: `application-core-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/03-application-core.md#L58`
|
||
- owning-module: `application-core`
|
||
- classification: **Observed.** `build.gradle`의 production project dependency는 `:shared-contract` 하나뿐이다. application-core가 Spring, JPA, Redis, Kafka, filesystem provider 같은 구현 모듈을 직접 참조하지 않고, 외부 구현은 composition root와 adapter가 역으로 이 모듈의 port를 구현한다. **Observed.** `CommandUseCase<C extends Command,R>`와 `QueryUseCase<Q extends Query,R>`는 `UseCase<I,O>.handle(I)`를 write/read intent에 맞게 타입으로 좁힌다. 자체적으로 transaction을 열거나 security interceptor를 실행하지 않는다. 실행 정책은 `@UseCaseCapability`에 별도로 선언된다. **Observed.** `@UseCaseCapability`는 runtime TYPE annotation이며 `transactionMode`, `idempotency`, `repositoryAccess`를 필수로 받고 `externalOutboundAllowed`, `sensitiveRead`, `bulkWrite`, `crossTenantAdmin`을 추가 선언한다. annotation 자체는 metadata에 불과하지만 `CleanArchitectureTest`가 concrete Command/Query use case에 annotation 존재를 강제한다. **Observed.** architecture fitness function은 다음 coherence를 직접 검사한다. `READ_ONLY + READ_REPOSITORY`는 `TransactionPort.inRead`를 직접 호출해야 한다. `WRITE + WRITE_REPOSITORY`는 `inWrite` 또는 `inRootWrite`를 직접 호출해야 한다. `REQUIRES_NEW`는 `inNew`를 직접 호출해야 한다. `repositoryAccess != WRITE_REPOSITORY`인 use case가 repository write ve…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 43 — observability-models
|
||
|
||
> 관측 모델 — 무엇을 재고 무엇을 태그로 두지 않는가.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 상관 식별자가 세 벌이다
|
||
- slug: `adapter-inbound-web-c15`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1108`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: 세 번째는 전송이 달라 공존이 정상이다. 앞의 둘은 같은 서블릿 체인에서 같은 헤더를 두 번 처리한다. `traceparent`도 마찬가지로 두 번 파싱되며, `RequestLoggingFilter`는 응답에도 `traceparent`를 쓰고(`:68`) `WebMvcRequestIdFilter`는 쓰지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `audit`와 `auditing` 두 경로
|
||
- slug: `adapter-outbound-persistence-jpa-c44`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L3123`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: manual `AuditableEntity`/`AuditContextPort` 경로와 Spring Data `AuditMetadata`/`JpaAuditingConfiguration`이 함께 존재하지만 tests/docs가 후자를 candidate/dormant로 명시하고 default composition도 canonical manual audit 경로만 사용한다. 현재 중복 활성화 defect로 판정하지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `FailOpenDependencyLogger`: 진단을 business outcome과 분리하려는 계약
|
||
- slug: `adapter-outbound-support-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L119`
|
||
- owning-module: `adapter-outbound-support`
|
||
- classification: `logSuccess(...)`는 DEBUG로 다음 정보를 기록한다. dependency_name dependency_type outcome=`SUCCESS` correlation_id `logFailure(...)`는 WARN으로 다음을 추가한다. outcome=`FAILURE` error=`<exception simple class>: <cause.getMessage()>` README와 javadoc은 WARN을 선택한 이유를 “optional fail-open dependency가 실패해도 core use case 자체는 성공했기 때문”이라고 설명한다. 이 logger 자체는 retry, recovery, fallback을 수행하지 않는다. **실패 정책을 결정하는 주체가 아니라 이미 결정된 fail-open outcome을 관측하는 기술 seam**이다. repository-wide production reference scan에서 support package를 직접 import하는 current production files는 네 개뿐이었다. `MessagingConfig` `OutboundMessagePublisher` Notification: `NotificationConfig` `FailOpenNotificationProvider` 반대로 support README가 “공유 consumer”로 설명하는 `cache-redis`, `httpclient`는 Gradle dependency는 유지하지만 support production type을 직접 참조하지 않는다. 이 차이는 §8에서 별도로 다룬다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — architecture suite / dependency registry
|
||
- slug: `adapter-outbound-support-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L558`
|
||
- owning-module: `adapter-outbound-support`
|
||
- classification: `CleanArchitectureTest --rerun-tasks`: BUILD SUCCESSFUL `verifyCleanArchitectureDependencies`: BUILD SUCCESSFUL 이 둘은 source/package/project dependency constraint를 증명하며 diagnostics runtime failure나 PII behavior를 증명하지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 계약·불변식
|
||
- slug: `grpc-observability-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-observability.md#L74`
|
||
- owning-module: `grpc-observability`
|
||
- classification: `violations(Map)` 의 판정 순서가 셋이다. 클래스 javadoc 이 두 목록이 겹치는 이유를 적는다 — "Everything unlisted is refused anyway; naming the dangerous ones gives the refusal a message that says why rather than just that." 즉 `FORBIDDEN_TAGS` 는 판정을 바꾸지 않고 진단만 바꾼다. 두 번째 분기가 이미 그것들을 거절한다. 허용 태그 8개: `grpc.service` · `grpc.method` · `grpc.rpc_type` · `grpc.status` · `grpc.channel_profile` · `grpc.completion_outcome` · `grpc.retry_bucket` · `grpc.stream_termination_reason`. 명시적 거절 11개: `actor_id` · `tenant_id` · `object_id` · `stream_id` · `idempotency_key` · `request` · `response` · `metadata` · `authorization` · `error_detail` · `trace_id`. UUID · `sha256:` 접두 · `bearer ` 접두. 숫자 id, 이메일, 호스트명은 잡히지 않는다. 그리고 `.` 은 기본적으로 개행에 맞지 않으므로 값에 개행이 섞이면 `matches()` 가 거짓이 된다. `retryBucket(int)` 이 1-based 시도 수를 받아 `0`/`1`/`2`/`3+` 로 접는다. 0 이하는 던진다. javadoc 이 이유를 적는다 — "an attempt count is unbounded in principle and the distinction anyone acts on is first attempt, one retry, several." `GrpcRpcObservation` javadoc: 그 분리가 `GrpcObservationConvention.record(GrpcRpcObservation)` 에서 실제로 그렇게 구현되어 있다 — `RPC_DURATION` 타이머는 1회, `RPC_ATTE…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 실패 경로와 복구/번역
|
||
- slug: `messaging-admin-runtime-c05`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L472`
|
||
- owning-module: `messaging-admin-runtime`
|
||
- classification: 마지막 두 줄이 §12.3(a)의 요약이다 — 같은 코드 문자열, 다른 예외 타입, 다른 판정 규칙. `attempt(...)` 가 모든 `RuntimeException` 을 삼키는 것은 근거가 있지만 대가도 있다: 실패 사유가 어디에도 남지 않는다. 감사 이벤트는 `failed` 개수만 담고(`:135`), 어떤 메시지가 왜 실패했는지는 기록되지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-observability-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-observability.md#L70`
|
||
- owning-module: `messaging-observability`
|
||
- classification: 들어오는 것: `messaging-core-api`(api), `micrometer-core`(api). 나가는 것: `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`. **출하 조립은 두 개뿐이다.** 두 클래스는 `MessagingMetrics`의 생성자 인자다. 그런데 `MessagingMetrics` bean이 없다(§12.1). 즉 **재료 둘만 bean으로 있고 그것을 조립하는 것이 없다.** `MessagingTracer`·`MessagingAuditSink`·`DefaultMessagingObservationConvention`은 bean도 없고 소비자도 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
|
||
## TOPIC 44 — composition-and-lifecycle-models
|
||
|
||
> 조립과 수명주기 모델 — 무엇이 언제 만들어지고 언제 사라지는가.
|
||
> 이 Topic 의 노드는 CONCEPT recall pass 에서 나왔다 — 61개 canonical SSOT 본문이 설명하는
|
||
> **구현된 메커니즘**이고 §17 의 문제 finding 이 아니다. `source` 가 그 SSOT 절의 정확한 앵커다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 연산 정체성을 정하는 두 구현
|
||
- slug: `adapter-inbound-graphql-c03`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L336`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `GraphQlOperationNameInterceptor.apply(...)`(미배선)와 `runtime/GraphQlOperationSelectionHandler`(autoconf=2, 배선됨)가 같은 일을 한다. **배선된 쪽이 더 많이 한다**: 익명 연산 거부 · 다중 연산 시 `operationName` 요구 · 연산 정체성 정규화가 전부 배선된 경로에 있다. 정규화 규칙에는 근거도 붙어 있다 — "any name that cannot survive normalisation becomes the anonymous identity rather than being rejected — **a naming convention is not a reason to refuse an otherwise valid request**." 따라서 미배선 인터셉터는 **누락이 아니라 중복**이다. 다만 그것이 쓰는 `GraphQlOperationNamePolicy`(85줄, 참조자 = 인터셉터와 자기 자신뿐)도 함께 미배선이고, 배선된 핸들러는 다른 정책 객체를 쓴다. §12.2.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — 클라이언트 정책이 어떻게 정해지는가
|
||
- slug: `adapter-inbound-graphql-c06`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L471`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: 설계는 매니페스트 조회를 말한다: 자동설정은 단일 빈을 만든다: 그리고 그 하나가 여덟 개 빈(`:123` · `:237` · `:380` · `:389` · `:397` · `:453` · `:463` …)에 주입된다. 매니페스트는 만들어지지 않는다. §16.2. **프로파일 자체는 신뢰된 경로에서 온다** — `GraphQlAuthenticationContextFactory:59`가 `principal.clientProfile()`을 쓰고(검증된 principal), 미인증 호출자에는 `GraphQlPlatformWebInterceptor`의 `anonymousProfile`이 붙는다. 즉 `GraphQlClientProfileResolver`가 막으려는 노출(호출자가 자기 프로파일을 지정)은 배선된 경로에서도 발생하지 않는다. 그 타입은 중복이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — b 그 결과 — HTTP 전송 계약 계층이 미배선이고 실제 전송은 프레임워크가 정한다
|
||
- slug: `adapter-inbound-graphql-c09`
|
||
- readiness: `READY`
|
||
- source: `analysis/16-adapter-inbound-graphql.md#L660`
|
||
- owning-module: `adapter-inbound-graphql`
|
||
- classification: `http` 패키지 19개 파일 중 자동설정이 값으로 소비하는 둘(`GraphQlHttpProfile` autoconf=2, `GraphQlJsonStructurePolicy` autoconf=4)을 빼면, 나머지는 실행되지 않는다: GraphQL-over-HTTP에서 상태 코드 규칙은 미디어 타입에 달려 있다 — `application/json`은 실행 오류에도 200을, `application/graphql-response+json`은 실제 상태를 쓴다. 그 규칙을 `GraphQlHttpStatusMapper`와 `GraphQlAcceptHeader`가 담고 있고, 실제로 응답을 만드는 것은 Spring GraphQL이다. **노출이 아니라 통제권의 문제다.** Spring GraphQL 자신이 GraphQL-over-HTTP 스펙을 구현하므로 동작은 합리적이다. 잃는 것은 (a) 이 플랫폼이 선언한 프로파일(`V1`)이 실제 동작과 일치한다는 보장, (b) 사전 파싱 한계 중 봉투 검증기에만 있는 부분, (c) "새 결과 종류가 임의 상태를 갖고 한 호출 지점에 생기는 것"을 막겠다는 단일 팩토리의 목적. **실패 시나리오** — 운영자가 `GraphQlPlatformConfigurationReport`(§8.1을 고쳐 발행하게 된 뒤)에서 `httpProfile=V1`을 읽고 그 프로파일 문서대로 클라이언트를 작성한다. 실제 응답 상태와 미디어 타입은 Spring GraphQL이 정하며, 두 문서가 다른 지점에서 클라이언트가 깨진다. **권고** — 둘 중 하나다. (a) 프레임워크 전송을 정본으로 인정하고 `http` 패키지에서 전송 기계를 제거한 뒤 `GraphQlHttpProfile`을 프레임워크 동작의 서술로 좁힌다. (b) `WebGraphQlInterceptor`(`GraphQlPlatformWebInterceptor`가 이미 그 자리에 있다)에서 봉투 검증과 응답 정책을 적용해 프로파일을 실제로 강제한다. 지금은 선언과 실행이 분리돼 있다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.4) 문서/구현 드리프트 — 모듈 경계 선언과 실제 트리
|
||
- slug: `adapter-inbound-web-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L140`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `WebModuleBoundaryTest`가 다섯 개의 긍정 규칙과 **네 개의 부정 픽스처**를 갖는다: 부정 픽스처의 존재 이유가 명시돼 있다: "A boundary test that has never been shown to fail is indistinguishable from one that scans the wrong directory." 그리고 프로덕션 스캔에 `fileCount() > 100` 하한과 `packages()`에 특정 패키지 두 개가 있어야 한다는 확인이 함께 붙는다. 프레임워크 탐지 정규식에는 Jackson 2와 3이 **둘 다** 들어 있고 그 근거가 적혀 있다: "this repository runs on Spring 7, whose message converters take Jackson 3 — so a CORE module could have imported a mapper without this detector noticing, which is **a hole in exactly the check that is supposed to have none**." 이것은 이 저장소에서 확인한 경계 강제 중 가장 강하다. notification의 `EndpointGuardCallSiteTest`(호출처 목록이 가드 javadoc과 달랐던)와 달리, 여기서는 목록 자체가 스캔으로 생성된다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.3) XML/CBOR 표현의 런타임 배선
|
||
- slug: `adapter-inbound-web-c12`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L986`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: `build.gradle`이 두 백엔드를 `compileOnly`로 두고 그 이유를 길게 적는다(§2) — `implementation`이었을 때 "silently began parsing `application/xml` request bodies... an XXE surface nobody chose"였기 때문이다. 의도된 설계다. 그런데 그 잭슨 백엔드를 배포가 추가하더라도 메시지 컨버터를 등록하는 코드가 없다. `WebXmlMapperFactory`·`WebCborMapperFactory`·`RepresentationNegotiationPolicy`를 참조하는 파일은 자기 패키지와 테스트뿐이고, 두 자동설정(MVC 12빈 · WebFlux 11빈)에도 없다. `WebRepresentation.available()`이 "absent backend를 문장으로 바꾼다"는 장치는 그 문장을 낼 호출자가 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.2) 조건 형제 비교 — `X-Request-Id`에 대해 배선된 두 필터가 반대 정책을 쓴다
|
||
- slug: `adapter-inbound-web-c13`
|
||
- readiness: `READY`
|
||
- source: `analysis/14-adapter-inbound-web.md#L1071`
|
||
- owning-module: `adapter-inbound-web`
|
||
- classification: 두 필터 모두 서블릿 배포에서 등록되고 둘 다 `X-Request-Id` 응답 헤더를 쓴다. 순서상 `WebMvcRequestIdFilter`(`HIGHEST_PRECEDENCE + 10`)가 먼저 돌아 새 UUID를 헤더에 쓰고, `RequestLoggingFilter`(`LOWEST_PRECEDENCE`)가 나중에 돌아 **클라이언트가 보낸 값으로 덮어쓴다**. MDC의 `request_id`와 접근 로그도 클라이언트 값이다. §32.1.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 이 모듈의 형태 — 하나의 leaf, 세 개의 설정 네임스페이스
|
||
- slug: `adapter-inbound-websocket-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/17-adapter-inbound-websocket.md#L24`
|
||
- owning-module: `adapter-inbound-websocket`
|
||
- classification: 여섯 소스셋(inbound-web과 같은 형태)이고 `META-INF` 자동설정 리소스가 **없다**. 조립은 전적으로 컴포넌트 스캔에 달려 있으며, 컴포지션 루트는 이 leaf를 스캔에서 제외하지 **않는다**(graphql과 반대). 그런데 스캔이 잡을 수 있는 Spring 애노테이션을 가진 파일이 **169개 중 7개**다: **세 개의 설정 접두사가 있고 그중 하나에는 소비자가 없다:** 세 번째가 이 모듈의 핵심 사실이다. `backend.websocket` 네임스페이스가 규정하는 "플랫폼"이 main 169 파일 중 약 90개를 차지하고, 그것을 조립하는 `@Configuration`이 하나도 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 조립의 순서가 클래스 하나에 고정돼 있다
|
||
- slug: `adapter-outbound-cache-redis-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L85`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `RedisSdkAutoConfiguration`의 javadoc이 규칙을 적는다 — "`app.redis.enabled` is the whole switch. While it is false this class contributes nothing, and because `RedisSdkSettings` is registered here rather than by the application-wide `@ConfigurationPropertiesScan`, **'contributes nothing' is literal**: the properties are not bound, the cross-field rules are not run, no credential is resolved, and no policy resource, TLS material, client, connection or thread is created." 그 문장이 구조로 뒷받침된다. `RedisSdkSettings`는 `@ConfigurationPropertiesScan` 대상이 아니라 이 클래스의 `@Bean` + `@ConfigurationProperties`로만 존재한다. 그래서 Redis를 쓰지 않는 배포는 Redis 설정을 들고 다니지 않고, **켠 적 없는 잘못된 Redis 설정 때문에 벌을 받지도 않는다**. test가 그 넷을 이름으로 고정한다 — `absentSwitchRegistersNothing`, `disabledRegistersNothing`, `disabledIgnoresMalformedRedisConfiguration`, `disabledNeverAsksForASecretOrAConnection`. 순서도 bind → validate → build로 고정된다. 검증이 `@PostConstruct`나 리스너가 아니라 **bean factory 메서드 안**에 있어서, 설정 오류가 "그 bean을 만들지 못했다"는 실패로 보고되고 그 아래 어떤 것도 검증되지 않은 settings를 잡을 수 없다. 그리고 `redisSdkSettingsValidation`이 별도 bean인 이유도 적혀 있다 — Spring은 factory 메서드가 **반환한…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — raw allowlist 기본값은 없는 리소스를 가리키고, 그것이 의도다
|
||
- slug: `adapter-outbound-cache-redis-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/10-adapter-outbound-cache-redis.md#L107`
|
||
- owning-module: `adapter-outbound-cache-redis`
|
||
- classification: `RedisSdkSettings.Raw.policyResource` 기본값은 `classpath:redis-sdk/raw-command-allowlist.yml`인데, 저장소에 그 파일은 **없다**(`159-...` §8.3b, `git ls-files` 매치 0. 이 leaf의 main resource는 `AutoConfiguration.imports`와 `redis-sdk/redis-command-policy.yml` 둘뿐). 이것은 결함이 아니라 이미 잡혀 있는 함정이다. `requireRawPolicyResource`가 그 사실과 과거 증상을 함께 적는다 — "`validate()` only checks that the setting is non-blank, and the default points at … a resource this module does not ship. So enabling the raw gateway passed configuration validation and then **failed at the first raw command, from inside a request, against a live connection.** The allowlist is the entire authorisation model for that gateway; not being able to read it is a startup failure." test `enabledRejectsAMissingRawAllowlistResource`와 `enabledAcceptsAReadableRawAllowlistResource`가 양쪽을 고정한다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Confirmed — 적재 경로는 auto-configuration이 아니라 명시적 component scan이다
|
||
- slug: `adapter-outbound-fileserver-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/08-adapter-outbound-fileserver.md#L127`
|
||
- owning-module: `adapter-outbound-fileserver`
|
||
- classification: 이 leaf에는 `META-INF/spring/…AutoConfiguration.imports`가 **없다**(`142-...` §8.1). `FileExportConfig`/`FileserverR2Config`를 leaf 밖에서 이름으로 참조하는 production 코드도 없고, 유일한 외부 참조는 app-bootstrap의 test(`OptionalAdapterBeanGatingTest`)다. 실제 적재는 `CaSkeletonApplication`의 명시적 `@ComponentScan`이 `dev.caskeleton.adapter.outbound.fileserver`를 목록에 올려서 이루어진다(`:75`). 즉 CLAUDE.md의 "never activates unexpectedly when merely present on the classpath"는 **classpath 존재만으로 bean이 생기지 않는다**는 뜻으로는 정확하지만, 기전은 import filter가 아니라 "@Configuration은 스캔되고 bean 생성만 `@ConditionalOnProperty`로 막힌다"이다. fail-closed는 성립한다 — 기록해 두는 이유는 mongo leaf의 4중 opt-in(§sub-scope 01, 06번 문서)과 기전이 다르기 때문이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — "이름 없는 상태"를 없애는 것이 이 sub-scope의 주제다
|
||
- slug: `adapter-outbound-notification-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L84`
|
||
- owning-module: `adapter-outbound-notification`
|
||
- classification: 세 클래스가 각각 이전에는 **구분되지 않던 두 상황**을 구분한다. **`NotificationPlatformMode`** — 공급자가 하나도 조립되지 않은 플랫폼이 공급자가 있는 플랫폼과 똑같이 보였다. `INGEST_ONLY`는 **명시적으로 선택해야** 하고("A deployment that reaches zero providers by accident is a misconfiguration, and the whole point of this enum is that the two are told apart"), `NotificationProviderAssembly:183`이 그것을 강제한다 — 경로가 비었는데 모드가 `INGEST_ONLY`가 아니면 조립을 거부하고 메시지로 그 모드를 안내한다. app-bootstrap 쪽에서도 `NotificationPlatformWorkerConfig`가 "everything that starts a thread, and therefore everything `INGEST_ONLY` must not have"를 그 모드로 가른다. **`ProviderType`** — 설정이 타입을 자유 문자열로 날랐고 "the only thing that read it was a" 비교였다. 지금은 닫힌 enum이라 "the unknown type a binding failure at startup"이고 채널도 타입에서 유도된다. **`NotificationSecretRequirements`** — 이 sub-scope에서 가장 미묘한 판단이다. 이전에는 여덟 개 키를 **항상** 요구했다. 지금은 네 개(`CONTACT_ENCRYPTION`·`CONTACT_LOOKUP_HMAC`·`PAYLOAD_ENCRYPTION`·`PROVIDER_REQUEST_LOOKUP_HMAC`)가 모든 모드에 필요하고 — **수용 경로**에 있으므로 `INGEST_ONLY`에서도 필요하다 — 나머지 넷은 능력을 따라간다. 약해지면 안 되는 방향은 명시된다: "a capability that is switched *on* and whose key is missing still refuses the boot, because the …
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — (8.3) 중복 메커니즘 — 종료 경로
|
||
- slug: `adapter-outbound-notification-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/13-adapter-outbound-notification.md#L306`
|
||
- owning-module: `adapter-outbound-notification`
|
||
- classification: `NotificationSchedulerWorker.close`와 `NotificationBackgroundWorkers.close` 둘 다 "취소 → shutdown → awaitTermination(grace) → shutdownNow"를 수행한다. 형태는 같지만 대상이 다르다(폴링 스레드 + virtual-thread executor vs. 단일 데몬 scheduler). 중복 아님. 한 가지 기록: 스케줄러의 `close()`는 폴링 스레드를 `interrupt()`하지만(`:173`), `runOnce`의 `globalConcurrency.acquireUninterruptibly()`(`:90`)는 인터럽트에 반응하지 않는다. 주석(`:169`)은 "인터럽트가 poll-interval sleep을 깬다"고만 말하고 그 점은 정확하다. 세마포어는 in-flight 작업이 `finally`에서 반납하므로 결국 풀리고, 최악의 경우 `join(shutdownGrace)`가 만료된 뒤 종료가 계속된다. 결함 아님.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — always-install scan과 opt-in scan의 경계는 실제로 지켜지고 있다
|
||
- slug: `adapter-outbound-persistence-jpa-c56`
|
||
- readiness: `READY`
|
||
- source: `analysis/05-adapter-outbound-persistence-jpa.md#L4143`
|
||
- owning-module: `adapter-outbound-persistence-jpa`
|
||
- classification: `PersistenceJpaConfig`는 persistence root를 통째로 스캔하지 않고 20개 package를 열거한다. 빠진 것은 `config`, `h2`(JPA stereotype 없음)와 opt-in 두 개(`notification`, `fileserver`)다. 두 opt-in은 각자의 `@ConditionalOnProperty` configuration이 자기 package만 스캔한다. 이 배치의 이유는 javadoc과 `PersistenceEntityScanCoverageTest`에 기록돼 있다 — 과거에 root를 스캔해서 capability를 끈 배포가 `ddl-auto=validate`에서 `notification_request` / `fs_cleanup_item`을 요구하며 부팅에 실패했다. 측정 결과 always-install scan의 건전성은 유지되고 있다. `@Entity` 25개 중 opt-in package(`notification` 13, `fileserver` 6) 밖의 4개는 `idempotency_record`, `outbox_event`, `live_event_log`, `durable_operation`이고, 이 네 테이블은 모두 default location `db/migration/postgresql`(V1/V3/V11/V12)이 만든다. `postgresql` package는 scan 대상이지만 그 안의 candidate adapter들(`inbox`, `outbox` v2, `idempotency` v2)은 `@Entity`가 아니라 native SQL 기반이라 persistence unit에 들어오지 않는다. 즉 sub-scope 08이 발견한 "opt-in stream을 always-install scan이 끌고 들어온다" 유형의 결함은 현재 남아 있지 않다. 다만 `PersistenceEntityScanCoverageTest`가 지키는 범위에는 비대칭이 하나 있다. opt-in configuration 두 개에 대해서는 `@EntityScan` 목록과 `@EnableJpaRepositories` 목록이 **정확히 같은지** `containsExactly`로 검사한다("ent…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — opt-in은 네 겹이고, 각 겹이 서로 다른 실패를 막는다
|
||
- slug: `adapter-outbound-persistence-mongo-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L98`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: 네 겹 모두 `ca-skeleton.persistence-mongo.enabled=true`라는 같은 조건을 읽는다(`evidence/raw/123-...` §8.2). 이것은 중복이 아니라 계층별 차단이다: filter는 Boot의 후보군, 나머지 셋은 자기 bean 그래프를 담당한다. `MongoPersistenceConfigTest`가 실제 `@EnableAutoConfiguration` context로 default/false에서 `MongoClient`·`MongoTemplate` 부재를, `enabled=true` + mock client에서 `MongoTemplate` 단일 bean을 확인한다. `MongoPlatformAutoConfiguration`(443줄)은 이 leaf에서 가장 밀도가 높은 파일이고, 거의 모든 `@Bean`의 javadoc이 **과거에 "shipped했지만 아무 configuration도 만들지 않던" 경로**를 기록한다 — atomic/bulk template, reactive 실행 경로 일체, change-stream source와 consumer, startup validator, client generation registry, health indicator. 이 leaf는 그 미연결들을 한 번 훑어 고친 이력을 갖고 있고, 그 사실이 이 sub-scope의 판단 기준을 바꾼다: 남아 있는 미연결은 "아직 안 한 것"이 아니라 "훑고도 남은 것"이다. startup 검증 쪽 설계도 눈여겨볼 만하다. `mongoPlatformStartupCheck`는 `MongoTopologyProbe` bean이 있을 때만 돌지만, 그 조건이 곧 탈출구가 되는 것을 막기 위해 `mongoTopologyProbeRequirement`가 **probe 조건 없이** 등록되어 "platform profile이 있는데 probe가 없으면" 실패시킨다. javadoc이 그 이유를 한 줄로 적는다 — "a requirement that only applies when the thing it requires is present is not a requirement".
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — `failure`는 이 leaf에서 가장 잘 배선되고 가장 잘 논증된 부분이다
|
||
- slug: `adapter-outbound-persistence-mongo-c08`
|
||
- readiness: `READY`
|
||
- source: `analysis/06-adapter-outbound-persistence-mongo.md#L1142`
|
||
- owning-module: `adapter-outbound-persistence-mongo`
|
||
- classification: `MongoFailureClassifier`와 `MongoFailureTranslator`는 auto-configuration의 실제 bean이고(`MongoPlatformAutoConfiguration:85·92`), imperative·reactive 두 executor가 모두 그것을 통해 번역한다. 규칙 사슬도 순서까지 논증돼 있다 — **label → phase → 적용 가능성 → code table → fail closed**. 고쳐진 결함 이력이 촘촘하다. **번역기가 phase를 버렸다.** `DefaultMongoFailureTranslator`가 operationType을 들고도 context-free overload를 불러서, "FIND의 응답 유실 = 재현 가능한 읽기 / UPDATE의 같은 유실 = 결과 불명 쓰기"라는 구분이 **transaction이 아닌 모든 경로에서** 버려졌다 — 즉 모든 평범한 연산에서. 실패한 읽기가 ambiguous write로 보고됐다. **server-selection이 terminal이었다.** label도 code도 없는 실패가 `UNCLASSIFIED`로 떨어져 재시도 불가로 처리됐다 — 재시도가 명백히 안전한 유일한 경우인데. **Spring 래핑이 분류를 통째로 건너뛰었다.** `MongoFailureExtractor`가 그 수리다. cause 사슬을 깊이 16까지, `IdentityHashMap`으로 순환 안전하게 탐색한다("a cycle is about the same object appearing twice"). **message는 절대 읽지 않는다.** `MongoDriverFailureView`가 driver 예외를 label·code·boolean 둘로 좁히는 지점이고, 그 이후 어느 계층도 나머지에 닿을 수 없다 — "no later layer can reach the rest, because no later layer is ever handed it". `MongoFailureClassification`의 생성자가 `COMMIT_ONLY`를 `TRANSACTION_COMMIT_UNKNOWN`에만 허용하는 것도 §15의 불변식과 맞물린다. `security`도…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `adapter-outbound-support-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/04-adapter-outbound-support.md#L52`
|
||
- owning-module: `adapter-outbound-support`
|
||
- classification: `adapter-outbound-support`는 application port를 구현하는 하나의 기술 adapter라기보다 **여러 outbound adapter가 공유할 수 있는 기술적 보조 seam**이다. 현재 production surface는 사실상 세 가지다. 1. `OutboundCorrelation` SLF4J MDC에서 `correlation_id`를 조회한다. 값이 없거나 blank면 `"unknown"`을 반환한다. 2. `FailOpenDependencyLogger` optional/fail-open outbound 호출의 success/failure observation을 공통 포맷으로 기록한다. success는 DEBUG, failure는 WARN이다. 3. `OutboundSupportConfig` `FailOpenDependencyLogger` default bean을 제공한다. `@ConditionalOnMissingBean`으로 fork/application이 같은 타입을 override할 수 있게 한다. `package-info.java`는 outbound adapter package의 개괄만 가진다. `src/config/architecture/modules.json`은 support leaf가 다음 project dependency를 **허용**한다. `domain-core` `application-core` `shared-contract` 그러나 현재 `build.gradle`과 fresh `compileClasspath` 결과를 보면 실제 project dependency는 **0개**다. 실제 compile dependency는 다음 외부 라이브러리뿐이다. `spring-boot-autoconfigure` 4.0.8 `slf4j-api` 2.0.18 즉 registry의 `allowed_dependencies`는 가능한 최대 경계를 나타내고, 현재 source graph가 그 edge를 모두 사용한다는 뜻이 아니다. support는 현 snapshot에서 domain/application/shared 타입과도 결합하지 않는다. 이 구분은 Clean Architecture 설명에서도 중요하다.…
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 게이트가 세 조건을 순서대로 본다
|
||
- slug: `grpc-advanced-bootstrap-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-bootstrap.md#L76`
|
||
- owning-module: `grpc-advanced-bootstrap`
|
||
- classification: `requireStableStarterIsClean` 이 같은 불변식을 런타임에서도 확인한다 — 팻 자, 셰이드 산출물, 테스트 하네스처럼 다른 방식으로 조립된 런타임을 위해서다. **다만 그 메서드를 부르는 런타임이 없다**(§12.1). 지금 그 검사를 실행하는 것은 이 리프의 자기 테스트뿐이다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `grpc-observability-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-observability.md#L59`
|
||
- owning-module: `grpc-observability`
|
||
- classification: `grpc-core-api` 에서 쓰는 타입은 넷이다 — `GrpcMethodName`, `GrpcStatusCode`, `RpcType`, `GrpcCompletionOutcome`. 네 타입 모두 `GrpcRpcObservation` 의 record 성분이다. `GrpcStreamObservation` 은 `GrpcMethodName` 하나만 쓴다. 배선 없음(`EVD-325`). `runtime_memberships` 가 비어 있고, 저장소 어디에서도 `new GrpcObservationConvention(...)` 을 만드는 production 코드가 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-admin-runtime-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-admin-runtime.md#L78`
|
||
- owning-module: `messaging-admin-runtime`
|
||
- classification: 실측 import (`EVD-308`): 6개 중 3개가 미사용이다. 지금까지 본 리프 중 가장 많다. 배선은 starter 한 곳뿐이고, 이 리프에서 빈이 되는 것은 **둘**이다(`EVD-307`). `MessagingAdminService`, `ReplayService`, `RedriveService`, `DestructiveMessagingAdmin` — 넷 다 빈이 없다. starter 는 그중 하나에 대해서만 이유를 밝힌다. 나머지 셋의 부재에 대한 설명은 어디에도 없다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-kafka-share-experimental-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L54`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- classification: Kafka **Share Group**(KIP-932, 경쟁 소비자 work queue)을 실험적 어댑터로 감싼다. `runtime_memberships: []`이고 이름 자체가 `-experimental`이다. 이 leaf의 실질은 **거절**이다. 190줄 중 실제 동작을 하는 코드는 거의 없고, 세 가지를 거절한다. 핵심 진술이 validator javadoc에 있다. 두 번째 문단이 이 저장소의 experimental 정책을 한 문장으로 담는다 — **기본 꺼짐이 drift 방지 수단이다.**
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-kafka-share-experimental-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-kafka-share-experimental.md#L84`
|
||
- owning-module: `messaging-kafka-share-experimental`
|
||
- classification: 들어오는 것(project): `messaging-core-api`, `messaging-policy`, `messaging-transport-spi`, `messaging-kafka` — 넷 다 `api`. 들어오는 것(vendor): `org.apache.kafka:kafka-clients`(`implementation`) — **어떤 소스도 import하지 않는다**(§12.4). 나가는 것: **없다.** 어떤 leaf의 `allowed_dependencies`에도 이 leaf가 없고 starter 목록에도 없다. 런타임 배선: 없음. `runtime_memberships: []`. bean 없음(Spring 주석 0개). **소비자 없음·membership 없음·조립 없음의 삼중 정합**이다 — `messaging-schema-avro`·`messaging-schema-protobuf`와 같은 형태이고, incubating leaf의 올바른 상태다. **`messaging-policy`와 `messaging-kafka` 의존이 실제로 쓰이는가.** 네 project 의존 중 둘, 벤더 의존 하나가 미사용이다. §17.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 프로파일이 스스로 거부하는 것
|
||
- slug: `messaging-nats-experimental-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md#L94`
|
||
- owning-module: `messaging-nats-experimental`
|
||
- classification: `NatsJetStreamProfile` 은 record 이고, 압축 생성자가 이 어댑터의 불변식을 전부 들고 있다. 검증기가 호출되지 않는 지금, **실제로 실행되는 유일한 게이트가 여기다.** `ackMode` 거부 사유는 `NatsAckMode` 자신이 문장으로 들고 있고(`rejectionReason()`), 프로파일이 그 문장을 예외 메시지에 그대로 싣는다. `NONE` 은 "forgotten", `ALL` 은 "still in flight" — 테스트가 그 두 낱말로 각각 걸어 잠근다. `PARKING_HEADROOM = 1` 상수와 `parkAtDelivery() = maxDeliver - PARKING_HEADROOM` 가 §2 의 시점 선택을 숫자로 못 박는다. `NatsMaxDeliverParkingWorkflow.parkingThreshold()` 는 이 값을 그대로 위임한다 — 임계값의 정의가 한 곳에만 있다. **주의할 비대칭.** 편의 팩토리 `durable(subject, stream, durableName)` 는 중복 제거 창을 `Optional.of(2분)` 으로 채운다. 즉 팩토리를 거친 프로파일은 §17.1 의 구멍에 빠지지 않는다. 그러나 팩토리에도 호출자가 없고(저장소 전역 grep 0건), 정규 생성자는 빈 창을 정상값으로 받는다. 기본 경로가 안전하다는 사실이 그 구멍을 닫아 주지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — Git/설계 문서에서 확인한 변화와 실패 기록
|
||
- slug: `messaging-runtime-core-c07`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-runtime-core.md#L631`
|
||
- owning-module: `messaging-runtime-core`
|
||
- classification: 이 leaf는 **통째로 하나의 수정**이다. MSG-INT-003이라는 식별자가 세 파일의 javadoc에 나온다(`DeclaredDestinationAccess`, `TransportMessagingRuntime`, `MessagingCoreAutoConfiguration:461`). `build.gradle` 주석의 마지막 문장이 이 leaf 전체의 교훈이다 — "A starter that filled the gap with an application-supplied fake would pass a context test while running none of them."
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 의존성과 런타임 배선
|
||
- slug: `messaging-security-c02`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-security.md#L91`
|
||
- owning-module: `messaging-security`
|
||
- classification: 들어오는 것: `messaging-core-api`(api) 하나. 나가는 것: `messaging-runtime-core`, `messaging-kafka`, `messaging-rabbit`, `messaging-admin-runtime`, `messaging-pulsar-experimental`, `messaging-nats-experimental`, `messaging-spring-boot-starter`. **이 leaf는 messaging family에서 배선이 가장 잘 된 축에 속한다.** 어댑터 두 곳이 직접 소비한다. 이 leaf 자체는 Spring 주석을 갖지 않는다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 선택은 닫힌 레지스트리이고, 등록과 조립은 다르다
|
||
- slug: `messaging-spring-boot-starter-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L94`
|
||
- owning-module: `messaging-spring-boot-starter`
|
||
- classification: `MessagingProviderSelection` 에 지도가 셋이다. 셋째 지도가 이 클래스의 판단이다. 등록되어 있다는 것과 조립할 수 있다는 것을 분리했고, 그 이유를 적었다 — Rabbit 을 고르면 핵심 설정 깊은 곳에서 `MessagingTransport` 빈이 없다는 오류가 나는데, 그것은 운영자에게 빈이 없다고만 말하지 고른 전송이 완성되지 않았다고는 말하지 않는다. 결과로 오늘 조립 가능한 전송은 `kafka` 하나다. `RabbitMessagingAutoConfiguration` 98줄은 선택 단계에서 거부되므로 **어떤 경로로도 도달하지 않는다**(§12.3).
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
### CONCEPT — 모듈의 정체와 경계
|
||
- slug: `messaging-spring-cloud-stream-bridge-c01`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-cloud-stream-bridge.md#L56`
|
||
- owning-module: `messaging-spring-cloud-stream-bridge`
|
||
- classification: Spring Cloud Stream 바인딩을 이미 쓰는 서비스가 같은 논리 목적지에 닿게 하는 **상호운용 seam**이다. **"look enough like the platform's to be mistaken for them"**이 이 leaf 전체의 위협 모델이다. 브리지는 기능을 추가하지 않고 **차이를 드러낸다.** 세 층으로 그것을 한다. 세 번째가 특이하다 — 거절할 수 없는 차이를 문서화 가능한 값으로 만든다.
|
||
- missing-verification: SSOT 본문의 메커니즘 서술을 옮긴 것이다. 실행 확인 범위는 그 SSOT 의 커버리지 원장과 §확인하지 못한 것이 소유한다.
|
||
|
||
---
|
||
# Decomposition checklist
|
||
|
||
### CASE — 배정 식별자 때문에 insert-first 청구가 UPSERT 가 되어 커밋된 결과를 덮었다
|
||
- slug: `assigned-id-turns-claim-into-upsert`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-operation-ledger-jpa.md#L133`
|
||
- code: `.../grpc-operation-ledger-jpa/.../ledger/JpaGrpcOperationLedger.java` · `.../GrpcOperationLedgerEntity.java` · `.../GrpcOperationIdentity.java:36`
|
||
- evidence: 없음 — 코드 통독과 Spring Data `save` 계약 대조로 판정했다. build-only 리프라 실행 경로가 없다
|
||
- classification: `claim` 은 insert-first, read-on-conflict 를 주장한다. 그러나 엔티티의 `@Id` 가 배정값(`caller|method|keyHash`)이라 `SimpleJpaRepository.save` 가 `persist` 가 아니라 `merge` 로 간다. 그 파생 키가 유니크 제약의 세 컬럼과 같은 행을 가리키므로 두 번째 청구는 위반을 일으키지 않고 기존 행을 갱신한다 — 상태가 `IN_PROGRESS` 로, `outcome_reference` 가 널로 되돌아가고 `claim` 은 빈 값을 돌려줘 호출자가 소유를 얻었다고 읽는다. 인메모리 테스트 이중의 `save` 는 키가 있으면 던지므로 INSERT 를 흉내 내고 그 차이를 가린다.
|
||
- missing-verification: 실제 데이터베이스로 두 번 청구해 재현하지 않았다.
|
||
- relations: `reference:atomic-type-is-not-atomicity` · `reference:check-which-duplicate-is-wired` · `case:two-owners-popped-the-evidence-frame`
|
||
|
||
### CASE — 배수 완료가 자기가 읽은 값으로 상태를 다시 써서 진행 중인 자격증명 회전을 되돌린다
|
||
- slug: `complete-drain-rolls-back-a-rotation`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-policy.md#L158`
|
||
- code: `.../grpc-policy/.../security/GrpcCredentialRotationManager.java` · `.../grpc-client/.../GrpcChannelRuntimeRegistry.java`
|
||
- evidence: 없음 — 원자성 분석으로 판정했다. 두 리프 모두 배선 경로가 없다
|
||
- classification: `AtomicReference` 를 들고 있으면서 `rotate` 와 `completeDrain` 이 모두 `get()` 후 조건 없는 `set()` 을 한다. 회전 경합에서는 덮인 세대가 배수 목록에 오르지 못하고, 배수 완료에서는 자기가 읽은 `observed.current()` 로 새 상태를 만들기 때문에 그 사이에 일어난 회전이 지워지고 이전 세대가 다시 현재가 된다. 자격 자재를 떨어뜨리지 않고 교체하려고 만든 클래스가 교체 자체를 되돌린다. 같은 파일의 형제(`install`)와 같은 가족의 `GrpcRetryBudget` 이 비교 후 교체를 정확히 쓴다.
|
||
- missing-verification: 동시 회전과 동시 배수 완료를 실행으로 재현하지 않았다.
|
||
- relations: `reference:atomic-type-is-not-atomicity` · `case:assigned-id-turns-claim-into-upsert` · `decision:retry-safety-is-decided-by-evidence`
|
||
|
||
### CASE — 시작 검증기가 유일한 소비자인 설정 키 넷이 아무것도 게이트하지 않는다
|
||
- slug: `startup-validator-is-the-only-reader-of-four-keys`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-spring-boot-starter.md#L139`
|
||
- code: `.../grpc-spring-boot-starter/.../boot/GrpcPlatformStartupValidator.java` · `.../boot/GrpcPlatformAutoConfiguration.java` · `.../boot/GrpcPlatformProperties.java`
|
||
- evidence: 없음 — 호출자 전수 검색으로 판정했다
|
||
- classification: 검증기를 이름으로 부르는 파일은 자기 자신과 자기 테스트뿐이다. 자동 설정은 빈 아홉 개를 만들고 `requireValid` 를 부르지 않으며 초기화 콜백도 없다. 그 검증기가 유일한 소비자인 설정 키가 넷이다 — `transport`·`tls-enabled`·`trust-all-certificates`·`operation-ledger-enabled`. 따라서 운영 환경의 TLS 바닥도, 비운영 전송 거부도, 멱등 키 필수 메서드의 원장 요구도 강제되지 않는다. 같은 저장소가 정본을 둘 갖고 있다 — messaging 의 `StartupProfileValidation` 과 fileserver 의 증명 호출.
|
||
- missing-verification: 스타터를 올려 컨텍스트를 세우지 않았다. build-only 리프다.
|
||
- relations: `case:a14-f018-webplatformstartupvalidator` · `reference:a-bean-is-not-composition-evidence` · `case:validator-declared-and-never-injected`
|
||
|
||
### CASE — 같은 자동 설정 안에서 검증기 하나만 감싸이지 않아 트랜잭션 조건이 검사되지 않는다
|
||
- slug: `validator-declared-and-never-injected`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L148`
|
||
- code: `.../messaging-spring-boot-starter/.../autoconfigure/KafkaMessagingAutoConfiguration.java:41,54,74` · `.../autoconfigure/StartupProfileValidation.java` · `.../messaging-kafka/.../KafkaTransactionProfileValidator.java`
|
||
- evidence: 없음 — 자동 설정의 빈 선언 대조로 판정했다
|
||
- classification: `KafkaProfileValidator` 는 `StartupProfileValidation` 으로 감싸여 `afterPropertiesSet` 에서 돌고, 같은 파일의 `KafkaTransactionProfileValidator` 는 빈으로 발행만 된다. Rabbit 쪽은 하나뿐인 검증기를 감싼다. 그래서 트랜잭션 식별자 접두·멱등 생산자·`acks=all`·수동 커밋 요구가 시작 시 검사되지 않고, 어댑터는 `brokerTransaction=true` 를 무조건 답한다. 검증기의 절반은 `messaging-kafka` 가, 배선의 절반은 스타터가 소유하므로 어느 문서도 혼자서는 이 사실을 말할 수 없다.
|
||
- missing-verification: 조건을 어긴 프로파일로 컨텍스트를 세워 재현하지 않았다.
|
||
- relations: `case:startup-validator-is-the-only-reader-of-four-keys` · `reference:a-bean-is-not-composition-evidence` · `case:capability-constant-outlives-its-condition`
|
||
|
||
### CASE — 능력 상수가 프로파일 조건보다 오래 살아서 중복 제거 없는 목적지가 가드를 통과한다
|
||
- slug: `capability-constant-outlives-its-condition`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-nats-experimental.md#L111`
|
||
- code: `.../messaging-nats-experimental/.../NatsJetStreamTransport.java` · `.../NatsJetStreamProfileValidator.java` · `.../messaging-core-api/.../destination/MessagingCapabilities.java`
|
||
- evidence: 없음 — 능력 상수와 식별자 생성 경로 대조로 판정했다
|
||
- classification: 어댑터가 `deduplicatedPublish=true` 를 상수로 답한다. 그런데 중복 제거 식별자는 프로파일에 창이 있을 때만 만들어지고, 창은 선택 사항이며 생성자도 검증기도 요구하지 않는다. 창이 없으면 `Nats-Msg-Id` 가 실리지 않아 서버가 중복을 제거하지 않는다. 이 플래그는 능력 열둘 중 부재가 예외를 만드는 유일한 것이라, 창 없는 목적지가 그 가드를 통과한 뒤 모호 재발행에서 스트림에 같은 메시지를 두 번 넣는다. 어댑터 자신의 javadoc 이 그 조건을 알고 있다 — 재시도를 안전하게 만드는 것은 창이며 '프로파일이 그것을 켰을 때' 라고 적는다.
|
||
- missing-verification: 창 없는 프로파일로 모호 재발행을 재현하지 않았다.
|
||
- relations: `case:validator-declared-and-never-injected` · `reference:a-bean-is-not-composition-evidence` · `case:support-matrix-said-the-opposite-of-the-code`
|
||
|
||
### CASE — 마스킹이 IPv4 만 알아서 검사가 나머지 주소 형태를 전부 통과시킨다
|
||
- slug: `ipv4-only-mask-passes-every-other-form`
|
||
- readiness: `READY`
|
||
- source: `analysis/grpc/grpc-advanced-diagnostics.md#L123`
|
||
- code: `.../grpc-advanced-diagnostics/.../diagnostics/GrpcDiagnosticsRedactor.java` · `.../diagnostics/GrpcChannelDiagnosticsSnapshot.java`
|
||
- evidence: 없음 — 정규식과 생성자 검사 대조로 판정했다
|
||
- classification: `maskAddress` 는 IPv4 패턴 하나만 갖고 맞지 않는 입력을 그대로 돌려준다. 그리고 스냅숏 생성자는 마스킹 결과가 입력과 같으면 이미 마스킹된 것으로 판정한다. 두 규칙이 겹치면 IPv6 주소·파드 DNS 이름·유닉스 소켓 경로가 전부 검사를 통과한다. 이 플랫폼이 겨냥하는 배포가 쿠버네티스이고 헤드리스 레코드의 엔드포인트가 DNS 이름이므로 도달 가능한 형태다. 테스트의 주소 리터럴은 전부 IPv4 다. 같은 사이클이 철회한 P1 의 원인도 듀얼스택 호스트명이었다 — 판정이 모르는 형태를 통과 쪽으로 접는 같은 계열이다.
|
||
- missing-verification: IPv6 주소로 스냅숏을 만들어 재현하지 않았다.
|
||
- relations: `reference:make-the-unsafe-state-unrepresentable` · `case:a-red-test-misread-as-a-product-defect` · `reference:agreement-between-documents-proves-nothing`
|
||
|
||
### REFERENCE — 검증기는 발행이 아니라 주입이 강제다
|
||
- slug: `a-validator-is-enforced-by-injection`
|
||
- readiness: `READY`
|
||
- source: `analysis/messaging/messaging-spring-boot-starter.md#L148`
|
||
- code: `.../autoconfigure/StartupProfileValidation.java` · `.../boot/GrpcPlatformStartupValidator.java`
|
||
- evidence: 없음 — 이 저장소의 네 사례에서 뽑은 규칙이다
|
||
- classification: 검증기를 빈으로 만드는 것과 그것이 도는 것은 다른 사실이다. 이 저장소는 그 차이를 한 번 겪고 `StartupProfileValidation` 으로 고쳤으며, 그 javadoc 이 이전 상태를 기록한다 — 컨텍스트가 브로커마다 검증기를 발행하고 아무것도 검증하지 않았다. 같은 형태가 네 곳에 남아 있다.
|
||
- scope: 기동 시점 검증기 전부. 판정은 두 질문이다 — 이 검증기를 부르는 초기화 콜백이나 러너가 있는가, 그리고 그 호출자가 실제로 만들어지는 조건에 있는가.
|
||
- exceptions: 검증기가 값 객체의 생성자 안에서 도는 형태라면 주입이 필요 없다. 그때는 그 값 객체를 만드는 경로가 하나뿐인지가 대신 확인 대상이다.
|
||
- relations: `case:validator-declared-and-never-injected` · `case:startup-validator-is-the-only-reader-of-four-keys` · `case:a14-f018-webplatformstartupvalidator`
|
||
|
||
## Source integrity
|
||
- [x] 문서 해시(`10eabd7a…`)와 리비전(`21234e38…`)을 헤더에 기록
|
||
- [x] 모든 노드에 source 앵커 하나 이상
|
||
- [x] source 앵커가 제목이 함의하는 내용을 실제로 담고 있음
|
||
- [x] 런타임 의존 주장은 evidence를 명명하거나 `missing-verification`에 적었고, 실행이 필요한 것은 대응하는 Open Question을 갖는다
|
||
|
||
## Topic quality
|
||
- [x] 16개 Topic 전부 공학 문제 공간이며 디렉터리 이름이 아니다 — 각각 여러 leaf/모듈을 가로지른다
|
||
- [x] 두 Topic이 같은 인과 사슬을 임의로 쪼개지 않음. 인접한 쌍의 구분 근거:
|
||
- `assembly-ownership`(조립이 안 됨) ↔ `what-a-gate-does-not-prove`(검증이 그것을 못 잡음) — 다른 질문이다
|
||
- `owner-safe-state-machines`(소유권을 SQL에 적기) ↔ `transaction-deadline-and-pool`(시간과 커넥션 예산) — 같은 leaf지만 다른 실패 축이다
|
||
- `bounding-by-type`(값의 도메인) ↔ `drift-direction`(문서의 진술) — 전자는 코드, 후자는 문서다
|
||
- `duplicate-mechanisms`(둘 중 약한 쪽이 조립됨) ↔ `redis-command-admission`(우회 경로의 수) — 후자는 한 leaf의 구체적 admission 구조다
|
||
- [x] 가장 큰 Topic(`assembly-ownership` 18노드)이 전부 같은 판정 기준을 공유
|
||
|
||
## Concept
|
||
- [x] 각 Concept이 사건이 아니라 메커니즘의 설명이다
|
||
- [x] Concept이 Case의 요약이 아니다 — 각 Concept은 여러 Case가 참조하는 구조를 다룬다
|
||
- [x] 본문에 넣을 코드·상태도·표의 출처가 source 앵커에 있다
|
||
- [x] 계약 파일이 이 종류를 아직 기술하지 않는다는 사실을 트리 머리말에 적었다
|
||
|
||
## Case
|
||
- [x] 각 Case에 구체적 사건·실험·실패·진단·검증 시퀀스가 있다
|
||
- [x] 제목이 역사를 지어내지 않아도 이해된다
|
||
- [x] 결론이 실제 증거로 경계 지어졌고, 증거 없는 추론은 `missing-verification`에 적었다
|
||
- [x] 저장소의 javadoc·마이그레이션 헤더가 사후 기록인 경우 그 사실을 evidence 항목에 적었다
|
||
|
||
## Reference
|
||
- [x] 각 규칙이 원 사건 밖에서 재사용 가능 — 프로젝트 이름을 지워도 규칙이 남는지 확인
|
||
- [x] Case 요약을 선언형으로 바꾼 것이 아님 — `scope`와 `exceptions`가 사건 밖의 조건을 말한다
|
||
- [x] `exceptions`를 전부 source에서 진술. 특히 다음 넷은 반대 사례가 실재한다:
|
||
- `a-bean-is-not-composition-evidence` — build-only leaf의 공개 API 표면
|
||
- `a-gate-nobody-runs-reports-the-last-run` — 대체 경로가 있는 messaging 인증 레인
|
||
- `check-which-duplicate-is-wired` — 조립된 쪽이 더 나은 자격 증명 회전
|
||
- `the-startup-validator-follows-the-autoconfiguration-root` — 루트가 있는데도 부르지 않는 둘
|
||
|
||
## Open Question
|
||
- [x] 답이 분석 안에 이미 있지 않음 — 여덟 개 전부 실행 또는 부팅이 필요하다
|
||
- [x] known / unknown / next-verification이 분리됨
|
||
- [x] 후보 선택지를 지어내지 않음
|
||
- [x] `container-lanes-not-executed`가 나머지 네 개(commit-ambiguity · v2-state-machine · redis-topology · pool-contract)의 상위 집합임을 relations로 표시
|
||
|
||
## Decision
|
||
- [x] 프로젝트 선택이 명시적으로 기록됨 — ADR 5건(`ADR-JPA-002`·`ADR-JPA-003`·`ADR-JPA-004`·`ADR-GRPC-001`·`ADR-GRPC-003`·`ADR-GRPC-ADV-001`) + 코드/`build.gradle`/마이그레이션 헤더의 근거 주석
|
||
- [x] "기술이 있다"를 근거로 쓰지 않음
|
||
- [x] 권고에 그치는 것은 `NEEDS_DECISION` — `widen-doc-contract-assertions` 하나
|
||
|
||
## Duplication
|
||
- [x] 두 노드가 같은 일차 목적을 갖지 않음
|
||
- [x] 내용 복사 대신 relations 사용
|
||
- [x] Topic을 가로지르는 참조가 있는 노드는 양쪽에서 relations로 연결했다 — 예: `a-release-gate-with-no-evidence-producer`는 `what-a-gate-does-not-prove`에 속하고 `learning-transfer-between-families`가 참조한다
|
||
|
||
---
|
||
|
||
# Generation summary
|
||
|
||
| kind | 총계 | 비고 |
|
||
|---|---:|---|
|
||
| CONCEPT | 386 | |
|
||
| CASE | 402 | |
|
||
| REFERENCE | 105 | |
|
||
| OPEN QUESTION | 31 | |
|
||
| DECISION | 25 | |
|
||
| **합계** | **949** | candidate ledger 기반 |
|
||
|
||
> **소스 매니페스트 정정(재통독).** `root-tree-source-manifest.json` 이 최상위 분석 문서 22편과
|
||
> `final/document.md` · `source-index.md` · `analysis/module.md` 25개만 등록하고 있었다. 계약은
|
||
> `document-detail/README.md:40` 이 정의한다 — "records SHA-256 hashes for `final/document.md`,
|
||
> `source-index.md`, and **all included analysis files**". 리프 SSOT 43편(messaging 25 · grpc 18)이
|
||
> 빠져 있었으므로, 리프 문서만 바뀌었을 때 그 변경이 매니페스트에 드러나지 않았다. 매니페스트가
|
||
> 존재하는 이유가 정확히 그 경우다. 43편을 등록해 **68개 소스**로 재생성했고, 그 과정에서 기존 25개 중
|
||
> **21개의 해시가 이미 어긋나 있던 것**도 함께 갱신했다(최상위 01~18 · 19 · 99-cross-scope ·
|
||
> `final/document.md`). 이 문서의 frontmatter 세 해시는 재생성 이후 값이다.
|
||
|
||
## Candidate coverage
|
||
|
||
- explicit P1/P2/P3 findings inventoried: `200`
|
||
- explicit unresolved items without P1/P2/P3: `5`
|
||
- explicit analysis candidates total: `205`
|
||
- ledger total candidates (baseline nodes 포함): `965` — 61 SSOT 전수 recall + CONCEPT recall pass
|
||
- `EMITTED`: `915`
|
||
- `MERGED`: `49`
|
||
- `BLOCKED`: `1`
|
||
- `REJECTED`: `0`
|
||
- `UNMAPPED`: `0`
|
||
- 기존 16 Topic + coverage sweep 추가 Topic 9 + 리프 SSOT 재분해 Topic 6(TOPIC 17~22) = `22` 선언 · 본문 Topic 절 `22`
|
||
|
||
`MERGED`는 관련성이 아니라 **동일 사건/동일 검증 단위**일 때만 사용한다. `Open questions / 남은 질문`과 analysis-authored `Tech-Log:` kind hint는 Case보다 우선하며, 채택 evidence가 없는 `DECISION 후보`는 Decision으로 승격하지 않는다.
|
||
|
||
## Coverage sweep 추가 Topic
|
||
|
||
- `contract-domain-and-bounds` — CASE 11, OPEN QUESTION 4
|
||
- `runtime-reachability-and-composition` — CASE 33
|
||
- `verification-path-coverage` — CASE 27, OPEN QUESTION 2
|
||
- `state-ownership-and-concurrency` — CASE 18, OPEN QUESTION 1
|
||
- `schema-and-data-contracts` — CASE 6, OPEN QUESTION 1
|
||
- `transport-and-provider-semantics` — CASE 6
|
||
- `security-policy-enforcement` — CASE 4, OPEN QUESTION 1
|
||
- `declaration-and-document-drift` — CASE 14
|
||
- `runtime-contract-correctness` — CASE 26, REFERENCE 1, OPEN QUESTION 1
|
||
|
||
## 리프 SSOT 재분해 — TOPIC 17~22
|
||
|
||
이 트리의 원본은 `final/document.md` 이고, 그 문서는 최상위 분석 23편을 종합한 것이다. messaging·grpc
|
||
두 가족은 그 종합에서 가족 문서 둘(`19`·`20`)로만 대표되므로, **43편의 리프 SSOT 는 거의 분해되지 않은
|
||
채로 남아 있었다** — 재분해 직전 측정에서 리프 문서를 `source` 로 인용한 노드가 340개 중 **12개**였고
|
||
인용된 리프 문서는 **10/43** 이었다.
|
||
|
||
그 43편이 들고 있는 finding 은 **226건**(P1 5 · P2 66 · P3 155)이다. 이 여섯 Topic 은 그중 기존 16 Topic 이
|
||
담지 않는 문제 공간을 골라 낸 것이다.
|
||
|
||
| Topic | 무엇을 묻는가 | 노드 |
|
||
|---|---|---:|
|
||
| `capability-declaration-vs-proof` | 능력 선언 세 출처가 같은 답을 하는가, 그 답이 프로파일에서 파생되는가 | 7 |
|
||
| `non-atomic-check-then-act` | 원자 타입을 쓰면서 원자성을 얻지 못한 자리 | 5 |
|
||
| `retention-and-unbounded-growth` | 무엇을 언제까지 기억하는가, 포트가 어느 형태를 기본으로 만드는가 | 6 |
|
||
| `drain-and-shutdown-ordering` | 선언된 종료 순서와 실제 종료 경로의 거리 | 5 |
|
||
| `operator-approval-and-destructive-operations` | 승인·검증·실행이 분리되어 있는가 | 6 |
|
||
| `failure-category-across-adapters` | 범주가 재시도·DLQ·대시보드에 무엇을 지시하는가 | 4 |
|
||
|
||
나머지 findings 는 대부분 기존 Reference 가 이미 규칙을 갖고 있어 노드를 늘리지 않았다. 그 판정은
|
||
`candidate-ledger.json` 이 항목별로 들고 있다.
|
||
|
||
# Coverage Summary
|
||
|
||
- candidate-ledger: `candidate-ledger.json`
|
||
- source-manifest: `root-tree-source-manifest.json` — 소스 68개
|
||
- total candidates: `965`
|
||
- emitted: `915`
|
||
- merged: `49`
|
||
- blocked: `1`
|
||
- rejected: `0`
|
||
- unmapped: `0`
|
||
- recall audit: `notes/tech-log-candidate-recall-audit.md` · `notes/tech-log-concept-recall-audit.md`
|
||
|
||
# 터미널 증거 자산
|
||
|
||
**정본은 `raw`이고 SVG는 표현이다**(도구 README의 Evidence rule). 각 `meta/*.json`이
|
||
command · cwd · executedAt · exitCode · sourceRevision을 담는다. 8종 전부 exit 0이고 출력에
|
||
secret이 없어 redaction은 발생하지 않았다.
|
||
|
||
| 자산 | 연결된 노드 | raw | 한 화면에 보이는 것 |
|
||
|---|---|---|---|
|
||
| `tl-kafka-dedup-drift` | `case:support-matrix-said-the-opposite-of-the-code` | 10줄 | 코드 배열 10번째 `false` · 문서 표 같은 행 `O` · 그 플래그를 강제하는 유일한 지점 |
|
||
| `tl-web-six-unowned-components` | `case:scan-exclusion-without-an-owner` · `concept:three-assembly-paths` | 29줄 | 여섯 컴포넌트 애노테이션과 mainRefs=0 · 두 자동설정이 실제로 도달함 · `@Bean` 13/10 · `@Import` 0 · 13개가 전부 협력자 |
|
||
| `tl-grpc-release-gate-no-producer` | `case:a-release-gate-with-no-evidence-producer` | 15줄 | 생성 지점 4곳이 전부 자기 테스트 · Gradle 태스크 0 · CI 0/28 · 대조군 messaging |
|
||
| `tl-outbox-unsatisfiable-condition` | `case:outbox-chain-behind-an-unsatisfiable-condition` | 26줄 | 조건 사슬 · 두 뿌리 port 구현 mainRefs=0 · 같은 starter의 기본 구현 대조 |
|
||
| `tl-messaging-observation-noop` | `case:observation-downgraded-by-the-composition` | 36줄 | 6인자 생성자 선택 · `NO_OBSERVATION` 위임 · 방출자 넷 0 대 협력자 둘 |
|
||
| `tl-rotation-defect-reproduced` | `case:the-same-rotation-defect-closed-once-and-reproduced` | 19줄 | messaging 계약 테스트 문장 · gRPC의 `get()`/`set()` 지점 · CAS 0건 · 경합을 아는 javadoc |
|
||
| `tl-platform-suite-results` | `case:the-second-platform-carried-the-design-not-the-wiring` · `open-question:container-lanes-not-executed` | 13줄 | 두 가족 스위트 집계(851/579, 0 실패 0 skip) · 증거 레인 3종 · **미실행 레인 목록** |
|
||
| `tl-grpc-evidence-lanes` | 여러 Case 공통 | 63줄 | 증거 레인 3종 Gradle 실행 원문 — Netty가 실제 소켓을 열고 통과 |
|
||
|
||
## 기존 probe에서 재캡처가 필요한 자산
|
||
|
||
아래는 원문이 `evidence/raw/`에 있으나 4줄 헤더(`# evidence NNN — slug` 등)를 포함한 형식이다.
|
||
렌더러는 명령 출력 원문을 기대하므로 record 작성 시 헤더 없는 clean raw로 재캡처한다.
|
||
|
||
| 노드 | 기존 raw |
|
||
|---|---|
|
||
| `case:high-water-mark-swallowed-a-redelivered-change` | `135-mongo-change-stream-probe.txt` · `135a-…java` |
|
||
| `case:pii-through-an-exception-message` | `021-support-logger-pii-probe.txt` · `021a-…java` |
|
||
| `case:scriptable-detection-bypassed-by-a-bom` | `146-fileserver-*-probes.txt`의 EXECUTION PROBE 블록 |
|
||
| `case:the-readme-recipe-does-not-start` | `129-mongo-empty-type-registry-write-probe.txt` · `129a-…java` |
|
||
| `case:a-build-gate-that-is-not-in-the-build` | `163-redis-*-probes.txt` §8.1 |
|
||
| `case:five-adapters-bypass-the-single-admission-point` | `165-redis-*-probes.txt` §8.1 |
|
||
| `case:apply-is-a-setting-approval-is-not` | `155-objectstorage-*-probes.txt` §8.1 |
|
||
| `case:seven-rows-downgraded-by-the-family-itself` | `213-inbound-graphql-release-probes.txt` |
|
||
|
||
## 터미널 증거가 맞지 않는 노드
|
||
|
||
근거가 명령 출력이 아닌 것들이다. record 작성 시 코드 인용·마이그레이션 헤더 인용·diff로 대체한다.
|
||
|
||
- 저장소의 javadoc·마이그레이션 헤더가 사후 기록인 Case — `two-owners-popped-the-evidence-frame`,
|
||
`autoconfiguration-in-name-only`, `a-lease-without-an-owner`, `h2-hid-a-column-type-mismatch`,
|
||
`a-customizer-that-discarded-the-bound-property`, `a-five-second-string-that-broke-every-prod-deploy` 등
|
||
- 코드 경로 비교가 근거인 Case — `a-weaker-private-copy-on-the-wired-path`,
|
||
`a-policy-reversed-by-a-later-filter`, `endpoint-check-only-on-upload`
|
||
- 컨테이너가 필요해 아직 실행하지 못한 것 — `commit-ambiguity-is-not-only-sqlstate-08`(대응 Open Question 있음)
|
||
|
||
`evidence/svg/`는 비워 뒀다. 다이어그램은 관계를 설명할 수 있으나 사건의 발생을 증명하지 못한다는
|
||
규칙(`writing-tech-log-from-analysis` §Evidence)에 따라, 필요한 Case에서 개별 판단한다.
|