From 044c2bfc8eed6e9cb1957106bd8bee1dc9f1f454 Mon Sep 17 00:00:00 2001 From: UHeeJoon Date: Thu, 20 Aug 2026 13:02:12 +0900 Subject: [PATCH 1/5] =?UTF-8?q?feat(build):=20=EC=96=B4=EB=8C=91=ED=84=B0?= =?UTF-8?q?=20=EC=BB=A4=EB=B2=84=EB=A6=AC=EC=A7=80=20=EC=9E=84=EA=B3=84=20?= =?UTF-8?q?80%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 통합 exec 만 보면 27% 다 — JaCoCo 가 Lua 를 못 보고 어댑터 Java 는 단위 시험이 도는 곳이라 엉뚱한 것을 재게 된다. 두 계층 exec 를 합쳐 잰다. Refs: CY-236 --- .github/workflows/_test.yml | 5 +- .github/workflows/main.yml | 11 +++- .github/workflows/pr.yml | 16 ++++- .../2026/08/AIJ-0024-adapter-coverage.md | 66 +++++++++++++++++++ ai/journal/index.md | 1 + build.gradle | 35 ++++++++++ 6 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 ai/journal/2026/08/AIJ-0024-adapter-coverage.md diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 29f0794a..feba3944 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -8,7 +8,7 @@ on: workflow_call: inputs: layer: - description: unit | integration | context | chaos | mutation + description: unit | integration | context | chaos | mutation | coverage type: string required: true timeout-minutes: @@ -48,6 +48,9 @@ jobs: context) ./gradlew --no-daemon contextTest ;; chaos) ./gradlew --no-daemon chaosTest ;; mutation) ./gradlew --no-daemon pitest ;; + # 어댑터는 단위·통합 양쪽에서 덮인다. 계층이 잡으로 갈려 exec 가 + # 한자리에 안 모이므로 여기서 둘을 함께 돌려 합쳐 잰다. + coverage) ./gradlew --no-daemon test integrationTest adapterCoverageVerification ;; *) echo "::error::알 수 없는 계층 '${{ inputs.layer }}'"; exit 1 ;; esac diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 4dab98e4..ffb15c73 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -93,6 +93,11 @@ jobs: uses: ./.github/workflows/_test.yml with: { layer: chaos, timeout-minutes: 45 } + test-coverage: + needs: build + uses: ./.github/workflows/_test.yml + with: { layer: coverage, timeout-minutes: 25 } + # PR 에서는 안 돌린다 — 느리고, 도메인이 안 바뀌면 결과도 안 바뀐다 test-mutation: needs: build @@ -121,7 +126,7 @@ jobs: # 취소를 통과로 세지 않기 위해 별도 잡으로 뺐다. 규칙은 verdict 액션에 있다. verdict: name: 판정 - needs: [context, conventions, build, test-unit, test-integration, test-context, test-chaos, test-mutation, security, load-idle] + needs: [context, conventions, build, test-unit, test-integration, test-context, test-chaos, test-coverage, test-mutation, security, load-idle] if: ${{ !cancelled() }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -140,7 +145,7 @@ jobs: report: name: 리포트 needs: [context, conventions, build, test-unit, test-integration, test-context, - test-chaos, test-mutation, security, load-idle, verdict] + test-chaos, test-coverage, test-mutation, security, load-idle, verdict] if: ${{ !cancelled() }} uses: ./.github/workflows/_report.yml with: @@ -155,6 +160,7 @@ jobs: 단위=${{ needs.test-unit.result }} 통합=${{ needs.test-integration.result }} 카오스=${{ needs.test-chaos.result }} + 커버리지=${{ needs.test-coverage.result }} 뮤테이션=${{ needs.test-mutation.result }} 보안=${{ needs.security.result }} 부하(R1)=${{ needs.load-idle.result }} @@ -175,6 +181,7 @@ jobs: 통합 테스트${{ needs.test-integration.result }} 컨텍스트 테스트${{ needs.test-context.result }} 카오스 테스트${{ needs.test-chaos.result }} + 어댑터 커버리지${{ needs.test-coverage.result }} 뮤테이션 테스트${{ needs.test-mutation.result }} 보안 스캔${{ needs.security.result }} 부하 (R1 검증)${{ needs.load-idle.result }} diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 270f76f6..218f910c 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -119,6 +119,17 @@ jobs: layer: chaos timeout-minutes: 40 + # 어댑터 커버리지는 단위·통합 exec 를 합쳐야 잴 수 있다. 계층 잡은 + # 각자 자기 exec 만 남기므로 여기서 둘을 함께 돌린다. + test-coverage: + name: 테스트 + needs: [context, build] + if: needs.context.outputs.code == 'true' + uses: ./.github/workflows/_test.yml + with: + layer: coverage + timeout-minutes: 25 + # ── 4단계: 보안 ──────────────────────────────────────────────────────────── # 시크릿 검사는 항상 돈다 — 문서에도 토큰을 커밋할 수 있다. # CVE 스캔만 코드가 바뀌었을 때로 좁힌다. @@ -149,7 +160,7 @@ jobs: # 취소를 통과로 세지 않기 위해 별도 잡으로 뺐다. 규칙은 verdict 액션에 있다. verdict: name: 판정 - needs: [context, conventions, build, test-unit, test-integration, test-context, test-chaos, security, load-smoke] + needs: [context, conventions, build, test-unit, test-integration, test-context, test-chaos, test-coverage, security, load-smoke] if: ${{ !cancelled() }} runs-on: ubuntu-latest timeout-minutes: 5 @@ -167,7 +178,7 @@ jobs: report: name: 리포트 - needs: [context, conventions, build, test-unit, test-integration, test-context, test-chaos, security, load-smoke, verdict] + needs: [context, conventions, build, test-unit, test-integration, test-context, test-chaos, test-coverage, security, load-smoke, verdict] if: ${{ !cancelled() }} uses: ./.github/workflows/_report.yml with: @@ -183,6 +194,7 @@ jobs: 단위=${{ needs.test-unit.result }} 통합=${{ needs.test-integration.result }} 카오스=${{ needs.test-chaos.result }} + 커버리지=${{ needs.test-coverage.result }} 보안=${{ needs.security.result }} 부하=${{ needs.load-smoke.result }} link-url: ${{ github.event.pull_request.html_url }} diff --git a/ai/journal/2026/08/AIJ-0024-adapter-coverage.md b/ai/journal/2026/08/AIJ-0024-adapter-coverage.md new file mode 100644 index 00000000..6afbf2ac --- /dev/null +++ b/ai/journal/2026/08/AIJ-0024-adapter-coverage.md @@ -0,0 +1,66 @@ +# AIJ-0024 · 어댑터 커버리지 — 통합 exec 만으로는 엉뚱한 것을 잰다 + +- **날짜** 2026-08-20 +- **종류** decision +- **확신** medium +- **티켓** CY-236 + +## 무엇을 했나 + +`adapterCoverageVerification`. `adapter.redis` 패키지 브랜치 커버리지가 80% +미만이면 빌드가 실패한다 (G3.9). 보고용 `adapterCoverageReport` 를 같이 뒀다 — +못 넘겼을 때 **어디가 빈지** 봐야 고칠 수 있다. + +## 실측이 계획을 고쳤다 + +태스크 이름은 "통합 테스트 커버리지 임계" 였다. 그대로 통합 exec 만 보게 +만들고 재 봤더니 **27.1%** 였다. 임계를 80 으로 두면 그냥 못 넘긴다. + +| exec | 브랜치 커버리지 | +|---|---| +| 통합만 | 13/48 = **27.1%** | +| 단위 + 통합 | 44/48 = **91.7%** | + +이유는 둘이다. + +1. **JaCoCo 는 Lua 를 못 본다.** 통합 시험이 실제로 검증하는 것은 스크립트인데 + 그건 계측 대상이 아니다. 통합 exec 에 남는 것은 그 주변 Java 뿐이다. +2. **어댑터의 Java 는 대부분 순수 로직이다.** `RedisKeys`·`ShardHash`· + `ClockSkewTracker` 는 키를 만들고 해시를 돌린다. 그건 단위 시험이 도는 곳이다. + +그래서 통합 exec 만으로 재면 **낮은 숫자가 나오는 게 아니라 엉뚱한 것을 +재게 된다.** 임계를 27% 로 낮추는 것은 더 나쁘다 — 통과하는데 아무것도 안 +보는 게이트가 된다. 두 계층 exec 를 합쳐서 잰다. + +## CI 에 붙이는 방식 + +계층은 **잡으로 갈려** 있다 (TS-5 — 깨진 계층이 이름만 보고 드러나야 한다). +그래서 각 잡은 자기 exec 만 남기고 한자리에 안 모인다. + +두 갈래가 있었다. + +- **exec 를 아티팩트로 올려 합치는 잡을 하나 더 둔다** — 정석이지만 잡이 + 하나 늘고 업로드·다운로드 배선이 붙는다 +- **커버리지 잡 하나에서 두 계층을 함께 돌린다** — 단위·통합을 한 번 더 도는 + 비용이 든다 + +후자를 골랐다. 통합이 30초 안쪽이라 중복 비용이 배선 비용보다 싸다. **틀리면 +바꾸기도 쉽다** — 아티팩트 배선은 한 번 깔면 걷어내기 번거롭다. 통합이 분 +단위로 늘면 그때 전자로 옮긴다. + +## 확신이 낮은 부분 + +- **80 의 근거가 약하다.** 지금 실측이 91.7% 라 여유가 있어 잡은 값이다. + 어댑터가 커지면 이 여유가 먼저 줄고, 그때 80 이 맞는지 다시 봐야 한다. +- **Lua 는 여전히 커버리지 밖이다.** 스크립트 분기가 다 돌았는지는 이 숫자가 + 말해 주지 않는다. 그건 통합 시험의 단언이 보증할 몫이다. + +## 검증 + +- 임계를 0.95 로 올리면 실패, 0.80 이면 통과 (게이트가 실제로 문다) +- `clean` 후 `test integrationTest adapterCoverageVerification` 통과 + +## 다음 사람에게 + +**커버리지 숫자가 낮으면 먼저 "무엇을 재고 있는지" 를 의심해라.** 27% 를 +보고 테스트를 더 쓰는 것은 틀린 대응이었다. 재는 대상이 어긋나 있었다. diff --git a/ai/journal/index.md b/ai/journal/index.md index 56c11ebc..08c451d3 100644 --- a/ai/journal/index.md +++ b/ai/journal/index.md @@ -9,6 +9,7 @@ | ID | 날짜 | 종류 | 제목 | 확신 | 승격 | |---|---|---|---|---|---| | [AIJ-0025](2026/08/AIJ-0025-crash-and-promotion.md) | 2026-08-20 | implement | 강제 종료와 승격 — 계획의 전제가 두 군데 틀렸다 | medium | — | +| [AIJ-0024](2026/08/AIJ-0024-adapter-coverage.md) | 2026-08-20 | decision | 어댑터 커버리지 — 통합 exec 만으로는 엉뚱한 것을 잰다 | medium | — | | [AIJ-0023](2026/08/AIJ-0023-cluster-mode.md) | 2026-08-20 | implement | 클러스터 모드 — 무엇을 잡고 무엇을 못 잡나 | medium | — | | [AIJ-0022](2026/08/AIJ-0022-leader-election.md) | 2026-08-20 | implement | 리더는 한 대다 — 소유권을 값에 담는 이유 | high | — | | [AIJ-0021](2026/08/AIJ-0021-sweep.md) | 2026-08-20 | implement | 이탈자 청소 — 앞부분만 본다 | medium | — | diff --git a/build.gradle b/build.gradle index 3261003a..f5311347 100644 --- a/build.gradle +++ b/build.gradle @@ -145,6 +145,41 @@ tasks.named('jacocoTestCoverageVerification') { } } +// 어댑터는 통합 시험이 실제로 레디스를 치며 도는 곳이라 단위 exec 로는 +// 잴 수 없다. 계층이 잡으로 갈려 exec 가 한자리에 안 모이므로, 통합 exec +// 하나로 어댑터만 따로 잰다. +tasks.register('adapterCoverageVerification', JacocoCoverageVerification) { + description = '어댑터 커버리지 임계 — 통합 시험 기준' + group = 'verification' + dependsOn test, integrationTest + executionData test, integrationTest + sourceDirectories.setFrom(sourceSets.main.java.sourceDirectories) + classDirectories.setFrom(sourceSets.main.output) + violationRules { + rule { + element = 'PACKAGE' + includes = ['com.kafkick.waiting.adapter.redis'] + limit { + counter = 'BRANCH' + minimum = 0.80 + } + } + } +} + +tasks.register('adapterCoverageReport', JacocoReport) { + description = '어댑터 커버리지 보고 — 임계를 못 넘겼을 때 어디가 빈지 본다' + group = 'verification' + dependsOn test, integrationTest + executionData test, integrationTest + sourceDirectories.setFrom(sourceSets.main.java.sourceDirectories) + classDirectories.setFrom(sourceSets.main.output) + reports { + xml.required = true + html.required = true + } +} + tasks.named('check') { dependsOn jacocoTestCoverageVerification } From 569bd5560bdc5dbf9ffe736a0cd860b3a3ae6d9b Mon Sep 17 00:00:00 2001 From: UHeeJoon Date: Thu, 20 Aug 2026 13:14:58 +0900 Subject: [PATCH 2/5] =?UTF-8?q?fix(journal):=20AIJ-0024=20=ED=94=84?= =?UTF-8?q?=EB=A1=A0=ED=8A=B8=EB=A7=A4=ED=84=B0=EC=99=80=20=EB=A1=9C?= =?UTF-8?q?=EC=BB=AC=20=EA=B2=80=EC=82=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 형식을 확인하지 않고 앞 글의 본문만 베껴 프론트매터를 통째로 빠뜨렸다. 로컬 리뷰가 CI 와 같은 것을 보게 해 다시 못 새게 막는다. Refs: CY-236 --- .claude/hooks/review-branch.sh | 39 +++++++++++++++++-- .../2026/08/AIJ-0024-adapter-coverage.md | 20 +++++++--- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/.claude/hooks/review-branch.sh b/.claude/hooks/review-branch.sh index aa1bae99..de78c720 100755 --- a/.claude/hooks/review-branch.sh +++ b/.claude/hooks/review-branch.sh @@ -97,14 +97,32 @@ check_rx2() { | sed "s|^| RX-2 $file:|" | sed 's/$/ ← repeatWhen 을 쓴다. 회복 시 몰아서 터진다/' } +# 작업 로그 형식·색인 동기화. CI(_verify-conventions.yml)와 **같은 것**을 본다. +# 여기서 안 보면 프론트매터를 통째로 빠뜨린 글이 푸시된 뒤에야 드러난다. +check_journal() { + local file=$1 out="" k id + for k in id date kind confidence; do + grep -qE "^$k:" "$file" \ + || out+=" JN-1 $file:1 ← 프론트매터에 '$k:' 없음"$'\n' + done + id=$(grep -oE '^id: AIJ-[0-9]+' "$file" | awk '{print $2}') + if [[ -z "$id" ]]; then + out+=" JN-1 $file:1 ← id 를 읽을 수 없다"$'\n' + elif ! grep -q "$id" ai/journal/index.md 2>/dev/null; then + out+=" JN-2 $file:1 ← $id 가 index.md 에 없다 — 색인에서 빠지면 없는 것과 같다"$'\n' + fi + printf '%s' "$out" +} + review_files() { local -a files=("$@") - local java=() lua=() + local java=() lua=() journal=() for f in "${files[@]}"; do [[ -f "$f" ]] || continue case "$f" in - *.java) java+=("$f") ;; - *.lua) lua+=("$f") ;; + *.java) java+=("$f") ;; + *.lua) lua+=("$f") ;; + */ai/journal/*/*/AIJ-*.md|ai/journal/*/*/AIJ-*.md) journal+=("$f") ;; esac done @@ -138,6 +156,15 @@ review_files() { for f in "${lua[@]}"; do run_hook check-lua.sh "$f" || clean=0; done ((clean)) && say " 위반 없음" || findings=$((findings + 1)) fi + + if ((${#journal[@]})); then + head2 "작업 로그 — 프론트매터·색인" + local jout="" + for f in "${journal[@]}"; do jout+=$(check_journal "$f"); done + jout=$(printf '%s' "$jout" | grep -v '^[[:space:]]*$') + if [[ -n "$jout" ]]; then say "$jout"; findings=$((findings + 1)) + else say " 위반 없음"; fi + fi } # ── 자기검증 — 통과만 하는 검사는 검사가 아니다 ────────────────────────────── @@ -171,6 +198,12 @@ self_test() { # record 와 클래스가 한 파일에 섞여도 클래스만 잡는다 probe "record 가 있어도 다른 클래스는 검사한다" "src/main/java/H.java" \ $'public record Marker(int x) {}\n\nclass H {\n public H() {}\n}\n' "JS-12" + # 프론트매터를 통째로 빠뜨린 글이 CI 까지 갔다. 검사가 실제로 무는지 본다. + probe "저널 프론트매터 누락" "ai/journal/2026/08/AIJ-9998-probe.md" \ + $'# 제목\n\n- **날짜** 2026-08-20\n' "JN-1" + probe "저널이 색인에 없음" "ai/journal/2026/08/AIJ-9999-probe.md" \ + $'---\nid: AIJ-9999\ndate: 2026-08-20\nkind: implement\nconfidence: high\n---\n\n# 제목\n' "JN-2" + # **상대경로 회귀.** git 은 선행 슬래시 없는 경로를 준다. 절대경로로 # 안 바꾸면 테스트 전용 검사가 아예 안 돌고(미탐), 동시에 테스트가 # 프로덕션 규칙으로 검사된다(오탐). 둘 다 실제로 났다. diff --git a/ai/journal/2026/08/AIJ-0024-adapter-coverage.md b/ai/journal/2026/08/AIJ-0024-adapter-coverage.md index 6afbf2ac..16bc35a5 100644 --- a/ai/journal/2026/08/AIJ-0024-adapter-coverage.md +++ b/ai/journal/2026/08/AIJ-0024-adapter-coverage.md @@ -1,9 +1,17 @@ -# AIJ-0024 · 어댑터 커버리지 — 통합 exec 만으로는 엉뚱한 것을 잰다 - -- **날짜** 2026-08-20 -- **종류** decision -- **확신** medium -- **티켓** CY-236 +--- +id: AIJ-0024 +date: 2026-08-20 +kind: decision +phase: 3 +plan: [3.9.1] +jira: CY-236 +commits: [64e1f18] +agent: claude-opus-5 +confidence: medium +promoted-to: +--- + +# 어댑터 커버리지 — 통합 exec 만으로는 엉뚱한 것을 잰다 ## 무엇을 했나 From ec7cc2a292752efd8d5c2f9434736ef0caab2743 Mon Sep 17 00:00:00 2001 From: UHeeJoon Date: Thu, 20 Aug 2026 13:54:23 +0900 Subject: [PATCH 3/5] =?UTF-8?q?fix(build):=20=EC=96=B4=EB=8C=91=ED=84=B0?= =?UTF-8?q?=20=EB=B3=B4=EA=B3=A0=EC=84=9C=20=EB=B2=94=EC=9C=84=EC=99=80=20?= =?UTF-8?q?=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main 전체를 넣어 도메인·부트 클래스가 섞여 '어댑터 보고서' 라는 이름이 거짓이었다. 임계를 못 넘겼을 때가 보고서가 가장 필요한 때라 finalizer 로 건다. Refs: CY-236 --- .claude/hooks/review-branch.sh | 52 ++++++++++++++++++++++++++++------ build.gradle | 49 ++++++++++++++++++++------------ 2 files changed, 75 insertions(+), 26 deletions(-) diff --git a/.claude/hooks/review-branch.sh b/.claude/hooks/review-branch.sh index de78c720..c93d9c1e 100755 --- a/.claude/hooks/review-branch.sh +++ b/.claude/hooks/review-branch.sh @@ -16,6 +16,8 @@ ROOT_SCRIPT=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOU cd "$(git rev-parse --show-toplevel)" || exit 1 HOOKS=".claude/hooks" +# 자기검증이 갈아 끼운다 — 저장소의 진짜 색인 상태에 결과가 흔들리면 안 된다. +JOURNAL_INDEX="ai/journal/index.md" findings=0 say() { printf '%s\n' "$*"; } @@ -99,17 +101,30 @@ check_rx2() { # 작업 로그 형식·색인 동기화. CI(_verify-conventions.yml)와 **같은 것**을 본다. # 여기서 안 보면 프론트매터를 통째로 빠뜨린 글이 푸시된 뒤에야 드러난다. +# +# 색인 경로를 인자로 받는다 — 자기검증이 저장소의 진짜 색인을 읽으면 누가 +# 그 ID 를 실제로 등록하는 순간 검사가 아니라 상태가 결과를 바꾼다. check_journal() { - local file=$1 out="" k id + local file=$1 index=${2:-ai/journal/index.md} out="" k id front + # **프론트매터 안만 본다.** 파일 전체를 훑으면 본문에 'date:' 한 줄만 + # 있어도 프론트매터가 있는 것으로 쳐서 검사가 통과한다. + front=$(awk 'NR==1 && $0!="---" {exit} NR==1 {next} $0=="---" {exit} {print}' "$file") + if [[ -z "$front" ]]; then + printf '%s' " JN-1 $file:1 ← 프론트매터가 없다"$'\n' + return + fi for k in id date kind confidence; do - grep -qE "^$k:" "$file" \ + printf '%s\n' "$front" | grep -qE "^$k:" \ || out+=" JN-1 $file:1 ← 프론트매터에 '$k:' 없음"$'\n' done - id=$(grep -oE '^id: AIJ-[0-9]+' "$file" | awk '{print $2}') + # 값 **전체**가 AIJ-<숫자> 여야 한다. 끝 경계가 없으면 'AIJ-123-extra' 가 + # 'AIJ-123' 으로 잘려 엉뚱한 ID 로 색인을 찾는다. + id=$(printf '%s\n' "$front" | sed -nE 's/^id:[[:space:]]*(AIJ-[0-9]+)[[:space:]]*$/\1/p') if [[ -z "$id" ]]; then - out+=" JN-1 $file:1 ← id 를 읽을 수 없다"$'\n' - elif ! grep -q "$id" ai/journal/index.md 2>/dev/null; then - out+=" JN-2 $file:1 ← $id 가 index.md 에 없다 — 색인에서 빠지면 없는 것과 같다"$'\n' + out+=" JN-1 $file:1 ← id 가 'AIJ-<숫자>' 형식이 아니다"$'\n' + # 부분 일치를 막는다. 색인에 AIJ-1234 만 있어도 AIJ-123 이 통과했다. + elif ! grep -qE "(^|[^0-9A-Za-z-])$id([^0-9]|$)" "$index" 2>/dev/null; then + out+=" JN-2 $file:1 ← $id 가 색인에 없다 — 색인에서 빠지면 없는 것과 같다"$'\n' fi printf '%s' "$out" } @@ -159,8 +174,13 @@ review_files() { if ((${#journal[@]})); then head2 "작업 로그 — 프론트매터·색인" - local jout="" - for f in "${journal[@]}"; do jout+=$(check_journal "$f"); done + local jout="" one + for f in "${journal[@]}"; do + # 명령 치환이 후행 줄바꿈을 먹는다. 그냥 이으면 앞 파일의 마지막 + # 지적과 다음 파일의 첫 지적이 한 줄에 붙는다. + one=$(check_journal "$f" "$JOURNAL_INDEX") + [[ -n "$one" ]] && jout+="$one"$'\n' + done jout=$(printf '%s' "$jout" | grep -v '^[[:space:]]*$') if [[ -n "$jout" ]]; then say "$jout"; findings=$((findings + 1)) else say " 위반 없음"; fi @@ -198,11 +218,27 @@ self_test() { # record 와 클래스가 한 파일에 섞여도 클래스만 잡는다 probe "record 가 있어도 다른 클래스는 검사한다" "src/main/java/H.java" \ $'public record Marker(int x) {}\n\nclass H {\n public H() {}\n}\n' "JS-12" + # **색인을 갈아 끼운다.** 저장소의 진짜 색인을 읽으면 누가 이 ID 를 + # 실제로 등록하는 순간 검사가 아니라 상태가 결과를 바꾼다. + JOURNAL_INDEX="$tmp/index.md" + printf '| [AIJ-9990](x.md) | 2026-08-20 | implement | 있음 | high | — |\n' \ + > "$JOURNAL_INDEX" + # 프론트매터를 통째로 빠뜨린 글이 CI 까지 갔다. 검사가 실제로 무는지 본다. probe "저널 프론트매터 누락" "ai/journal/2026/08/AIJ-9998-probe.md" \ $'# 제목\n\n- **날짜** 2026-08-20\n' "JN-1" probe "저널이 색인에 없음" "ai/journal/2026/08/AIJ-9999-probe.md" \ $'---\nid: AIJ-9999\ndate: 2026-08-20\nkind: implement\nconfidence: high\n---\n\n# 제목\n' "JN-2" + # 본문의 'date:' 한 줄이 프론트매터 행세를 하던 구멍 + probe "본문 키는 프론트매터가 아니다" "ai/journal/2026/08/AIJ-9997-probe.md" \ + $'# 제목\n\nid: AIJ-9997\ndate: 2026-08-20\nkind: implement\nconfidence: high\n' "JN-1" + # 끝 경계가 없어 'AIJ-9990-extra' 가 'AIJ-9990' 으로 잘리던 구멍 + probe "id 는 값 전체가 맞아야 한다" "ai/journal/2026/08/AIJ-9996-probe.md" \ + $'---\nid: AIJ-9990-extra\ndate: 2026-08-20\nkind: implement\nconfidence: high\n---\n' "JN-1" + # 색인의 AIJ-9990 이 AIJ-999 를 통과시키던 구멍 + probe "색인 부분 일치는 등록이 아니다" "ai/journal/2026/08/AIJ-999-probe.md" \ + $'---\nid: AIJ-999\ndate: 2026-08-20\nkind: implement\nconfidence: high\n---\n' "JN-2" + JOURNAL_INDEX="ai/journal/index.md" # **상대경로 회귀.** git 은 선행 슬래시 없는 경로를 준다. 절대경로로 # 안 바꾸면 테스트 전용 검사가 아예 안 돌고(미탐), 동시에 테스트가 diff --git a/build.gradle b/build.gradle index f5311347..498dc301 100644 --- a/build.gradle +++ b/build.gradle @@ -145,16 +145,42 @@ tasks.named('jacocoTestCoverageVerification') { } } -// 어댑터는 통합 시험이 실제로 레디스를 치며 도는 곳이라 단위 exec 로는 -// 잴 수 없다. 계층이 잡으로 갈려 exec 가 한자리에 안 모이므로, 통합 exec -// 하나로 어댑터만 따로 잰다. +// 어댑터는 단위·통합 양쪽에서 덮인다. JaCoCo 는 Lua 를 못 보고 어댑터의 +// Java 는 대부분 단위 시험이 도는 순수 로직이라, 통합 exec 만 보면 엉뚱한 +// 것을 잰다 (AIJ-0024). 계층이 잡으로 갈려 exec 가 한자리에 안 모이므로 +// 두 exec 를 합쳐 어댑터만 따로 잰다. +// +// **보고 대상도 어댑터로 좁힌다.** main 전체를 넣으면 도메인과 부트 클래스가 +// 섞여 들어가 "어댑터 보고서" 라는 이름이 거짓이 된다. +def 어댑터클래스 = { -> + sourceSets.main.output.classesDirs.asFileTree.matching { + include 'com/kafkick/waiting/adapter/redis/**' + } +} + +tasks.register('adapterCoverageReport', JacocoReport) { + description = '어댑터 커버리지 보고 — 단위·통합 결합 기준' + group = 'verification' + dependsOn test, integrationTest + executionData test, integrationTest + sourceDirectories.setFrom(sourceSets.main.java.sourceDirectories) + classDirectories.setFrom(어댑터클래스()) + reports { + xml.required = true + html.required = true + } +} + tasks.register('adapterCoverageVerification', JacocoCoverageVerification) { - description = '어댑터 커버리지 임계 — 통합 시험 기준' + description = '어댑터 커버리지 임계 — 단위·통합 결합 기준' group = 'verification' dependsOn test, integrationTest executionData test, integrationTest sourceDirectories.setFrom(sourceSets.main.java.sourceDirectories) - classDirectories.setFrom(sourceSets.main.output) + classDirectories.setFrom(어댑터클래스()) + // **못 넘겼을 때가 보고서가 가장 필요한 때다.** finalizer 로 걸지 않으면 + // 실패한 실행에서만 보고서가 없어 어디가 빈지 볼 수 없다. + finalizedBy 'adapterCoverageReport' violationRules { rule { element = 'PACKAGE' @@ -167,19 +193,6 @@ tasks.register('adapterCoverageVerification', JacocoCoverageVerification) { } } -tasks.register('adapterCoverageReport', JacocoReport) { - description = '어댑터 커버리지 보고 — 임계를 못 넘겼을 때 어디가 빈지 본다' - group = 'verification' - dependsOn test, integrationTest - executionData test, integrationTest - sourceDirectories.setFrom(sourceSets.main.java.sourceDirectories) - classDirectories.setFrom(sourceSets.main.output) - reports { - xml.required = true - html.required = true - } -} - tasks.named('check') { dependsOn jacocoTestCoverageVerification } From 3d296baee3d95e9f77e6bf663f7977dcf932a699 Mon Sep 17 00:00:00 2001 From: UHeeJoon Date: Thu, 20 Aug 2026 13:55:18 +0900 Subject: [PATCH 4/5] =?UTF-8?q?docs(journal):=20=EC=BB=A4=EB=B2=84?= =?UTF-8?q?=EB=A6=AC=EC=A7=80=20=EB=A6=AC=EB=B7=B0=20=EC=A7=80=EC=A0=81=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refs: CY-236 --- ai/journal/2026/08/AIJ-0024-adapter-coverage.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ai/journal/2026/08/AIJ-0024-adapter-coverage.md b/ai/journal/2026/08/AIJ-0024-adapter-coverage.md index 16bc35a5..21092008 100644 --- a/ai/journal/2026/08/AIJ-0024-adapter-coverage.md +++ b/ai/journal/2026/08/AIJ-0024-adapter-coverage.md @@ -5,7 +5,7 @@ kind: decision phase: 3 plan: [3.9.1] jira: CY-236 -commits: [64e1f18] +commits: [64e1f18, 0252dec, f864363] agent: claude-opus-5 confidence: medium promoted-to: @@ -56,6 +56,16 @@ promoted-to: 바꾸기도 쉽다** — 아티팩트 배선은 한 번 깔면 걷어내기 번거롭다. 통합이 분 단위로 늘면 그때 전자로 옮긴다. +## 리뷰에서 더 나온 것 + +- **보고 대상을 어댑터로 좁혔다.** `sourceSets.main.output` 전체를 넣어 + 도메인과 부트 클래스가 보고서에 섞였다 — "어댑터 보고서" 라는 이름이 + 거짓이었다. +- **보고서를 검증의 finalizer 로 걸었다.** 임계를 못 넘긴 실행에서만 보고서가 + 없었다. **못 넘겼을 때가 보고서가 가장 필요한 때다.** +- **`description` 이 "통합 시험 기준" 이었다.** 실제로는 단위·통합을 합치는데 + CI 출력이 계산 범위를 잘못 안내했다. + ## 확신이 낮은 부분 - **80 의 근거가 약하다.** 지금 실측이 91.7% 라 여유가 있어 잡은 값이다. From 00b1a13e77482ac63c6af132c8c4a895fb7dd995 Mon Sep 17 00:00:00 2001 From: UHeeJoon Date: Thu, 20 Aug 2026 16:00:32 +0900 Subject: [PATCH 5/5] =?UTF-8?q?fix(hooks):=20=ED=94=84=EB=A1=A0=ED=8A=B8?= =?UTF-8?q?=EB=A7=A4=ED=84=B0=20=EB=8B=AB=EB=8A=94=20=EA=B5=AC=EB=B6=84?= =?UTF-8?q?=EC=9E=90=20=ED=99=95=EC=9D=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 여는 줄만 보고 끝까지 읽으면 안 닫힌 파일에서 본문 전체가 프론트매터 행세를 해 같은 구멍이 다시 열린다. Refs: CY-236 --- .claude/hooks/review-branch.sh | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/.claude/hooks/review-branch.sh b/.claude/hooks/review-branch.sh index c93d9c1e..9dd45205 100755 --- a/.claude/hooks/review-branch.sh +++ b/.claude/hooks/review-branch.sh @@ -108,9 +108,19 @@ check_journal() { local file=$1 index=${2:-ai/journal/index.md} out="" k id front # **프론트매터 안만 본다.** 파일 전체를 훑으면 본문에 'date:' 한 줄만 # 있어도 프론트매터가 있는 것으로 쳐서 검사가 통과한다. - front=$(awk 'NR==1 && $0!="---" {exit} NR==1 {next} $0=="---" {exit} {print}' "$file") - if [[ -z "$front" ]]; then - printf '%s' " JN-1 $file:1 ← 프론트매터가 없다"$'\n' + # + # **닫는 --- 까지 확인한다.** 여는 줄만 보고 끝까지 읽으면 닫히지 않은 + # 파일에서 본문 전체가 프론트매터 행세를 해 같은 구멍이 다시 열린다. + local status + front=$(awk ' + NR == 1 { if ($0 != "---") exit 1; next } + $0 == "---" { closed = 1; exit 0 } + { print } + END { if (!closed) exit 1 } + ' "$file") + status=$? + if ((status != 0)); then + printf '%s' " JN-1 $file:1 ← 프론트매터가 없거나 --- 로 닫히지 않았다"$'\n' return fi for k in id date kind confidence; do @@ -235,6 +245,9 @@ self_test() { # 끝 경계가 없어 'AIJ-9990-extra' 가 'AIJ-9990' 으로 잘리던 구멍 probe "id 는 값 전체가 맞아야 한다" "ai/journal/2026/08/AIJ-9996-probe.md" \ $'---\nid: AIJ-9990-extra\ndate: 2026-08-20\nkind: implement\nconfidence: high\n---\n' "JN-1" + # 여는 --- 만 있고 안 닫힌 파일이 본문까지 프론트매터로 치던 구멍 + probe "닫는 구분자가 없으면 프론트매터가 아니다" "ai/journal/2026/08/AIJ-9995-probe.md" \ + $'---\nid: AIJ-9990\ndate: 2026-08-20\nkind: implement\nconfidence: high\n\n# 제목\n' "JN-1" # 색인의 AIJ-9990 이 AIJ-999 를 통과시키던 구멍 probe "색인 부분 일치는 등록이 아니다" "ai/journal/2026/08/AIJ-999-probe.md" \ $'---\nid: AIJ-999\ndate: 2026-08-20\nkind: implement\nconfidence: high\n---\n' "JN-2"