Skip to content

test(F-INT-001): 에러 코드 다섯째 사본을 대조에 넣는다 — tsc 는 늦게 잡고, 느슨해지면 못 잡는다 - #571

Merged
hd0rable merged 3 commits into
mainfrom
fix/error-code-fifth-copy
Sep 9, 2026
Merged

hd0rable merged 3 commits into
mainfrom
fix/error-code-fifth-copy

Conversation

@hd0rable

@hd0rable hd0rable commented Sep 9, 2026 •

Copy link
Copy Markdown
Member

ErrorCodeContractTest 가 계약의 다섯 번째 사본까지 본다. #542 리뷰에서 제가 뗀다고 한
자리입니다.

무엇이 문제였나

web/src/lib/errorText.ts 의 Record<ErrorCode, string> 이 코드마다 사용자 문면을 하나씩
드는데, 그 사본은 tsc 만 지켰습니다. tsc 가 지키는 것 자체는 괜찮은데 순서가 나쁩니다.

Note

리뷰가 「main 이 깨졌다」를 실측으로 반증했고, 그대로 정정했습니다(5009302). 첫 부모
이력에서 두 파일이 다 있는 154 커밋을 대조했고 어긋남이 0건입니다. 깨진 커밋은
main 의 tip 이었던 적이 없습니다 — 고침 커밋과 같은 머지로 들어왔습니다. 아래 도식과
논지는 그 결과로 바뀌었고, 이 PR 이 하려는 말은 더 정확해졌습니다.

전 — 코드를 하나 더하는 사람이 지나가는 길

  핸들러에 코드 추가
      ↓
  ./gradlew test        ◇ openapi·CLAUDE.md·types.ts 를 맞추라고 넷을 시킨다
      ↓                    맞추면 → 초록 ✓ "다 맞췄다"
  커밋 · 푸시
      ↓
  CI                    ◇ types.ts 를 건드렸으니 web 스텝이 돈다 → tsc 빨강
      ↓                    머지는 막힌다 ✓ (main 은 안 깨진다)
  CI 로그를 열어서 안다   ✗ 서버만 고친 사람은 여기를 늦게 본다
                        ← #527 브랜치에서 5분 뒤 고침 커밋이 붙은 자리다

  ◇ 그리고 표를 Partial<…> 로 느슨하게 바꾸면    tsc 가 아무것도 안 잡는다  ✗ 흔적 없음


후 — 같은 길

  핸들러에 코드 추가
      ↓
  ./gradlew test        ◇ 다섯을 시킨다 — errorText.ts 의 문면까지  ◀━━
      ↓                    하나라도 빠지면 → 빨강, 그 자리에서 ✓
  커밋 · 푸시
      ↓
  CI                    tsc 는 둘째 그물로 남는다
                        ◇ 표가 느슨해지는 변경도 여기서 잡힌다 ✓ 남음
                          (그 변경은 web/ 만 건드리므로 ci.yml 의 server 판별이 있어야 한다)

새로 남는 것 둘입니다. 하나, 서버 쪽 한 번의 실행이 다섯 사본을 다 말해 주는 것 — 빨강이
CI 로그에서 로컬로 옮겨 옵니다. 둘, 표가 느슨해지는 갈래 — 지금은 아무 그물도 없습니다.

❗이 대조가 tsc 보다 세다

전체 맵이라 코드가 늘면 컴파일이 깨지는 것이 그 파일의 설계인데, 누가 표를 Partial<…> 나
Record<string, string> 으로 느슨하게 바꾸면 tsc 는 그 순간부터 아무것도 안 잡습니다.
여기는 키 집합을 계약과 직접 맞추므로 그대로 잡습니다.

역검증 — 변이를 넣어 빨개지는 것을 봤다

표에서 코드 한 줄 삭제(FORBIDDEN)        빨강
Partial 로 바꾸고 코드 삭제               빨강   ← tsc 가 놓치는 자리
표를 다른 이름으로(const MESSAGES)        빨강 — 「못 찾았다」로. 조용히 안 통과한다

❗입력 선언이 같은 커밋에 있어야 한다 — 실측으로 알았다

처음엔 변이를 넣어도 BUILD SUCCESSFUL 이었습니다. Gradle 이 test 를 UP-TO-DATE 로
건너뜁니다 — 그 파일은 소스셋 밖이라 입력으로 선언해야 하고, 앞의 네 사본은 이미 선언돼
있었습니다(build.gradle 이 그 이유를 각각 적어 뒀습니다). 한 줄 더했습니다.

그러자 CiServerFilterMirrorsGradleInputsTest 가 빨개졌습니다.

정본(build.gradle 의 inputs.file)에만 있고 사본(ci.yml 의 server 판별)에 없는 경로다.
Expecting empty but was: ["web/src/lib/errorText.ts"]

설계된 그대로입니다 — 정본에만 넣으면 그 파일만 고친 PR 에서 server 잡이 아예 안 떠서
지키려던 대조가 안 돕니다(#285 · #270). 그래서 ci.yml 의 server_extra 에도 같은
경로를 더했습니다. ^web/ 을 통짜로 넓히지 않는다는 그 블록의 단정은 그대로입니다 —
파일 하나를 이름으로 더한 것뿐입니다.

바꾼 파일

  • server/src/test/.../ErrorCodeContractTest.java — 다섯째 대조와 그 리더 추가.
    표를 못 찾으면(이름이 바뀌면) 조용히 통과하지 않고 그 자리에서 빨개집니다.
    @DisplayName 도 다섯을 적습니다.
  • server/build.gradle — web/src/lib/errorText.ts 를 test 입력으로 선언. 위 실측을
    주석에 적었습니다(선언을 빼면 변이가 안 잡힌다).
  • .github/workflows/ci.yml ❗소유자 파일입니다 — server_extra 에 경로 하나.
    위 대조 테스트가 요구하는 것이고 메시지가 지시하는 자리라 같은 커밋에 넣었습니다.
    곁가지로 그 블록의 주석 목록이 실물보다 한 줄 짧았습니다 — ai-service/app/rubrics/ 가
    빠져 있어서 같이 채웠습니다(대조하는 것은 RiskItemCatalogMirrorsRubricsTest, #346).
  • CLAUDE.md — 「네 벌」 → 「다섯 벌」. tsc 문단은 지우지 않고 늦게 잡는 것과 못 잡는
    갈래
    로 고쳤고, "main 이 깨진 적은 없다" 를 실측과 함께 적었습니다 — 그 파일을 근거로
    읽는 사람이 CI 의 web 스텝이 그것을 막고 있다는 사실을 봐야 하기 때문입니다(리뷰 지적).

뒤따르는 것 하나 — 남의 파일이라 이슈로 뗐습니다

web/src/lib/errorText.ts:26-28 이 스스로 "계약 ↔ web 대조를 ErrorCodeContractTest 의 네
번째 대상으로 넣을지는 판단이 필요하고, 그때까지는 이 문단이 유일한 방어다"
라고 적는데
main 에서 이미 거짓입니다(네 번째는 #316 이 넣었고 webUnionMatchesContract 가 돕니다).
이 PR 이 머지되면 다섯째까지 생겨 두 배로 거짓이 됩니다. 소유자 파일이라 결정 9.5 대로
근거만 넘깁니다.

검증

./gradlew test 799건 통과. web 파일은 변이 실험 후 원복해서 이 PR 에 없습니다.

`web/src/lib/errorText.ts` 의 `Record<ErrorCode, string>` 이 계약의 다섯 번째 사본인데
`ErrorCodeContractTest` 는 넷까지만 봤다. 그래서 순서가 나빴다 — 서버 쪽 넷을 맞추면
대조가 초록이 되고, 문면을 안 쓴 것은 `npm run build` 를 돌리는 사람에게만 보인다.
서버만 고치는 사람은 그것을 안 돌린다. `DOCUMENT_UNPROCESSABLE` 을 넣은 커밋이 실제로
그렇게 main 을 깨뜨렸다(#521).

이 대조는 tsc 보다 세다. 표를 `Partial<…>` 이나 `Record<string, string>` 으로 느슨하게
바꾸면 tsc 는 그 순간부터 아무것도 안 잡는데, 여기는 키 집합을 계약과 직접 맞춘다.

## 역검증 — 변이를 넣어 빨개지는 것을 확인했다

  표에서 코드 한 줄 삭제(FORBIDDEN)          빨강
  Partial 로 바꾸고 코드 삭제                 빨강 (tsc 가 놓치는 자리)
  표를 다른 이름으로(const MESSAGES)          빨강 — 「못 찾았다」로. 조용히 통과하지 않는다

## 입력 선언이 같은 커밋에 있어야 한다

처음엔 변이를 넣어도 `BUILD SUCCESSFUL` 이었다 — Gradle 이 test 를 UP-TO-DATE 로
건너뛴다. 그 파일은 소스셋 밖이라 입력으로 선언해야 하고, 앞의 네 사본은 이미 선언돼
있었다. `build.gradle` 에 한 줄 더했다.

그러자 `CiServerFilterMirrorsGradleInputsTest` 가 빨개졌다 — 정본(build.gradle)에만 있고
사본(ci.yml 의 server 판별)에 없다고. 설계된 대로다. `ci.yml` 의 `server_extra` 에도
같은 경로를 더했다. 안 더하면 그 파일만 고친 PR 에서 server 잡이 아예 안 뜬다.

곁가지로 그 블록의 주석 목록이 실물보다 두 줄 짧았다 — `ai-service/app/rubrics/` 가
빠져 있어서 같이 채웠다(대조하는 것은 `RiskItemCatalogMirrorsRubricsTest`).

`./gradlew test` 799건 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions github-actions Bot added 리뷰대기: 오준서 오준서 이 배정됐고 아직 아무것도 제출하지 않았다 리뷰대기: 윤지석 윤지석 이 배정됐고 아직 아무것도 제출하지 않았다 리뷰대기: 정세현 정세현 이 배정됐고 아직 아무것도 제출하지 않았다 labels Sep 9, 2026
@hd0rable hd0rable added server Spring (:8000) 인프라 CI·배포·실행 환경·개발환경 (인프라 R: 오준서) 계약 모듈 간 계약 — contracts/, openapi.yaml, 스키마 labels Sep 9, 2026

@gitIt-sehyeon gitIt-sehyeon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

그물은 맞습니다 — 역검증 셋을 다 재현했고, 제가 둘을 더 넣어 봤습니다. 그런데 main 이 깨졌다 가 실측과 다릅니다. 그 문장이 지금 PR 제목·javadoc·CLAUDE.md 세 곳에 있어서 변경을 요청합니다. 고치면 이 PR 이 하려는 말이 오히려 더 정확해집니다 — 진짜 값은 다른 자리에 있습니다.

재현한 것 — 전부 맞습니다

기준선                                   BUILD SUCCESSFUL (799건, 35s)
FORBIDDEN 한 줄 삭제                      BUILD FAILED
Partial<…> 로 바꾸고 코드 삭제             BUILD FAILED  (ErrorCodeContractTest.java:163)
표 이름을 const MESSAGES 로                BUILD FAILED  (:173 — 「못 찾았다」)

inputs.file 주장도 그대로입니다. 선언을 빼면 변이가 안 잡힙니다.

선언 없음 + 변이 있음    > Task :test UP-TO-DATE   BUILD SUCCESSFUL   ← 안 잡는다
선언 되살림 + 같은 변이   > Task :test FAILED       BUILD FAILED

ci.yml 대조도 확인했습니다. server_extra 에서 errorText.ts 를 되돌리면 CiServerFilterMirrorsGradleInputsTest 가 빨개집니다. 그러니 세 파일이 한 커밋에 있어야 하는 것이 맞습니다.

제가 더 넣어 본 둘

표 안에 `// NOTE: …` 주석 한 줄        BUILD SUCCESSFUL  ← 정규식이 `//` 를 안 넘는다. 안전

(?m)^\s*([A-Z_]+): 가 헛 코드를 만들 수 있나 봤는데 안 만듭니다. 그리고 빗나가는 방향이 전부 빨강입니다 — 키를 따옴표로 싸도("NOT_FOUND":), 두 항목을 한 줄에 써도, 주석 처리해도 집합이 작아져 빨개집니다. 조용히 통과하는 경로를 못 찾았습니다.

❗main 이 깨졌다 — main 의 tip 은 한 번도 안 깨졌습니다

먼저 그 두 커밋이 같은 머지에 들어갔습니다.

c051775  2026-09-07 13:40:28  DOCUMENT_UNPROCESSABLE 도입 (#527 리뷰)
7614628  2026-09-07 13:45:47  에러 코드의 다섯 번째 자리를 채운다 — 앞 커밋이 웹 빌드를 깨뜨렸다
                              ↑ 5분 차이 · 둘 다 b88671d (Merge pull request #527) 로 들어왔다
$ git log --oneline --merges c051775..7614628      (사이에 머지 없음)
$ git rev-list --first-parent origin/main | grep -c ^c051775   →  0

c051775 는 main 의 tip 이었던 적이 없습니다. 깨진 상태는 feat/upload-wiring-521 위에서 5분 살았습니다.

그리고 이력 전체를 훑었습니다. 각 커밋에서 openapi 의 ApiError.code enum 과 errorText.ts 키 집합을 대조했습니다.

main 의 첫 부모 이력 154 커밋 검사     ✅ 어긋난 것 0건
(참고) 모든 조상 392 커밋 검사         c051775 하나 — 그 기능 브랜치 커밋이다

왜 못 닿나 — 구조가 막고 있습니다

에러 코드를 하나 더하려면 web/src/api/types.ts 를 반드시 건드립니다. 기존 네 번째 대조(webUnionMatchesContract)가 유니온을 계약과 맞추라고 요구하니까요. 그러면

web/ 아래 파일이 바뀐다
  → ci.yml:188  web=false; hit '^(web/|…)' && web=true      → web=true
  → 「web · 타입 검사 + 빌드」 스텝(ci.yml:246, `npm run build` = tsc --noEmit && vite build)
  → 그 스텝은 **잡이 하나**인 `test`(= 커밋 상태 「테스트 (4모듈)」) 안에 있다
  → tsc 가 빨개지면 그 체크가 빨개진다  →  머지 전에 걸린다

즉 "서버만 고치는 사람은 npm run build 를 안 돌린다" 는 맞지만, CI 가 대신 돌리고 그것이 머지를 막습니다. #527 에서 실제로 그렇게 걸려서 5분 뒤 커밋이 붙은 것으로 보입니다.

그런데 고치면 이 PR 의 논지가 더 세집니다

문장을 낮추라는 게 아닙니다. 지금 본문이 진짜 값을 부차로 적고 있습니다.

지금 적힌 값     "main 이 깨진다"                         ← 구조가 이미 막는다
실제 값 1        빨개지는 자리가 CI 에서 ./gradlew test 로 앞으로 온다
실제 값 2        Partial<…> 로 느슨해지면 tsc 는 아무것도 안 잡는다   ◀━ 이게 새 그물이다

2번이 이 PR 만 잡는 것이고, 본문의 "이 대조가 tsc 보다 세다" 절이 그것을 이미 말합니다. 그리고 그 절을 server_extra 변경이 완성합니다 — 표를 느슨하게 바꾸는 것은 web/ 만 건드리는 변경이라, errorText.ts 를 서버 판별에 넣지 않으면 그 PR 에서 서버 잡이 아예 안 떠서 새 대조가 안 돕니다. 제가 위에서 재본 그 빨강이 정확히 그 자리를 지킵니다.

그러니 순서를 바꿔 주시면 좋겠습니다 — "tsc 가 못 잡는 것이 있다(느슨해지는 것) + 잡는 것도 너무 늦게 잡는다(CI)" 가 참이고, "main 이 깨진다" 는 아닙니다.

고쳐야 하는 세 자리

PR 제목                              「tsc 만 잡아서 main 이 깨졌다」
ErrorCodeContractTest.java javadoc   「실제로 그렇게 … main 을 깨뜨렸다 (#521)」
CLAUDE.md                            「실제로 그렇게 main이 깨졌다(#521)」

❗**CLAUDE.md 쪽이 제일 중요합니다.** 그 파일은 다음 사람이 근거로 읽는 자리이고, "main 이 깨진 적이 있다" 로 적히면 CI 의 web 스텝이 그것을 막고 있다는 사실이 안 보입니다. 그러면 다음에 그 스텝을 조건부로 만들거나 잡을 쪼개려는 사람이 자기가 무엇을 무는지 모릅니다. 제안 문면입니다.

❗다섯 번째 자리는 tsc도 잡는다 — 그런데 늦게 잡고, 못 잡는 갈래가 있다. 문면 표가 전체 맵이라 코드를 더하고 문면을 안 쓰면 npm run build가 깨진다. 코드를 더하면 유니온(types.ts)도 고쳐야 하므로 CI의 web 스텝이 반드시 돌고, 그것이 머지 전에 막는다(그래서 main이 깨진 적은 없다 — 첫 부모 이력 154 커밋 실측). 문제는 둘이다. 하나, 빨개지는 자리가 CI다 — 앞 네 대조가 초록이라 "다 맞췄다"로 읽고 서버만 고친 사람은 CI 로그를 안 본다(#527에서 그렇게 5분 뒤 고침 커밋이 붙었다). 둘, 누가 그 표를 Partial<…>이나 Record<string, string>으로 느슨하게 바꾸면 tsc는 그 순간부터 아무것도 안 잡는다. 그래서 같은 테스트가 그 표까지 보고, errorText.ts를 서버 판별(ci.yml의 server_extra)에도 넣었다 — 표만 느슨하게 바꾸는 PR은 web/만 건드려서 서버 잡이 안 뜬다. 문면 규칙은 그 파일 주석에 있다.

❗딸린 것 — errorText.ts 의 주석이 이미 낡았습니다 (오준서 님 파일)

그 파일이 스스로 이렇게 적고 있습니다(web/src/lib/errorText.ts:26-28).

계약 ↔ web 대조를 ErrorCodeContractTest 의 네 번째 대상으로 넣을지는 @hd0rable 판단이 필요하고, 그때까지는 이 문단이 유일한 방어다.

main 에서 이미 거짓입니다. 네 번째 대상은 #316 이 넣었고 지금 webUnionMatchesContract(ErrorCodeContractTest.java:113)가 돌고 있습니다. 이 PR 이 머지되면 다섯째까지 생겨서 두 배로 거짓이 됩니다.

web/ 파일을 이 PR 에서 뺀 판단(변이 실험 후 원복)에는 동의합니다 — 그러면 별건이 필요합니다. 오준서 님 파일이니 그쪽에 요청하시거나 이슈로 떼는 쪽이 좋겠습니다. "이 문단이 유일한 방어다" 가 남아 있으면 다음 사람이 그것을 근거로 테스트를 안 찾습니다.

나머지 확인

ci.yml 주석 목록이 실물보다 한 줄 짧았다   맞습니다 — 정규식에는 ai-service/app/rubrics/ 가
                                        있고 주석 목록에만 없었다. 같이 채운 것이 맞다
build.gradle 주석의 실측 기록             제가 재현한 결과와 같다
@DisplayName 다섯으로 갱신                반영됨
`^web/` 을 통짜로 안 넓힌 것              단정 그대로 유지됨 — 파일 하나를 이름으로 더했다

제 쪽에서 뒤따를 것

#542(공개 README)의 「에러 코드 네 벌」이 이 PR 이 머지되면 다섯 벌이 됩니다. 순서를 그쪽으로 두라고 하신 것(#542 승인 코멘트)이 맞고, 이 PR 이 머지되면 제가 바로 고칩니다. 표가 가리키는 테스트가 실제로 다섯을 볼 때 수를 바꾸는 것이 그 표의 규칙입니다.

문면 셋만 고쳐지면 승인하겠습니다. 그물 자체는 이견이 없습니다 — 변이 다섯 개를 넣어 봤고 조용히 통과하는 경로가 없었습니다.

@github-actions github-actions Bot added 리뷰중: 정세현 정세현 이 코멘트·변경요청을 냈고 아직 승인하지 않았다 and removed 리뷰대기: 정세현 정세현 이 배정됐고 아직 아무것도 제출하지 않았다 labels Sep 9, 2026
리뷰가 실측으로 반증했고 제가 다시 셌다. 첫 부모 이력에서 두 파일이 다 있는 154 커밋을
대조했고 어긋남이 0건이다. 깨진 커밋(c051775)은 main 의 tip 이었던 적이 없다 — 고침
커밋과 **같은 머지(b88671d)로** 들어왔고 그 사이에 머지가 없다.

못 닿는 이유가 구조에 있다. 코드를 더하면 네 번째 대조가 `web/src/api/types.ts` 도
고치라고 요구하고, 그러면 ci.yml 의 web 판별이 켜져 `npm run build`(tsc --noEmit &&
vite build)가 반드시 돈다. 그 스텝이 잡 하나 안에 있어서 커밋 상태가 빨개지고 머지를
막는다.

그래서 이 대조가 값을 내는 자리를 둘로 다시 적었다.

  시점   앞 네 대조가 초록이면 「다 맞췄다」로 읽고 npm run build 를 안 돌린다 —
         빨강을 CI 로그에서 만난다(#527 브랜치에서 5분 뒤 고침 커밋이 붙었다).
         여기서 보면 ./gradlew test 한 번에 같이 나온다
  갈래   표를 Partial<…> 로 느슨하게 바꾸면 tsc 는 그 순간부터 아무것도 안 잡는다.
         이 대조는 키 집합을 계약과 직접 맞추므로 그대로 잡는다

`ci.yml` 의 `server_extra` 가 왜 같은 커밋에 있어야 하는지도 그 갈래로 설명이 붙는다 —
표만 느슨하게 바꾸는 변경은 `web/` 만 건드려서 server 잡이 안 뜬다. build.gradle 주석에
그 한 줄을 더했다.

`./gradlew test` 799건 통과.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@hd0rable hd0rable changed the title test(F-INT-001): 에러 코드 다섯째 사본을 대조에 넣는다 — tsc 만 잡아서 main 이 깨졌다 test(F-INT-001): 에러 코드 다섯째 사본을 대조에 넣는다 — tsc 는 늦게 잡고, 느슨해지면 못 잡는다 Sep 9, 2026
@hd0rable

hd0rable commented Sep 9, 2026

Copy link
Copy Markdown
Member Author

지적이 맞습니다. 제가 다시 셌고 같은 결과입니다 — 문면 셋 다 고쳤습니다(5009302).

반증을 제 손으로 재현했다

git rev-list --first-parent origin/main | grep -c ^c051775      →  0
git log --oneline --merges c051775..7614628                     →  0줄
둘을 들여온 머지                                                  b88671d (Merge PR #527)

이력 대조(openapi ApiError.code enum ≡ errorText.ts 키 집합)
  first-parent 392 커밋 중 두 파일이 다 있는 154 커밋 검사        어긋남 0건
  c051775 만 어긋난다: 계약 14 · 표 13 (DOCUMENT_UNPROCESSABLE 없음)

main 의 tip 은 한 번도 그 상태였던 적이 없습니다. 깨진 것은 feat/upload-wiring-521
위에서 5분입니다.

구조가 막고 있다는 것도 확인했습니다.

ci.yml:342·355   web 스텝의 조건이 steps.filter.outputs.web == 'true'
ci.yml:359       npm run build = tsc --noEmit && vite build
ci.yml:112-113   그 스텝이 잡 하나(test = 커밋 상태 「테스트 (4모듈)」) 안에 있다

코드를 더하면 네 번째 대조가 types.ts 를 고치라고 요구하므로 web=true 가 되고, 그
스텝이 반드시 돕니다. 머지 전에 걸립니다.

고친 것

지적하신 대로 논지의 순서를 바꿨습니다 — 값은 「깨진다」가 아니라 시점과 갈래 둘입니다.

시점   앞 네 대조가 초록이면 「다 맞췄다」로 읽고 npm run build 를 안 돌린다 —
       빨강을 CI 로그에서 만난다(#527 브랜치의 5분 뒤 고침 커밋이 그 자리다).
       이 대조가 있으면 ./gradlew test 한 번에 같이 나온다
갈래   표를 Partial<…> · Record<string, string> 로 느슨하게 바꾸면 tsc 는 그 순간부터
       아무것도 안 잡는다. 이 대조는 키 집합을 계약과 직접 맞추므로 그대로 잡는다

세 자리 전부:

  • PR 제목 → "tsc 는 늦게 잡고, 느슨해지면 못 잡는다"
  • ErrorCodeContractTest javadoc → 「main 이 깨졌다」를 빼고, "main 이 깨진 적은 없다
    (첫 부모 154 커밋, 어긋남 0건)"
    를 실측으로 적고 시점·갈래 두 절로 갈랐습니다
  • CLAUDE.md → 제안 문면을 거의 그대로 받았습니다. 그 파일이 근거로 읽히는 자리라
    CI 의 web 스텝이 막고 있다는 사실이 보여야 한다는 지적이 정확합니다 — 그게 없으면
    다음에 그 스텝을 조건부로 만들거나 잡을 쪼개려는 사람이 자기가 무엇을 무는지 모릅니다
  • PR 본문 도식도 바꿨고, 정정 사실을 머리에 알림으로 남겼습니다

build.gradle 주석에 한 줄 더했습니다. 지적하신 대로 server_extra 가 같은 커밋에
있어야 하는 이유가 그 「갈래」로 설명됩니다 — 표만 느슨하게 바꾸는 변경은 web/ 만
건드려서, 없으면 그 PR 에서 server 잡이 아예 안 뜨고 새 대조가 안 돕니다.

넣어 보신 변이 둘

(?m)^\s*([A-Z_]+): 가 헛 코드를 만들지 못한다는 것과, 빗나가는 방향이 전부 빨강이라는
것 — 제가 안 재본 방향입니다. 특히 따옴표로 싼 키("NOT_FOUND":)를 넣어 보신 것이
그렇습니다. 그 형태로 표를 다시 쓰면 집합이 작아져 빨개지므로 조용히 통과하지 않습니다.

errorText.ts 주석 — 이슈로 뗐습니다

"이 문단이 유일한 방어다" 가 이미 거짓인 것도 맞습니다. 소유자 파일이라 결정 9.5 대로
근거만 넘겼습니다 — #573. 이 PR 이 머지되면 다섯째까지 생기니 그때 같이 고치는 것이
맞겠습니다.

#542 의 「네 벌」을 머지 후에 고쳐 주신다는 것도 그 순서가 맞습니다 — 표가 가리키는
테스트가 실제로 다섯을 볼 때 수를 바꾸는 것이 그 표의 규칙입니다.

./gradlew test 799건 통과.

@junseo2323 junseo2323 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

대조 자체는 옳고, ci.yml 한 줄도 그 테스트가 지시하는 자리가 맞습니다. 다만 제목이 본문의 정정과 반대로 말합니다 — 그것만 고쳐서 머지해 주세요.

먼저, 본문의 실측을 제가 다시 쟀습니다

main 의 first-parent 이력에서 두 파일이 다 있는 커밋마다 types.ts 유니온과 errorText.ts 표의 코드 집합을 비교했습니다.

두 파일이 다 있는 first-parent 커밋      154
코드 집합이 어긋난 커밋                    0

본문에서 정정한 쪽("main 이 깨진 적은 없다")이 맞습니다. 154 도 정확히 일치합니다. 코드를 더하면 네 번째 대조가 types.ts 를 요구하고, 그러면 web 스텝이 반드시 떠서 tsc 가 머지 전에 막습니다.

❗그래서 제목과 「전」 다이어그램이 낡았습니다

제목        "tsc 만 잡아서 main 이 깨졌다"
다이어그램   "main 이 깨진다   ← #521 에서 실제로 이렇게 났다"
javadoc     "main 이 깨진 적은 없다(154 커밋, 어긋남 0건)"
CLAUDE.md   같음

같은 PR 이 두 가지를 말합니다. 트리에 들어가는 파일 둘은 정정된 쪽이라 머지되는 내용은 옳은데, 제목이 머지 커밋 제목으로 이력에 남습니다. 이 저장소가 #542 에서 저한테 요구한 기준(재현 안 되는 수는 뺀다)을 그대로 적용하면 제목도 같은 자리입니다. 커밋 없이 고칠 수 있으니 머지 직전에 바꿔 주세요 — 예: "에러 코드 다섯째 사본을 대조에 넣는다 — tsc 는 늦게 잡고, 표가 느슨해지면 못 잡는다".

#521 인용도 같이 봐 주세요. 실제로 5분 뒤 고침 커밋이 붙은 것은 #527 브랜치이고(javadoc 이 그렇게 적었습니다), 브랜치에서 고쳐진 것이지 main 이 깨진 게 아닙니다.

대조의 값은 본문이 정리한 둘이 맞습니다

시점 — 앞 네 대조가 초록이면 「다 맞췄다」로 읽힙니다. ./gradlew test 한 번에 다섯이 다 나오는 쪽이 낫습니다.
갈래 — 표를 Partial<…>·Record<string, string> 으로 바꾸면 tsc 는 그 순간부터 아무것도 안 잡는데 여기는 키 집합을 계약과 직접 맞추므로 잡습니다. 이쪽이 진짜 새로 생기는 그물입니다.

읽은 것

ERROR_TEXT_ENTRY  (?m)^\s*([A-Z_]+):   주석줄(//)은 안 물림 ✓  표 15개 코드 전부 읽힘 ✓
표 못 찾을 때      isNotNegative 로 그 자리에서 빨강 — 조용히 통과 안 함 ✓
build.gradle      inputs.file 선언 없으면 UP-TO-DATE 로 변이가 안 잡힌다는 실측, 맞습니다
ci.yml            CiServerFilterMirrorsGradleInputsTest 가 정본↔사본을 물고 있어 한쪽만 넣으면 빨강
                  ^web/ 통짜로 안 넓힌 것도 그 블록 단정과 일관 ✓
CI                4모듈 전부 실행 · 초록

한 줄에 두 항목을 적으면(A: "x", B: "y") ^ 앵커라 뒤엣것을 놓치는데, 그때는 계약과 집합이 안 맞아 빨개지는 쪽으로 틀립니다. 조용히 통과하는 방향이 아니라 괜찮습니다.

곁가지로 채운 ai-service/app/rubrics/ 주석도 실물과 맞습니다.

제목만 정리하고 머지해 주세요. 승인합니다.

@github-actions github-actions Bot removed the 리뷰대기: 오준서 오준서 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 9, 2026

@gitIt-sehyeon gitIt-sehyeon left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인합니다. 요청한 세 자리가 다 고쳐졌고, 제가 안 요청한 자리 하나를 더 채우셨습니다.

다시 확인한 것

제목        「tsc 는 늦게 잡고, 느슨해지면 못 잡는다」                      ✓
CLAUDE.md   「그래서 main이 깨진 적은 없다(첫 부모 이력 … 154 커밋, 어긋남 0건)」  ✓
javadoc     「main 이 깨진 적은 없다」 + 시점·갈래 둘로 갈라 적었다           ✓
git grep    커밋된 문면에 「main 이 깨졌다」 잔존 0건                        ✓
./gradlew test                                              BUILD SUCCESSFUL (28s)
Partial<…> + 코드 삭제 변이                                   BUILD FAILED  — 그물 그대로

숫자도 재봤습니다. origin/main 의 첫 부모 이력이 394 커밋이고 그중 두 파일이 다 있는 것이 154 개입니다 — javadoc 의 문면과 맞습니다.

(사소한 것 하나. 코멘트 본문의 「first-parent 392 커밋 중」은 392 가 첫 부모가 아니라 모든 조상 기준 검사 수입니다. 첫 부모는 394 이고 그중 154 를 검사했습니다. 커밋된 문면은 정확하니 고칠 것은 없습니다.)

❗build.gradle 에 넣어 주신 주석이 이 PR 의 마지막 구멍을 닫습니다

제가 리뷰에서 "server_extra 변경이 그 절을 완성한다" 고만 적었는데, 그 근거를 선언 옆에 두셨습니다.

// ❗**`ci.yml` 의 server 판별에도 같은 경로가 있어야 한다**(CiServerFilterMirrorsGradleInputsTest
// 가 대조한다). 이 표를 `Partial<…>` 로 느슨하게 바꾸는 변경은 `web/` 만 건드려서, 없으면
// 그 PR 에서 server 잡이 아예 안 뜨고 새 대조가 안 돈다.

두 목록이 왜 같아야 하는지가 이제 두 파일에 다 있습니다. 대조 테스트는 「같아야 한다」를 강제하는데 왜 같아야 하는지는 안 말하고, ci.yml 주석은 목록을 나열하는 자리입니다. 그 이유가 inputs.file 선언 옆에 있으면, 다음에 여기에 파일을 더하는 사람이 ci.yml 쪽을 테스트가 시켜서가 아니라 알고 고칩니다.

정정된 문면이 원래보다 나은 이유

javadoc 이 값을 시점과 갈래 둘로 갈랐습니다. 그러면 이 그물의 범위가 문면에서 바로 읽힙니다.

시점    CI 로그 → ./gradlew test    이미 잡히던 것을 더 일찍 잡는다
갈래    Partial<…> 로 느슨해짐      tsc 가 아무것도 안 잡는 자리 — 이 대조만 잡는다

그리고 "#527 브랜치에서 5분 뒤 고침 커밋이 붙었다" 로 사실을 안 버리고 범위만 좁힌 것이 좋습니다. 그 사건은 실재하고, 그것이 증명하는 것이 「main 이 깨진다」가 아니라 「사람이 CI 로그를 보고 나서야 안다」였습니다.

뒤따를 제 몫

#542(공개 README)의 「에러 코드 네 벌」을 이 PR 이 머지되면 다섯 벌로 고칩니다. 그 표의 규칙(원칙 옆에 그것을 무는 테스트 이름)대로, 테스트가 실제로 다섯을 보게 된 뒤에 수를 바꾸는 순서입니다 — #542 승인 코멘트에서 그쪽으로 두라고 하신 그대로입니다.

errorText.ts 주석의 「유일한 방어」는 #573 으로 떼신 것을 확인했습니다.

@github-actions github-actions Bot removed the 리뷰중: 정세현 정세현 이 코멘트·변경요청을 냈고 아직 승인하지 않았다 label Sep 9, 2026

@yoonjiseok yoonjiseok left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인합니다. 변이를 전부 독립으로 다시 걸었고 다섯 다 본문대로입니다. 특히 입력 선언이 대들보라는 것을 양방향으로 확인했습니다 — 이 PR 에서 제일 놓치기 쉬운 자리인데 본문이 이미 짚어 뒀습니다.

환경 — 이 레포에서 server 를 로컬로 돌리는 법을 찾았습니다

제 머신에 JDK 21 만 등록돼 있어 ./gradlew test 가 이렇게 죽었습니다.

Cannot find a Java installation matching {languageVersion=17} for MAC_OS on aarch64

openjdk@17 이 brew 에 깔려 있는데 /usr/libexec/java_home 에 등록이 안 돼 있는 상태였습니다. 경로를 주면 돕니다 — 같은 상태인 분 있으면 쓰세요.

./gradlew test -Porg.gradle.java.installations.paths=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home

(#542 가 「JDK 17 이 build.gradle 의 원본」이라고 적어 둔 그 값이 실제로 이렇게 걸립니다.)

변이 셋 — 본문 표 그대로입니다

베이스라인을 먼저 한 번 돌려 UP-TO-DATE 갈래를 만든 뒤에 걸었습니다.

ⓐ 표에서 FORBIDDEN 한 줄 삭제              6 tests, 1 failed   (1 executed, 3 up-to-date)
ⓑ Partial<…> 로 바꾸고 FORBIDDEN 삭제      6 tests, 1 failed   ← tsc 가 놓치는 갈래
ⓒ 표 이름을 const MESSAGES 로               6 tests, 1 failed   「못 찾았다」로 — 조용히 안 통과
복원                                        BUILD SUCCESSFUL

ⓑ 가 이 PR 의 값입니다. 그 파일이 스스로 "부분 맵이면 새 코드가 조용히 기본 문면으로 떨어지고 그게 화면에서는 정상처럼 보인다" 로 위험을 적어 뒀는데, 그 위험을 막는 장치가 타입 하나뿐이라 타입을 느슨하게 하는 변경 앞에서 없어집니다. 자기가 적어 둔 이유를 자기가 못 지키는 구조였고, 여기가 그걸 밖에서 잡습니다.

❗입력 선언 — 양방향으로 쟀습니다

본문의 "처음엔 변이를 넣어도 BUILD SUCCESSFUL 이었다" 를 재현하려다 한 번 헛짚었습니다. build.gradle 을 고치면 그것만으로 태스크가 다시 돌아서, 선언을 지우고 같은 명령을 한 번에 돌리면 «잡혔다» 로 나옵니다. 단계를 갈라야 합니다.

1단계  선언 3줄을 지우고 먼저 한 번 돌린다      BUILD SUCCESSFUL  (1 executed)   ← 상태 확립
2단계  그 상태에서 errorText.ts 에 ⓐ 변이       BUILD SUCCESSFUL  (4 up-to-date) ❗변이가 조용히 통과
선언 복원 후 같은 변이                          BUILD FAILED      (1 executed)

선언이 없으면 4 up-to-date 로 아무것도 안 재고 초록입니다. 그래서 "이 줄이 대조와 같은 커밋에 있어야 한다" 가 맞고, build.gradle 주석에 그 실측을 적어 두신 것도 맞습니다. 제 노트에도 "Gradle 입력 등재는 깨끗한 워크트리에서 재면 안 된다 — UP-TO-DATE 갈래를 안 지나서 틀린 배선이 초록으로 안 보인다" 가 있는데, 재는 쪽에서 한 칸 더 있었습니다(빌드 스크립트를 건드리는 변이는 스스로 무효화한다). 다음에 쓰겠습니다.

❗ci.yml 쪽도 대들보입니다 — 그리고 주석이 대신 만족시키지 않습니다

server_extra 에서 web/src/lib/errorText\.ts$ 만 제거        CiServerFilterMirrorsGradleInputsTest → 1 failed
   그때 ci.yml 에 남은 errorText 언급: 1건 (주석 줄)         ← 그런데도 빨강

#368 이 "내가 방금 단 주석이 검사를 만족시킨다" 로 밟았던 자리인데, 이 대조는 판별에 실제로 쓰이는 줄만 봅니다. 주석 목록을 같이 늘리신 것(errorText.ts · rubrics/ 두 줄)도 사람이 읽는 쪽이라 맞습니다.

테스트 본체 — 닫히는 방향입니다

assertThat(from).as("ERROR_TEXT 표를 못 찾았다 — 이름이 바뀌었으면 …").isNotNegative();
assertThat(end).as("표의 끝을 못 찾았다 — 형식이 바뀐 것이다").isNotNegative();
assertThat(out).as("표에서 코드를 하나도 못 읽었다면 이 테스트의 정규식이 낡은 것이다").isNotEmpty();

0건 방지가 세 자리에 있습니다. 그리고 end 를 못 찾거나 표가 잘려도 코드가 모자라서 실패하지 조용히 통과하지 않습니다 — 어느 쪽으로 부서져도 빨강입니다.

정규식((?m)^\s*([A-Z_]+):)도 표 안의 주석에 안 걸립니다. 그 표에 // ❗「서비스 오류」로 적으면 안 된다 같은 줄이 여럿인데 전부 / 로 시작해서 안 물립니다(확인함).

정정을 그대로 실은 것

리뷰가 「main 이 깨졌다」를 실측으로 반증했고, 그대로 정정했습니다(5009302). … 깨진 커밋은 main 의 tip 이었던 적이 없습니다 — 고침 커밋과 같은 머지로 들어왔습니다.

PR 제목이 처음에 "tsc 만 잡아서 main 이 깨졌다" 였는데 본문·docstring·커밋을 전부 정정하셨고, 정정 뒤에 논지가 더 좁고 더 정확해졌습니다(«깨진다» → «빨개지는 시점과 갈래»). 틀린 근거를 뺐는데 결론이 살아남으면 그 결론이 원래 맞았던 것이고, 그게 이 PR 이 그렇습니다.

곁가지 둘 — 둘 다 이미 잡혀 있습니다

ⓐ errorText.ts 의 자기 주석이 이 PR 뒤에 더 거짓이 됩니다.

errorText.ts:24-25   "…네 번째 대상으로 넣을지는 @hd0rable 판단이 필요하고,
                      그때까지는 이 문단이 유일한 방어다."

이 PR 이 그 판단을 실행하므로 «판단이 필요하고» 도 «유일한 방어» 도 끝납니다. #573 이 이미 그걸로 열려 있고 오준서 님께 배정돼 있어서 여기서 막지 않습니다(그 파일도 오준서 님 것이고요).

ⓑ #542 의 README 가 「네 벌」로 적습니다. 이 PR 이 CLAUDE.md 를 다섯으로 바꾸는데, #542(열림·승인됨)가 README.md 에 이 줄을 더합니다.

| 에러 코드 네 벌(핸들러·OpenAPI·`CLAUDE.md`·프론트 유니온)이 같다 | `ErrorCodeContractTest` |

파일이 달라서 텍스트 충돌이 안 나고, 나중에 머지되는 쪽이 앞을 안 보므로 어느 순서든 한쪽이 낡습니다. #542 에도 같은 내용을 적었고 거기에 셋(미리 다섯으로 · 이 PR 이 README 도 · 수를 빼기)을 놓아 뒀습니다 — 이 PR 이 나중에 머지되면 여기서 README 한 줄을 같이 고치시는 게 제일 짧습니다. 어느 쪽이든 막지 않습니다.

@github-actions github-actions Bot removed the 리뷰대기: 윤지석 윤지석 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 9, 2026
@hd0rable
hd0rable merged commit 29eebee into main Sep 9, 2026
3 checks passed
gitIt-sehyeon added a commit that referenced this pull request Sep 9, 2026
#571 이 CLAUDE.md 만 고쳤고 여섯 자리가 남아 있었다(윤지석 전수). 그중
내 소유 셋을 여기서 닫는다.

  docs/functional-spec-v1.2.md:381   네 벌 → 다섯 벌
  docs/functional-spec-v1.2.md:549   네 벌 → 다섯 벌
  server/.../contract/WebTypesMirrorContractTest.java:43   네 벌 → 다섯 벌

❗그 줄을 고치려고 세어 보니 코드 수도 낡았다. openapi ApiError.code
enum 이 14종인데 v1.2 는 13종이라 적고, 목록에서 DOCUMENT_UNPROCESSABLE
가 빠져 있다 — 하필 이 이야기를 만든 그 코드다(#521/#527 이 다섯째 사본을
드러낸 그 추가). 그것도 같이 고쳤다.

이 수에는 어디에도 그물이 없다. ErrorCodeContractTest 가 CLAUDE.md 에서
읽는 것은 코드 목록이고 「다섯 벌」이라는 낱말이 아니다. 그래서 여섯
자리가 다 손으로 맞추는 산문이고, 한 곳만 고치면 나머지가 조용히 낡는다.

남은 셋은 남의 파일이다 — GlobalExceptionHandler:104·:116(강희진) ·
ai-service/tests/test_manual_source_propagates.py:106(윤지석, 본인이 정리).
gitIt-sehyeon added a commit that referenced this pull request Sep 9, 2026
…copy

docs: README 의 에러 코드 사본 수를 다섯으로 — #571 이 다섯째를 대조에 넣었다
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Spring (:8000) 계약 모듈 간 계약 — contracts/, openapi.yaml, 스키마 인프라 CI·배포·실행 환경·개발환경 (인프라 R: 오준서)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants