test(F-INT-001): 에러 코드 다섯째 사본을 대조에 넣는다 — tsc 는 늦게 잡고, 느슨해지면 못 잡는다 - #571
Conversation
`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>
gitIt-sehyeon
left a comment
There was a problem hiding this comment.
그물은 맞습니다 — 역검증 셋을 다 재현했고, 제가 둘을 더 넣어 봤습니다. 그런데 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 이 머지되면 제가 바로 고칩니다. 표가 가리키는 테스트가 실제로 다섯을 볼 때 수를 바꾸는 것이 그 표의 규칙입니다.
문면 셋만 고쳐지면 승인하겠습니다. 그물 자체는 이견이 없습니다 — 변이 다섯 개를 넣어 봤고 조용히 통과하는 경로가 없었습니다.
리뷰가 실측으로 반증했고 제가 다시 셌다. 첫 부모 이력에서 두 파일이 다 있는 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>
|
지적이 맞습니다. 제가 다시 셌고 같은 결과입니다 — 문면 셋 다 고쳤습니다( 반증을 제 손으로 재현했다
구조가 막고 있다는 것도 확인했습니다. 코드를 더하면 네 번째 대조가 고친 것지적하신 대로 논지의 순서를 바꿨습니다 — 값은 「깨진다」가 아니라 시점과 갈래 둘입니다. 세 자리 전부:
넣어 보신 변이 둘
|
junseo2323
left a comment
There was a problem hiding this comment.
대조 자체는 옳고, 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/ 주석도 실물과 맞습니다.
제목만 정리하고 머지해 주세요. 승인합니다.
gitIt-sehyeon
left a comment
There was a problem hiding this comment.
승인합니다. 요청한 세 자리가 다 고쳐졌고, 제가 안 요청한 자리 하나를 더 채우셨습니다.
다시 확인한 것
제목 「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 으로 떼신 것을 확인했습니다.
yoonjiseok
left a comment
There was a problem hiding this comment.
승인합니다. 변이를 전부 독립으로 다시 걸었고 다섯 다 본문대로입니다. 특히 입력 선언이 대들보라는 것을 양방향으로 확인했습니다 — 이 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 한 줄을 같이 고치시는 게 제일 짧습니다. 어느 쪽이든 막지 않습니다.
#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(윤지석, 본인이 정리).
…copy docs: README 의 에러 코드 사본 수를 다섯으로 — #571 이 다섯째를 대조에 넣었다
ErrorCodeContractTest가 계약의 다섯 번째 사본까지 본다.#542리뷰에서 제가 뗀다고 한자리입니다.
무엇이 문제였나
web/src/lib/errorText.ts의Record<ErrorCode, string>이 코드마다 사용자 문면을 하나씩드는데, 그 사본은
tsc만 지켰습니다. tsc 가 지키는 것 자체는 괜찮은데 순서가 나쁩니다.Note
리뷰가 「main 이 깨졌다」를 실측으로 반증했고, 그대로 정정했습니다(
5009302). 첫 부모이력에서 두 파일이 다 있는 154 커밋을 대조했고 어긋남이 0건입니다. 깨진 커밋은
main 의 tip 이었던 적이 없습니다 — 고침 커밋과 같은 머지로 들어왔습니다. 아래 도식과
논지는 그 결과로 바뀌었고, 이 PR 이 하려는 말은 더 정확해졌습니다.
새로 남는 것 둘입니다. 하나, 서버 쪽 한 번의 실행이 다섯 사본을 다 말해 주는 것 — 빨강이
CI 로그에서 로컬로 옮겨 옵니다. 둘, 표가 느슨해지는 갈래 — 지금은 아무 그물도 없습니다.
❗이 대조가 tsc 보다 세다
전체 맵이라 코드가 늘면 컴파일이 깨지는 것이 그 파일의 설계인데, 누가 표를
Partial<…>나Record<string, string>으로 느슨하게 바꾸면 tsc 는 그 순간부터 아무것도 안 잡습니다.여기는 키 집합을 계약과 직접 맞추므로 그대로 잡습니다.
역검증 — 변이를 넣어 빨개지는 것을 봤다
❗입력 선언이 같은 커밋에 있어야 한다 — 실측으로 알았다
처음엔 변이를 넣어도
BUILD SUCCESSFUL이었습니다. Gradle 이test를 UP-TO-DATE 로건너뜁니다 — 그 파일은 소스셋 밖이라 입력으로 선언해야 하고, 앞의 네 사본은 이미 선언돼
있었습니다(
build.gradle이 그 이유를 각각 적어 뒀습니다). 한 줄 더했습니다.그러자
CiServerFilterMirrorsGradleInputsTest가 빨개졌습니다.설계된 그대로입니다 — 정본에만 넣으면 그 파일만 고친 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 test799건 통과. web 파일은 변이 실험 후 원복해서 이 PR 에 없습니다.