세션에게 「검사를 약하게 하지 마세요」라고 부탁하지 않는다. criteria.tsv 는 해시로 잠겨 있고, 고치면 채점자가 exit 77 로 죽는다.
바퀴 15
지형 타일 48 → 32px — 고정값이 바뀌었다(위 표). 캐릭터 48px 과 속도 240 px/s 는
2026-09-14
초록
문제코드에 48 이 12군데 박혀 있었다. 그중 하나(WorldCollide.HALF = 22)는 그냥 숫자를 바꾸는 것으로 끝나지 않았다 — 22 를 두면 몸 44px 이 통로 32px 보다 넓어져 1칸 통로에 끼는데, 단위 검사 스무 개 남짓이 전부 손 계산한 48 기준 기대값이라 어느 것도 안 빨개진다. 그리고 실측 게이트 measure_collide 의 미끄럼 구간(1.2초)은 세로로 203.7px 를 가는데 곧은 해안 7줄이 336px 에서 224px 로 줄어 판정이 무너졌다.
원인고정값 하나가 자(尺)이자 게이트의 기대값이자 검사의 손 계산이라는 세 역할을 동시에 하고 있었다. 자를 바꾸면 나머지 둘이 따라와야 하는데, 박힌 숫자는 안 따라온다. 게이트의 시간 상수(1.2초)는 한술 더 뜬다 — 픽셀이 아니라 초로 적혀 있어서 타일이 작아진 것과 상관없어 보이지만, 실제로 재는 것은 「몸이 해안 몇 줄을 지나가나」다.
고친 것48 을 PlayerMotion.TILE 한 군데만 숫자로 남겼다. WorldCollide.HALF 는 숫자가 아니라 식이다 — TILE * 0.5 - 2.0 (22 → 14). world_view.gd · main.gd · scenes/main.tscn(스폰 6168 → 4112) · measure_window · measure_camera · measure_collide · measure_move · test_project_settings · test_player_motion · test_player_scene · test_world_view · test_world_collide(손 계산 스무 개 전부 다시 계산) · redteam.sh 의 기대값. test_visible_tiles_at_48px 는 이름에서 48 을 떼고 30 × 16.875 를 강제한다 — 해상도와 타일 둘 중 아무거나 바뀌면 빨개진다.
바꾼 결정고정값에서 따라 나오는 값은 숫자로 적지 않는다. 단, 검사의 손 계산은 예외다 — test_world_collide.gd 의 T 는 일부러 PlayerMotion.TILE 에서 안 가져왔다. 가져오면 타일을 바꿨을 때 기대값 스무 개가 소리 없이 틀린 채 초록이 된다. 대신 「T 와 TILE 이 어긋났나」를 test_body_is_narrower_than_a_tile 이 잰다. 게이트를 화면과 같이 키운다: 칸 수가 273 → 558 로 늘면 「색 100개 이상」 같은 문턱은 가만두는 것이 곧 반으로 헐거워지는 것이다 (100 → 200 · MAX_TILES 400 → 700). - 잰 값 (check.sh all 한 판 · 씨앗 20260914 · 창 1920×1080 · 물리 60Hz): VIEW 타일 32px · 보이는 칸 30.00 x 16.88 · 배율 2.00x · MOVE 239.72 px/s = 7.491 칸/s(240 은 안 바뀌고 칸 표기만 5 → 7.5) · COLLIDE 바다 면 7360.00 · 멈춘 자리 7345.99 = 7360 - 14 - 0.01 · 세로 169.60 px/s · 몸 반폭 14 · CAMERA 스폰 (4112, 4112) · 최대 편차 0.0000 px · DRAW 그린 칸 527(최대 558) · 불일치 0 · 최대 색차 0.8/255 · 물 47.3% → 37.7% · TESTS 66 passed, 0 failed · PARSE 26개 실패 0 · ALL GREEN(기준 6개) · SHOT 960x540 · 색 370개 · 가장 넓은 한 색 0.9% · 한 화면 색 수 217 → 349개. 대조군 한 판 REDTEAM 31 잡음, 0 놓침 · 7분 40초 (바퀴 14 는 31개 · 7분 41초). 타일을 바꾼 뒤에도 31개가 그대로 잡힌다 — 그중 셋은 이 바퀴에 문턱을 만진 것들이다 (줌 15 x 8.44 · 통째로 그리기 558 → 65536 · 캐시 문턱). 첫 판은 30 잡음, 1 놓침 이었다 — 검사를 67개로 늘려서 바닥(66)이 딱 하나만큼 헐거워졌던 것이라, 접고 다시 돌렸다
남긴 것[ASK] 계약 기준 4 의 글자가 아직 「48px 타일」이다. 무장돼 있어 세션이
바퀴 14
창을 띄우는 게이트를 한 프로세스로 합친다 — measure_view 와 measure_draw 는
2026-09-14
초록
문제합치고 나니 따로 돌 때는 없던 구멍이 하나 생겼다 — 한 프로세스가 반쪽만 돌고 죽으면 check.sh 가 그걸 초록으로 본다. 창 게이트를 일부러 1초에 끊어 보니 ^VIEW 와 ^DRAW \[ 두 grep 이 둘 다 맞았다(VIEW 2줄 · DRAW [서서] 까지 찍고 죽는다). 이름도 한 번 걸렸다: 요약 줄을 WINDOW 로 찍었더니 main.gd 가 시작할 때 찍는 배너 WINDOW 1920 x 1080 과 같은 prefix 라 게이트가 침묵해도 grep 이 걸렸다
원인따로 돌 때는 게이트 하나 = 프로세스 하나라 죽으면 그 게이트가 통째로 침묵했고, 「아무것도 안 찍었다」 검사가 그걸 잡았다. 합치면 그 성질이 깨진다 — 앞 절반이 찍은 줄이 뒤 절반의 침묵을 가려 준다. 게이트를 합치는 값은 창 횟수고, 치르는 값이 이거다
고친 것tools/tests/measure_window.gd 를 새로 만들었다 — 메인 씬을 한 번 세우고 5프레임 뒤 VIEW 를 먼저 재고(DRAW 가 플레이어를 해안으로 101칸 순간이동시키므로 순서가 뒤집히면 VIEW 가 「걷다 만 화면」을 잰다), 이어서 DRAW 를 [서서]·[걷고] 두 번 잰다. 실패 수는 따로 센다 — VIEW ok · DRAW ok 두 줄이 예전 그대로 나온다. 끝에 WINGATE ok (VIEW 0 · DRAW 0 · 창 한 번) 한 줄을 더 찍고 tools/loop/check.sh 가 세 줄을 다 본다. tools/tests/measure_view.gd · measure_draw.gd 와 짝 .uid 는 지웠다. 딸린 참조(measure_camera.gd · test_world_view.gd · redteam.sh 주석 · docs/GOTCHAS.md)도 새 이름으로 고쳤다
바꾼 결정게이트를 합칠 때는 「둘 다 돌았다」를 찍는 줄을 같이 만든다. 합치기는 공짜가 아니다 — 프로세스 경계가 하던 일(죽으면 침묵한다)을 줄 하나로 대신 사야 한다
남긴 것뺏김이 0 이 되지는 않는다. 계약 한 판에 창은 아직 2번 뜬다 — 남은 하나는
잰 값(씨앗 20260914 · 창 1920x1080 · 논리 960x540 · 줌 1 · macOS 25.6 · 표본 간격 100ms) check.sh tests 한 판 표본 102 · 뺏김 9(바퀴 13: 110 · 14) · 계약 한 판 166 · 12 (183 · 13) · 둘 다 시작 앞 = 끝 앞 iTerm2 · 창 여는 횟수 tests 2 → 1 · 계약 3 → 2 · 게이트 자체 VIEW 0.58s + DRAW 1.25s = 1.83s → 1.29s · 계약 한 판 21.86s → 20.28s · ALL GREEN(기준 6개) · TESTS 66 passed, 0 failed · PARSE 26개 스크립트, 실패 0. 잰 값은 글자 하나 안 달라졌다: VIEW 논리 960x540 · 창 1920x1080 · 배율 2.00x · 보이는 칸 20.00 x 11.25 · DRAW [서서] 표본 7844 · 불일치 0 · 물 45.9% · 그린 칸 273 · DRAW [걷고 144 px] 표본 7844 · 불일치 0 · 물 31.0%. 대조군 7/7 (게이트 단위 · 손 안 댄 판은 초록): 줌 → 보이는 칸 10.00 x 5.62 · 창 축소 → 창 1600x900 · 배율 1.00x · 안 그림 → 7844/7844 · 다른 씨앗 → 7844/7844 · 통째로 → 65536 칸 · 캐시 안 버림 → [서서] 부터 7844/7844 · 두 칸 문턱 → [걷고] 만 6982/7844. 창 축소 대조군이 VIEW FAIL 2개 인데 DRAW ok 로 끝난다 — 앞 절반이 빨개도 뒤 절반이 끝까지 돈다는 증거다. 잘린 판 대조군(1초에 끊기)은 WINGATE FAIL 게이트가 끝까지 못 갔다 로 잡혔다 — 그 줄이 없었으면 초록이었다 계약 단위 한 판도 커밋 뒤에 돌렸다(바퀴 11 이 남긴 숙제다): REDTEAM 31 잡음, 0 놓침 · 7분 41초 — 바퀴 10 의 30개 · 10분 03초 · 1 놓침에서 줄었다. 그 한 판의 창은 93번 → 62번이다
바퀴 13
창을 띄우는 게이트가 포커스를 안 뺏는다 — measure_view · measure_draw ·
2026-09-14
초록
문제막을 수 없다. 항목이 시킨 WINDOW_FLAG_NO_FOCUS + 화면 밖 위치를 포함해 6가지를 재서 전부 뺏겼다. 창을 띄우는 순간 Godot 이 맨 앞 앱이 된다
원인no_focus 는 창이 키 윈도우가 되는 것만 막는다 — 앱의 활성화는 못 막는다. 창 생성 시점에 걸어도(project.godot) 그대로였고, LSUIElement 래퍼 번들도 LaunchServices 의 open -g 도 엔진의 활성화 호출을 못 이겼다. 화면 밖 위치는 macOS 가 화면 안으로 물린다(요청 5000,5000 → 실제 1536,1154 = 화면-창). CLI 로 설정을 덮는 길(--display/window/size/no_focus=true)은 조용히 무시된다 — 인자 목록에는 남는데 설정값은 false 였다
고친 것새 도구 tools/loop/focus.sh — lsappinfo 로 맨 앞 앱을 계속 샘플해 표본 수 · 뺏긴 표본 수 · 앞뒤의 맨 앞 앱을 찍는다(접근성 권한이 필요 없다). 못 막으니 되돌려 준다: tools/loop/godot.sh 가 NARU_FOCUS_RESTORE=1 일 때 띄우기 직전의 맨 앞 앱을 기억했다가 끝나고 open 으로 돌려보낸다. 창을 띄우는 두 곳에만 걸었다 — tools/loop/check.sh 의 VIEW · DRAW 와 tools/loop/shot.sh. docs/NUMBERS.md 11절 · docs/GOTCHAS.md macOS 3줄
바꾼 결정막을 수 없는 것은 되돌리거나 횟수를 줄인다. 그리고 project.godot 에 no_focus 를 안 남겼다 — 켜두면 아무 이득 없이 사람이 직접 띄운 창에서 WASD 가 안 먹는다. 게이트 편의가 게임 동작을 바꾸면 안 된다
남긴 것뺏기는 것 자체는 그대로다 — 창 두 번이 도는 ~1.8초 동안 타이핑은 여전히
잰 값대조군 헤드리스 표본 6 · 뺏김 0 / 창 표본 7 · 뺏김 4 — 도구는 창을 본다. 막기 6종 전부 실패 — ① 실행 중 플래그 4/7 ② 창 생성 시점 no_focus 4/7 ③ 화면 밖 4/7 ④ LSUIElement 3/7 ⑤ open -g -n 5/13 ⑥ 최소화 18/23 + 프레임버퍼가 색 1개(죽는다. 손 안 댄 같은 프레임은 색 202개). 되돌리기 — 끔 Finder → SystemUIServer(안 돌아옴) · 켬 3/3 Finder → Finder · 헤드리스에 켜도 오발 없음. check.sh tests 한 판 표본 110 · 뺏김 14 · 시작 앞 = 끝 앞 · 계약 한 판(창 3번) 표본 183 · 뺏김 13 · 시작 앞 = 끝 앞 · TESTS 66 passed, 0 failed · VIEW 배율 2.00x · 보이는 칸 20.00 x 11.25 · DRAW [서서]/[걷고 144 px] 표본 7844 · 불일치 0 · SHOT 960x540 색 217개 · 한 색 1.3% · ALL GREEN(기준 6개 · 21.86s). 새 게임 코드는 없다 — 도구만 건드린 바퀴다
바퀴 12
계약이 check.sh tests 를 두 번 돈다 — 기준 3(mintests.sh)과 기준 4 가
2026-09-14
초록
문제세션이 일지를 안 적었다 — 이 절은 사람이 커밋과 실측으로 복원한 것이다. 원래 문제: 계약 기준 3(mintests.sh)과 기준 4 가 같은 실측 7종을 각각 돌았다. 한 판이 두 배로 길고 흔들릴 기회도 두 배였고, 창이 한 판에 4번 떠서 일하는 사람의 포커스를 뺏었다.
원인기준 3 은 「개수가 줄지 않는다」만 물으면 되는데 mintests.sh 가 check.sh tests 를 불렀다. tests 는 단위 검사 뒤에 실측 게이트 7종을 달고 있다 — 물어본 적 없는 것까지 같이 딸려 왔다.
고친 것tools/loop/check.sh 에 unit 단계를 새로 만들고 mintests.sh 가 그걸 부른다. 무장된 .loop/criteria.tsv 는 안 건드렸다 — 세션의 red line 이다. 계약 글자는 그대로 두고 그 글자가 부르는 스크립트 쪽에서 갈랐다. 실측이 깨지면 기준 4 가 여전히 빨개지므로 계약이 잡는 범위는 그대로다. (커밋 2bb1686)
바꾼 결정없음
남긴 것일지 게이트가 처음으로 진짜 일을 했다. 세션이 일을 하고 커밋까지 했는데 BACKLOG 체크와 일지를 둘 다 빼먹었고, 드라이버가 초록인데도 멈췄다 — 안 멈췄으면 「무슨 일이 있었는지 아무도 모르는 초록 바퀴」가 그대로 푸시됐을 것이다. 다만 왜 빼먹었는지는 모른다. 5분 · $1.55 로 가장 짧고 싼 바퀴였다 — 짧은 바퀴가 마무리를 건너뛰는 경향이 있는지 몇 판 더 보고 판단한다.
잰 값계약 한 판 32.81s → 22.0s · 창 뜨는 횟수 4번 → 2번 · ALL GREEN(기준 6개) · TESTS 66 passed, 0 failed · PARSE 27개 실패 0
바퀴 11
measure_facing.gd 가 사람의 마우스에 안 흔들리게 한다 — 진짜 커서를 뺏어 재는
2026-09-14
초록
문제게이트가 Input.warp_mouse 로 진짜 커서를 뺏어 쟀다. 사람이(또는 아무 프로세스가) 마우스를 건드리면 튄다 — 바퀴 8 · 사람 세션 · 바퀴 9 · 바퀴 10 네 번. 재현을 못 해서 네 바퀴 동안 「사람 탓」으로만 적고 넘겼다.
원인루트 뷰포트의 get_mouse_position() 은 매번 OS 커서를 되묻는다. 그래서 warp 해 놓은 값이 사람의 손에 언제든 덮인다 — 게이트가 기계 전체와 커서 하나를 놓고 경합하고 있었다.
고친 것tools/tests/measure_facing.gd — 메인 씬을 루트가 아니라 제 SubViewport 에 세우고 합성 InputEventMouseMotion 을 push_input 으로 밀어 넣는다. SubViewport 는 밀어 넣은 이벤트만 본다(warp_mouse 를 해도 안 흔들린다 — 쟀다). 창이 필요 없어져 tools/loop/check.sh 가 --headless 로 부른다. tools/loop/redteam.sh 에 FACE 대조군 ④ 캔버스 변환을 넣었다(30 → 31개).
바꾼 결정재현할 수 없는 흔들림은 방해를 직접 만들어서 재현한다. 두 번째 Godot 을 띄워 매 프레임 커서를 휘젓는 방해 프로세스를 썼더니 네 바퀴짜리 흔들림이 한 판에 재현됐다(옛 게이트 FAIL 13개 · 7구간 전부). 「사람 탓」은 원인이 아니라 아직 안 겨눈 조건이었다. 그리고 그 방해가 숨어 있던 둘째 버그를 끌어냈다 — 게이트는 facing 을 정하는 _physics_process 를 기다리면서 유휴 프레임을 세고 있었다. 부하가 걸리면 유휴 4프레임이 물리 한 틱도 못 품는다. Engine.get_physics_frames() 로 물리 틱을 센다(SETTLE_TICKS = 2).
남긴 것redteam.sh 한 판은 여전히 10분이다 — 줄어든 것은 「그동안 마우스를 못
잰 값방해를 건 채 — 옛 게이트 FAIL 13개 · 7구간 전부(커서 각 -155.5°·-137.3°· -158.2°, 기대 0°·90°·180°), 새 게이트 헤드리스 7구간 전부 ok · 오차 0 (0.0°·90.0°·180.0°·-90.0°·46.0°·60.0°) · 창 띄운 채 15/15 초록. 둘째 버그: 마우스 안 건드리고 부하만 — 유휴 프레임 셀 때 12회 중 1회 빨강 (커서 0.0° 인데 방향 (-1,0), 기대 (1,0)) → 물리 틱 셀 때 15회 중 0회. FACE 대조군 4종 전부 잡음 — ① 커서 안 읽음 FAIL 4개 · ② 히스테리시스 건너뜀 FAIL 1개 · ③ 코 안 돎 FAIL 4개 · ④ 캔버스 변환 빼먹음 FAIL 6개(새로 넣었다). TESTS 66 passed, 0 failed · PARSE 27개 실패 0 · IMPORT ok · ALL GREEN(기준 6개 · 32.81s). 새 게임 코드는 없다 — 게이트만 건드린 바퀴다.
사람
가짜 회귀로 멈췄다 · FACE 게이트가 사람 마우스에 다섯 번째
2026-09-14
초록
문제바퀴 10 이 초록으로 일하고 커밋까지 했는데 드라이버가 「회귀 — 기준 3」 으로 멈췄다. 그런데 채점은 ALL GREEN 이었고, 남은 증거는 TESTS 66 passed, 0 failed 인데 빨강 이라는 모순이었다.
원인셋이 겹쳤다. ① 항목의 verify 가 redteam.sh 였다. 그게 계약을 30번 부르면서 .loop/results.json 을 자기 마지막 판으로 덮었고, 회귀 감지가 「계약이 깬 것」이 아니라 「대조군이 일부러 깬 것」을 보고 멈췄다. ② mintests.sh 의 grep 이 ^TESTS 만 남겨서 실측 게이트가 실패한 줄을 버렸다 — 그래서 증거가 모순으로 보였다. ③ 진짜 빨강은 FACE 였다 — measure_facing.gd 가 진짜 커서를 뺏어 재는데 사람이 그때 마우스를 쓰고 있었다.
고친 것드라이버가 채점 직후의 results.json 을 따로 떠 두고 그것과 회귀를 비교한다. verify 뒤에는 증거를 채점 시점으로 되돌린다 — 일지의 「채점」과 대시보드가 대조군의 빨강을 물려받으면 안 된다. mintests.sh 는 빨갈 때 거른 줄을 도로 보여준다 — 고치자마자 다음 판에서 FACE FAIL 커서 각 60°-놓음 — 잰 값 61.28° · 기대 60.00° 를 한 줄로 잡아냈다. 그리고 measure_facing.gd 항목을 P1 맨 앞으로 올렸다.
바꾼 결정FACE 를 먼저 고친다. 매 바퀴 계약이 이 게이트를 돌기 때문에, 안 고치면 사람이 자기 컴퓨터를 쓰는 것만으로 아무 바퀴나 랜덤하게 빨개진다. 「사람이 마우스를 안 만지면 된다」는 해결이 아니다.
남긴 것기준 3 과 4 가 같은 check.sh tests 를 각각 돈다. 실측 게이트가 7종이라 한 판이 두 배로 길고 흔들릴 기회도 두 배다 — 백로그에 올렸다. 그리고 이번 판이 다시 알려준 것: 증거를 거르는 검사는 빨개져도 쓸모가 적다. mintests.sh 를 고치자마자 다섯 번 못 찾던 원인이 한 줄로 나왔다.
잰 값대조군 두 판 — REDTEAM 29 잡음, 1 놓침 · 28 잡음, 2 놓침. 놓친 판 전부 FACE FAIL (-1.43°·92.54°·61.28°, 기대 0°·90°·60°). 손 안 댄 check.sh tests 는 exit 0 · TESTS 66 passed, 0 failed · DRAW 표본 7844 · 불일치 0 · CAMERA 최대 편차 0.0000 px. 누적 다섯 번 — 바퀴 8 · 사람 세션 · 바퀴 9 · 대조군 두 판.
바퀴 10
measure_draw.gd 의 걷는 구간에 대조군을 붙이거나 그 구간을 뺀다
2026-09-14
초록
문제바퀴 9 가 넘긴 그대로다. 게이트는 [서서]·[걷고] 두 번 재는데 걷는 구간이 혼자 잡는 것이 하나도 없었다 — 대조군 4종이 전부 [서서] 에서 먼저 빨개진다. 혼자 잡는 게 없으면 그 구간은 게이트가 아니라 장식이고, 언젠가 「느리다」는 이유로 잘린다.
원인게이트가 플레이어를 해안까지 순간이동시킨다(스폰 128칸 → 해안 229칸 = 101칸). 그 점프 자체가 보이는 범위를 통째로 갈아치워서 어떤 캐시 버그든 [서서] 에서 이미 터진다. 바퀴 9 는 「캐시를 망가뜨리는 한 줄」만 찾았지 「순간이동은 멀쩡히 넘기고 한 칸씩 걸을 때만 망가지는 한 줄」을 못 찾았다 — 필요한 것은 버그가 아니라 문턱이었다.
고친 것tools/loop/redteam.sh 에 대조군 ⑤ 를 넣었다 — main.gd 의 캐시 조건을 r != _cache_range 에서 「두 칸 넘게 움직였을 때만 다시 채운다」로 바꾼다. 순간이동(101칸)은 문턱을 넘으니 멀쩡히 채우고, 걸을 때는 한 칸씩 움직여서 캐시가 한 칸 뒤처진 채로 끝난다. tools/tests/measure_draw.gd 머리말의 「걷는 구간이 혼자 잡는 것은 아직 하나도 없다」도 지웠다 — 이제 거짓이다.
바꾼 결정대조군은 「기능을 망가뜨리는 것」만이 아니다. 게이트의 어떤 구간이 무엇을 잡는지 물으려면, 망가지는 *조건*을 겨눠야 한다. 여기서 겨눈 것은 캐시가 아니라 캐시를 버리는 문턱이고, 그래서 순간이동과 걷기를 갈라낼 수 있었다. - 잰 값 (씨앗 20260914 · 창 1920x1080 · 논리 960x540 · 줌 1 · 60Hz): 대조군 ⑤ 는 [서서] 표본 7844 · 불일치 0 · 최대 색차 0.8/255 · 채운 횟수 2 (손 안 댄 상태와 글자 하나 안 다르다) · [걷고 144 px] 불일치 6982 / 7844 · 최대 색차 104.0/255 · 채운 횟수 5 → 3 · 첫 어긋남은 칸 (216,122) 71884b(기대 647b3e). 단위 검사는 TESTS 66 passed, 0 failed 로 전부 초록. 계약 ALL GREEN(기준 6개 · 33.65s). 전체 REDTEAM 29 잡음, 1 놓침(10분 03초 · 대조군 30개).
남긴 것놓친 하나는 또 FACE 다 — 그것도 아무것도 안 깨뜨린 첫 칸
바퀴 9
월드를 화면에 그린다 — 보이는 칸만. main.gd 의 임시 격자를 걷어낸다
2026-09-14
초록
문제두 가지다. ① 한 칸의 색이 6.71 µs 였다 — 잡음을 두 번 도는데(tile_at 이 한 번, height_at 이 한 번) 273칸이면 1.83 ms · 60Hz 한 프레임 예산의 11% 다. 나무도 몹도 UI 도 아직 없는데 그걸 프레임마다 쓸 수는 없었다. ② 캐시를 넣고 나서 새 게이트의 걷는 구간이 대조군을 하나도 못 잡는 것을 발견했다 — 대조군 4종이 전부 앞 구간([서서])에서 이미 빨개졌다.
원인① 색을 정직하게 만들려다 잡음을 두 번 돌았다. h > SEA_LEVEL 을 색 쪽에서 다시 쓰면 싸지지만, 그러면 판정이 두 벌이 되어 언젠가 한쪽만 고쳐지고 「보이는 땅에 못 서는」 날이 온다. 그래서 비용을 받아들이고 캐시로 갚기로 했다. ② 게이트가 해안까지 순간이동시키는데, 그 순간이동 자체가 카메라를 옮겨 캐시를 버리게 만든다. 즉 캐시가 상했는지는 걷기 전에 이미 드러난다. 「걷고 나서 또 잰다」는 그럴듯한 이유가 있었지 증거가 있었던 게 아니다.
고친 것scripts/world_view.gd 를 새로 만들었다 — 「어느 칸을 그리나」 + 「그 칸이 무슨 색인가」 둘뿐인 순수 계산이다. 물·땅 분기는 WorldGen.tile_at 이 한다 (충돌이 묻는 것과 글자 그대로 같은 함수). scripts/main.gd 의 _draw 가 visible_world_rect() 범위만 그리고, 보이는 범위가 바뀔 때만 색을 다시 채운다. scripts/world_gen.gd 의 _unit 을 unit 으로 열었다 — 흔들기가 같은 해시를 쓴다. 새 실측 게이트 tools/tests/measure_draw.gd 를 check.sh tests 의 CAMERA 뒤에 넣었다.
바꾼 결정화면은 이제 증거다. 전에는 「그려 봤더니 그럴듯하더라」였는데, 이제 구운 픽셀 7844 점을 그 자리의 월드 칸 색과 맞춰 본다 — 틀리면 어느 픽셀이 어느 칸에서 무슨 색이었는지가 찍힌다. 그리고 칸마다 밝기를 흔드는 것은 꾸밈이 아니라 게이트다: 안 흔들면 스폰 한 화면이 거의 단색이라 shot.sh --max-flat 이 아무것도 안 잡는다.
남긴 것놓친 하나는 게이트가 아니라 사람의 마우스다 — 「원복하면 초록」에서
잰 값TESTS 66 passed, 0 failed(57 → 66) · PARSE 27개 실패 0 · IMPORT ok · ALL GREEN(기준 5개). 실측(씨앗 20260914 · 창 1920x1080 · 논리 960x540 · 줌 1 · 60Hz): DRAW [서서] 표본 7844 · 불일치 0 · 최대 색차 0.8/255 · 물 45.9% · 땅 54.1% · 그린 칸 273 · 채운 횟수 2 · DRAW [걷고 144 px] ... 물 31.0% · 땅 69.0% · 채운 횟수 5 · 해안 칸 (229,128) · 보이는 칸 273 / 월드 65536 = 240배 · 한 칸 6.71 µs · 273칸 1.83 ms · SHOT 960x540 색 217개 · 가장 넓은 한 색 1.3%(임시 격자는 3개 · 50.0%). 대조군 4종(안 그림 / 다른 씨앗 / 통째로 그림 / 캐시 안 버림)은 넷 다 단위 검사 66개를 초록으로 남긴 채 실측만 잡았다 — 75.4/255 · 121.3/255 · 픽셀은 전부 일치하고 그린 칸만 273 → 65536 · 86.0/255. 전체 REDTEAM 28 잡음, 1 놓침.
사람
대조군 한 판이 빨갰는데 재현이 안 됐다
2026-09-14
초록
문제P1 을 다 돌린 뒤 redteam.sh 를 돌렸더니 REDTEAM 24 잡음, 1 놓침 이 나왔다. 놓친 것이 하필 마지막 「원복하면 다시 초록이다」(기대 exit 0 · 잰 값 1) 였다 — 대조군이 헐거운 게 아니라 원복 뒤에도 빨갛다는 뜻이라 훨씬 나쁜 신호였다. 그런데 바로 잰 워킹트리는 깨끗했고 계약은 ALL GREEN 이었다.
원인모른다. 재현이 안 됐다. expect() 가 계약 출력을 >/dev/null 2>&1 로 통째로 버려서 무엇이 왜 빨갰는지 증거가 하나도 안 남았다. 그 사이 내가 계약을 다시 돌려 .loop/results.json 까지 덮었다. 실측 게이트(MOVE·VIEW·FACE·CAMERA·COLLIDE)는 진짜 창을 띄우므로 Godot 을 25번 연달아 띄운 끝에 한 번 흔들렸을 수 있지만 증거가 없다.
고친 것redteam.sh 의 expect() 가 놓친 판의 증거를 남긴다 — 빨간 기준과 증거를 그 자리에서 찍고, 계약 출력 · results.json · 워킹트리 상태를 .loop/redteam/<번호>-* 로 떨군다. 다음에 같은 일이 나면 10분을 다시 태우지 않아도 된다.
바꾼 결정없음
남긴 것원인은 바퀴 9 가 찾았다 — 게이트가 아니라 사람의 마우스다. measure_facing.gd 는 진짜 커서를 뺏어 재므로 사람이 그 사이 마우스를 건드리면 FACE 가 기대 90° 에서 91.73° 로 튄다. 나는 그때 이 기계를 쓰고 있었다. 같은 자리(「원복하면 초록」)에서 바퀴 8 · 이 판 · 바퀴 9 — 세 번째다. 세 번째가 오면 고치기로 했으므로 BACKLOG P1 에 올렸다. 그리고 이 한 판이 알려준 게 하나 있다: 증거를 안 남기는 검사는 빨개져도 쓸모가 적다. expect() 말고도 출력을 버리는 자리가 더 있는지 볼 만하다.
잰 값재현 시도 — check.sh tests 3회 연속 TESTS 57 passed, 0 failed · MOVE 가로 239.14 / 239.48 / 239.46 대각 240.00 / 238.72 / 239.88 (기대 240 ±5) · CAMERA 최대 편차 0.0000 px (허용 0.50) · COLLIDE 해안 미끄럼 세로 171.11 / 169.61 / 169.56 px/s · FACE 46°-붙잡음 / 60°-놓음 셋 다 동일. 흔들리는 값은 못 찾았다. 다시 돌린 대조군 — REDTEAM 25 잡음, 0 놓침.
바퀴 8
카메라가 플레이어를 따라간다. 줌 없음 (해상도가 시야 이득이 되면 안 된다)
2026-09-14
초록
문제카메라를 붙이자 커서 게이트(FACE)가 통째로 무너질 뻔했다. measure_facing.gd 는 「월드 좌표 × 최종 배율 = 창 좌표」로 진짜 커서를 옮긴다. 플레이어가 월드 (6168, 6168) 로 가면 그 곱은 창 밖 12336 px 이라 커서가 아예 안 간다. 그리고 바퀴 4 에 넣어 둔 줌 대조군이 아무것도 안 잡는 가짜가 됐다.
원인둘 다 「화면 좌표 = 월드 좌표」라는 죽은 전제 위에 서 있었다. 카메라가 그 둘을 갈랐다. 대조군 쪽은 한 겹 더 있다 — Godot 의 Camera2D 는 먼저 트리에 들어온 쪽이 화면을 잡는다. 진짜 카메라가 생긴 뒤로는 main.tscn 에 카메라를 덧붙여도 그냥 무시된다.
고친 것scenes/player.tscn 에 Camera2D(줌 1 · 부드럽게 따라가기 끔) — 코드가 매 프레임 따라 붙이지 않는다. 부모-자식이라 공짜로 따라가고 한 틱 뒤처질 자리가 없다. scripts/main.gd 에서 tile_offset 과 screen_of() 를 지우고 visible_world_rect() 를 뒀다 (임시 격자를 월드에 고정으로 바꿨다 — 화면에 고정돼 있으면 카메라가 따라가는지 사람 눈에 안 보인다). scripts/world_collide.gd 의 이동량 인자도 뺐다. tools/tests/measure_facing.gd 는 이제 캔버스 변환으로 화면에 찍고 나서 커서를 옮긴다. 새 게이트 tools/tests/measure_camera.gd + 단위 검사 tools/tests/test_camera.gd(5개). tools/loop/redteam.sh 의 줌 대조군은 진짜 카메라의 줌을 직접 걸도록 겨눌 곳을 옮겼다.
바꾼 결정화면 칸 = 월드 칸. 바퀴 7 의 tile_offset 은 0 이 되는 게 아니라 사라졌다. 그리고 카메라 따라가기는 코드가 아니라 씬 구조로 한다 (NUMBERS 8절).
남긴 것놓친 하나는 여전히 테스트 바닥이다(검사 57 · mintests 바닥 53) — 무장된
잰 값CAMERA 최대 편차 0.0000 px · 걸은 거리 484.00 px · 줌 1.00x · 보이는 칸 20.00 x 11.25 · 표본 294(씨앗 20260914 · 헤드리스 · 60Hz · 4구간 × 0.5s) · 스폰 월드 (6168,6168) → 화면 (480,270) · TESTS 57 passed, 0 failed(53 → 57) · VIEW 배율 2.00x · 카메라 1.00x · COLLIDE 바다 면 11040.00 → 멈춘 자리 11017.99 = 11040 - 22 - 0.01 · 세로 169.70 px/s(좌표계가 옮겨간 값) · SHOT 960x540 색 4개 가장 넓은 한 색 50.0% · ALL GREEN(기준 5개) · 대조군 REDTEAM 24 잡음, 1 놓침 — 새 3종은 단위 57개를 전부 초록으로 남긴 채 실측만 잡았다: 카메라 끔 8363.56 px · 실행 중 줌 보이는 칸 10.00 x 5.62 · 부드럽게 끌려옴 82.10 px
바퀴 7
이동 충돌 — 바다에 못 들어가고, 해안에 비스듬히 붙으면 미끄러진다
2026-09-14
초록
문제충돌을 어느 좌표계에 붙일지가 막혔다. 섬의 스폰은 월드 한가운데(128,128)인데 플레이어는 화면 원점 근처(504,264 = 화면 칸 10,5)에 있다. 그대로 월드를 꽂으면 시작 칸이 테두리 바다라 플레이어가 첫 프레임부터 얼어붙는다. 그렇다고 플레이어를 스폰 칸으로 옮기면 화면(960×540) 밖으로 나가 오렌지 네모가 사라진다 — 카메라는 다음 항목이고, 검사(test_player_scene.gd)가 「시작 위치가 화면 안」을 지킨다. 중간에 measure_facing 이 빨개져서 한참 내 코드를 의심했는데 HEAD 에서도 빨갰다.
원인카메라가 없는 동안은 화면 좌표계와 월드 좌표계가 같다고 가정할 수 없다. 백로그가 충돌을 카메라보다 앞에 둔 이상 이 어긋남은 이번 바퀴가 떠안아야 하는 것이었다. measure_facing 쪽은 내 변경이 아니라 사람의 마우스였다 — 진짜 커서를 뺏는 게이트라 손이 닿으면 커서 각부터 어긋난다. 마우스에서 손을 떼니 세 번 연속 초록이었다.
고친 것scripts/world_collide.gd 를 새로 만들었다 — 격자에 대고 축을 따로 푸는 순수 계산(move·overlaps·solid_from_seed). player.gd 는 move_and_slide 를 버리고 이걸 부른다. main.gd 에 씨앗(20260914)과 화면 칸 → 월드 칸 이동량 tile_offset(118,123) 을 두고 플레이어에 solid Callable 을 꽂는다 — 카메라가 오면 0 이 되어 사라지는 값이다. 검사 18개(test_world_collide.gd), 실측 게이트 tools/tests/measure_collide.gd, check.sh tests 의 COLLIDE 단계, 대조군 3종. GOTCHAS 에 두 줄(_ready 타이밍 · 마우스).
바꾼 결정이동을 물리 엔진에 안 맡긴다. 65536칸에 정적 몸을 세울 수 없고, 창 주변만 세우면 결과가 「지금 무엇이 세워져 있나」에 달린다. 같은 입력이 같은 자리를 줘야 한다(GDD D-1 서버 재현성 · C-4 원정 보고 검증). 미끄러짐은 축을 따로 푸는 데서 공짜로 나온다.
남긴 것놓친 둘은 게이트가 아니라 환경과 바닥이다 — 「검사를 지우면 바닥이 잡는다」
잰 값씨앗 20260914 · 헤드리스 · 물리 60 Hz · 몸 반폭 22 · 틈 0.01 — TESTS 53 passed, 0 failed(35 → 53) · PARSE 21개 실패 0 · IMPORT ok · MOVE 가로 239.07 / 대각 239.99 px/s · VIEW 보이는 칸 20.00 x 11.25 · COLLIDE 해안 월드칸 (229,122) · 바다 면 5376.00 px · 바다로 직진 x 5353.99(= 5376 - 22 - 0.01) · 세로 0.00 px/s · 해안 미끄럼 x 5353.99 · 세로 169.64 px/s(기대 240/√2 = 169.71 ±5) · 질의 100000회 317.8 ms (1회 3.178 µs · 막힘 67.7%) · ALL GREEN(기준 5개) · REDTEAM 20 잡음, 2 놓침 — 새 대조군 3종은 전부 잡았고, 앞의 둘은 단위 검사 53개를 초록으로 남긴 채 COLLIDE 만 잡았다.
바퀴 6
시드 기반 월드 생성 256×256, 땅/바다 2종, 같은 시드 = 같은 월드
2026-09-14
초록
문제첫 구현이 섬이 아니라 동그란 덩어리였다. 숫자는 전부 맞았다 — 테두리는 물, 땅 33~37%, 체크섬도 재현됐다. 그런데 PNG 를 구워 보니 씨앗을 바꿔도 거의 같은 원이었다. 「같은 시드 = 같은 월드」는 통과하는데 「다른 시드 = 다른 월드」가 눈으로는 거짓이었다.
원인잡음의 진폭이 감쇠보다 작았다. fBm 4옥타브는 평균 0.5 언저리에 몰려 있어서 폭이 ±0.15 쯤인데, 가장자리 감쇠는 0 → 1.25 로 훑는다. 해안선의 위치를 잡음이 아니라 거리가 정한다 — 그래서 반지름이 고정된 원이 나온다.
고친 것scripts/world_gen.gd 에 CONTRAST 2.5 를 넣어 잡음을 평균에서 벌렸다 ((n-0.5)*2.5+0.5 를 0..1 로 자른다). 덩어리 크기도 BASE_CELLS 6.0 → 3.5 로 키웠다. 부등식 두 줄은 그대로 산다 — 자른 값이 여전히 0..1 이라 테두리 물 · 한가운데 땅이 깨지지 않는다. 그리고 이걸 보려고 tools/qa/world_png.gd(월드를 PNG 로 굽는다)를 만들었다.
바꾼 결정월드 난수는 RandomNumberGenerator 를 안 쓴다. 그건 「몇 번째로 뽑았나」에 값이 달려 있어서, 나중에 청크를 따로 만들거나 타일 하나만 다시 물으면 월드가 달라진다. 좌표를 넣으면 값이 나오는 해시로 갔다 — 순서가 없으니 부분 생성도 같은 답을 준다 (test_tile_query_matches_the_whole_map 이 이걸 지킨다). 그리고 월드를 화면에 그리는 일은 카메라 뒤로 미뤘다 — 섬이 (128,128) 에 있는데 시점은 원점이라, 지금 그리면 화면이 통째로 바다다. 백로그에 한 줄로 넘겼다.
남긴 것대조군 1종이 여전히 헐겁다 — 「검사를 지우면 바닥이 잡는다」가 또 놓쳤다.
잰 값TESTS 35 passed, 0 failed (26 → 35) · PARSE 19개 실패 0 · IMPORT ok · WORLD 256x256 · 씨앗 1 땅 38.07% 0x22a64c62 / 42 땅 39.01% 0x4d412265 / -20260914 땅 28.31% 0xb5650262 · 한 장 190 ms(3장 569 ms · 헤드리스) · 씨앗 12개 땅 비율 28.00% ~ 40.97% · MOVE 가로 239.71 / 대각 238.72 px/s · VIEW 보이는 칸 20.00 x 11.25 · FACE 구간 7 ok · ALL GREEN(기준 5개) · REDTEAM 18 잡음, 1 놓침
바퀴 5
마우스가 방향을 정한다 — 이동 방향과 별개. 4방향 스냅 + 히스테리시스
2026-09-14
초록
문제실측 게이트에 가짜 커서를 넣을 방법이 없었다. 헤드리스에서는 창이 없고, Input.parse_input_event(InputEventMouseMotion) 을 던져도 뷰포트의 커서가 안 움직여서 get_global_mouse_position() 이 계속 (0,0) 을 준다 — 방향을 재는 게이트가 통째로 무의미해진다.
원인InputEventMouseMotion 은 액션·입력 핸들러로만 흐르고 커서 위치를 안 바꾼다. 위치를 쥔 것은 DisplayServer 라 Input.warp_mouse() 만 그걸 옮긴다. 그리고 warp 는 창 좌표를 받는다 — 월드 좌표를 그대로 넣으면 배율 2.00x 만큼 어긋난 곳을 겨눈다.
고친 것scripts/player_facing.gd 를 새로 만들었다(순수 계산 · class_name PlayerFacing · 스냅 반각 45° · 히스테리시스 10° · 데드존 8px). scripts/player.gd 가 매 물리 프레임 aim_at(get_global_mouse_position()) 을 부르고, scenes/player.tscn 의 코 네모(16x16)를 중심에서 28px 떨어진 자리로 옮긴다. 단위 검사 8개(tools/tests/test_player_facing.gd) · 실측 게이트 tools/tests/measure_facing.gd — --headless 없이 창을 띄우고 Input.warp_mouse(월드 * 최종배율) 로 진짜 커서를 옮겨 잰다. tools/loop/check.sh 가 VIEW 뒤에 부른다. 알게 된 것 둘은 GOTCHAS.md 에 넣었다.
바꾼 결정몸의 방향과 얼굴의 방향을 분리했다. 이동은 8방향(정규화)인데 바라보는 방향은 4방향이다 — 스프라이트가 앞/뒤/옆뿐이라 대각선을 그릴 그림이 없다. 그래서 PlayerMotion 과 PlayerFacing 을 서로 안 보는 두 클래스로 뒀다 (GDD D-2c). - 잰 값 (창 1920x1080 · 배율 2.00x · 겨눔 거리 200px · 물리 60Hz): FACE 오른쪽 0.0° → (1,0) 코 (28,0) · 아래 90.0° → (0,1) 코 (0,28) · 왼쪽 -179.7° → (-1,0) · 위 -90.0° → (0,-1) · 46° → (1,0) 붙잡음 · 60° → (0,1) 놓음 (구간 7 전부 ok). TESTS 26 passed, 0 failed(18 → 26) · PARSE 15개 실패 0 · IMPORT ok · MOVE 가로 239.63 / 대각 238.78 px/s · VIEW 보이는 칸 20.00 x 11.25 · ALL GREEN(기준 5개). 화면: SHOT 960x540 · 색 4개 · 가장 넓은 한 색 50.0% — 코 네모가 몸 오른쪽에 붙어 보인다.
남긴 것① REDTEAM 15 잡음, 1 놓침. 새 대조군 3종(커서를 안 읽음 / 히스테리시스를
사람
바퀴 4 가 드러낸 것 — 대시보드가 굳고, 푸시가 없고, 일지 형식이 깨졌다
2026-09-14
초록
문제바퀴 4 는 초록으로 끝났는데 라이브 페이지는 09:38 것 그대로였다. ① 드라이버가 커밋만 하고 푸시를 안 했다 — 대시보드가 GitHub Pages 인데. ② 로컬에서 구운 페이지는 「루프 돌고 있다」로 굳어 있었다. ③ 세션이 절 끝에 --- 를 덧붙여서 드라이버가 찍는 줄이 절 밖으로 떨어졌고, 찍힌 커밋도 항목을 만든 eebde9b 가 아니라 그 뒤 부기 커밋 83ad8df 이었다.
원인②는 report.py 가 .loop/loop.pid 가 살아 있나 봤는데, 드라이버가 report.py 를 부르는 시점엔 드라이버 자신이 살아 있다. 늘 참이고, 바로 뒤에 루프가 끝나도 페이지는 그대로다. 「지금 돌고 있나」는 구운 스냅샷이 알 수 없는 것인데 알 수 있는 척했다. ③은 stamp 가 절 끝에 이어 붙였기 때문이고, 커밋 쪽은 finish_journal 이 roll_state 뒤에 돌아서 그 사이 부기 커밋이 HEAD 를 차지했다.
고친 것타일을 마지막 바퀴 (번호 · 결과 · 날짜) 로 바꿨다 — 잰 것만 적는다. push_state() 신설 — 계약 · 항목 verify · 일지를 다 통과한 뒤에만 부른다. stamp 는 마지막 - 무엇: 줄 바로 뒤에 끼우고, 커밋은 head_after 를 받는다. 그리고 report.py 가 실패하면 루프가 멈춘다 — 바깥에서 볼 수 있는 유일한 창이라 조용히 실패하면 페이지가 낡은 채로 며칠을 간다. 실제로 푸터 문구를 고치다 문법이 깨져 깨진 report.py 가 그대로 푸시됐고, 아무것도 빨개지지 않았다.
바꾼 결정초록으로 닫힌 바퀴만 바깥으로 나간다 (사람). 빨간 상태는 푸시하지 않는다. 끄려면 PUSH=0. 푸시 실패는 루프를 죽이지 않는다 — 네트워크는 이 루프의 판정 대상이 아니다.
남긴 것정적 페이지가 「지금」을 말하려 들면 반드시 굳는다. 다음에 상태 타일을 늘릴 때 같은 함정을 조심한다. 그리고 report.py 는 이제 게이트지만 대조군이 없다 — 일부러 깨뜨려서 루프가 진짜 멈추는지 redteam.sh 에 넣을 자리가 남았다.
잰 값REDTEAM 13 잡음, 0 놓침 · ALL GREEN (기준 5) · 라이브 페이지 http 200 · Pages 반영에 20~40초
바퀴 4
논리 960×540 · 정수 2배 · 보이는 칸 20 × 11.25
2026-09-14
초록
문제항목이 이미 끝나 있었다. test_project_settings.gd 가 960×540·1920×1080· integer·20×11.25 를 전부 강제하고 있어서 손댈 것이 없었다 — 그런데 그게 재는 것은 project.godot 의 글자뿐이다. 바퀴 3 에서 이미 같은 모양의 구멍을 봤다 (단위 검사 18개가 전부 초록인데 노드가 속도를 반으로 줄여도 안 잡혔다).
원인설정 값을 읽는 검사는 실행 중에 값이 덮이는 것을 못 본다. 카메라 줌 한 줄, window_set_size 한 줄이면 눈에 보이는 칸이 달라지는데 ProjectSettings.get_setting() 은 여전히 960 을 준다.
고친 것실측 게이트 tools/tests/measure_view.gd 를 만들었다 — 실제 창을 띄워 메인 씬을 5프레임 돌리고 논리 화면·창·배율·카메라 배율·보이는 칸을 잰다. 배율은 나눗셈이 아니라 root.get_final_transform(), 보이는 칸은 get_canvas_transform() 으로 월드 좌표로 환산해서 잰다. tools/loop/check.sh 의 tests 단계가 MOVE 뒤에 --headless 없이 부른다. 대조군 2종을 tools/loop/redteam.sh 에 박았고 docs/NUMBERS.md 1절· docs/GOTCHAS.md 에 잰 값과 함정을 적었다.
바꾼 결정없음. 다만 카메라 줌 = 1 을 지금부터 게이트가 강제한다 — BACKLOG P1 의 「줌 없음」이 나중 항목이 아니라 이미 잠겼다.
남긴 것헤드리스로는 배율을 못 잰다(창 크기가 (0,0)). 그래서 계약의 기준 4 가 GUI 세션을 필요로 한다 — 화면 없는 기계에서 루프를 돌리려면 이 게이트를 분리해야 한다. 사람이 볼 것: 20×11.25 칸이 실제로 답답하지 않은지는 숫자가 아니라 눈이 정한다 (바퀴 3 의 screencapture 권한 문제가 그대로 남아 있다).
잰 값VIEW 논리 960x540 · 창 1920x1080 · 배율 2.00x · 카메라 1.00x · 타일 48px · 보이는 칸 20.00 x 11.25 (드라이버 macOS · 5프레임 뒤) · TESTS 18 passed, 0 failed · MOVE 가로 238.41 px/s · 대각 238.35 px/s (실이동 240.00 px) · PARSE 12개 실패 0 · ALL GREEN (기준 5개). 대조군: 카메라 zoom=(2,2) → 보이는 칸 10.00 x 5.62 · 실행 중 창 1600×900 → 배율 1.00x. 둘 다 단위 검사 18개는 전부 초록으로 남았다 — 새 게이트만 잡았다. 전체 대조군: REDTEAM 13 잡음, 0 놓침. 바퀴 3 이 놓친 「검사를 지우면 테스트 바닥이 잡는다」도 이제 잡는다 — 사람이 mintests 바닥을 8 → 18 로 올려 놨기 때문이다.
사람
문서를 docs/ 로 · 바퀴 일지 · GitHub Pages 대시보드
2026-09-14
초록
문제바깥에서 루프가 어떤 상태인지 볼 방법이 없었다. .loop/results.json · spend.txt · STOPPED 은 전부 .gitignore 라 GitHub 에서 안 보이고, md 가 루트에 흩어져 있어 나중에 다시 볼 때 어느 파일이 어느 층인지 헷갈렸다. 그리고 일지가 없었다 — .loop/state.md 는 만든 것 위주라 「무엇이 막혔고 왜 그랬나」가 바퀴마다 새면서 사라졌다.
원인기록의 독자를 나눈 적이 없다. state.md 는 다음 바퀴의 세션이 읽는 것이고, 사람이 몇 주 뒤에 읽을 기록은 따로 필요한데 같은 파일에 섞여 있었다. 그런데 state.md 는 롤링되므로 사람이 읽을 것까지 같이 잘려 나갔다.
고친 것md 를 전부 docs/ 로 옮겼다 (CLAUDE.md 만 루트 — Claude Code 가 루트만 자동 로드한다). docs/JOURNAL.md 신설 — 세션은 읽지 않고 끝에 절만 덧붙인다. tools/loop/journal.sh 가 next/check/stamp 를 맡고, tools/loop/report.py 가 docs/index.html 을 굽는다. 드라이버가 매 바퀴 끝에 찍고 굽고 커밋한다.
바꾼 결정일지의 줄마다 주인을 갈랐다. 세션이 문제·원인·고친 것·바꾼 결정·잰 값·남긴 것 을, 드라이버가 날짜·결과·채점·비용·커밋 을 쓴다. 채점 칸에는 results.json — 채점자가 쓴 것 — 만 들어가므로 세션의 「됐습니다」가 결과가 되지 않는다. 그리고 빨간 바퀴에도 일지는 남긴다 — git reset --hard 전에 빼뒀다가 도로 넣는다. 왜 빨갰는지가 제일 비싼 기록인데 지금까지는 되돌리기에 같이 쓸려 나갔다. 세션이 사람 판단 줄을 비워 두면 초록이어도 멈춘다 (journal.sh check).
남긴 것dry-run 이 버그 둘을 잡았다 — cat PROMPT.md 가 이동을 못 따라가 지시서가 통째로 빠졌고(81줄), dry-run 은 finish_journal 을 건너뛰어 일지 게이트가 항상 빨갰다. 둘 다 고쳤다. 경로를 옮길 때 --dry-run 을 먼저 돌린다 를 규칙으로 삼을 만하다. 대시보드는 값을 페이지에 구워 넣는다 — 굽지 않으면 낡는다. 사람이 손으로 볼 땐 tools/loop/ctl.sh report.
잰 값REDTEAM 11 잡음, 0 놓침 · ALL GREEN (기준 5) · TESTS 18 passed, 0 failed · MOVE 가로 238.55 px/s · 대각 239.93 px/s (둘 다 240.00 px) · DOCLEN CLAUDE.md 41/45 · docs/PROMPT.md 60/70 · .loop/state.md 59/90 · 조립된 프롬프트 122 → 141줄 (일지 지시 19줄. 바퀴마다 늘지 않는 고정분이다)
사람
문서 구조 세분화 (매 바퀴 읽는 비용을 고정으로)
2026-09-13
초록
문제매 바퀴 읽는 문서가 바퀴마다 비싸졌다. .loop/state.md 가 3바퀴에 59줄인데 이 기울기면 34바퀴 뒤 ~600줄 ≈ 24,000 토큰이 된다. 1판을 죽인 것이 이 기울기였다. 그리고 중복이 하나 — 드라이버가 prompt.txt 에 PROMPT.md 를 통째로 넣는데 그 PROMPT.md 가 다시 "PROMPT.md 를 봐라" 라고 시켜서 두 번 들어갔다.
원인문서를 「층」으로 나누지 않고 전부 매 바퀴 읽게 뒀다. 고정 비용이 아니라 기울기가 문제인데, 절대량만 보고 있어서 안 보였다.
고친 것문서를 고정 / 주입 / 조건부 / 롤링 네 층으로 갈랐다. GOTCHAS.md 를 신설해 함정을 NUMBERS.md 에서 떼냈고(에러 났을 때만 grep), PROMPT.md 맨 위에 "드라이버가 넣어준다, 다시 열지 마라" 를 박아 중복을 없앴다. 얇음을 게이트로 만들었다 — tools/loop/doclen.sh 를 계약 기준 5로 올려서 세션이 문서를 부풀리면 그 자리에서 빨개진다.
바꾼 결정.loop/state.md 는 세션이 열지 않는다. 드라이버가 최근 2바퀴만 조립해 넣고, 3바퀴가 넘으면 .loop/archive/ 로 뺀다.
남긴 것없음
잰 값고정 문서 5,019 → 3,281 토큰 · 34바퀴 뒤 state.md ~24,000 → ~2,000 토큰으로 고정 · REDTEAM 11 잡음, 0 놓침 · 게이트가 만들자마자 PROMPT.md 72줄 > 상한 70 을 잡아서 57줄로 줄였다
사람
8방향 확정 · 화면 QA 도구
2026-09-13
초록
문제바퀴 3 이 「4방향 이동」 항목을 하면서 대각선을 정규화해 사실상 8방향을 만들었고, 이건 사람이 판정할 것이라 세션이 스스로 확정할 수 없었다. 그리고 화면을 눈으로 못 봤다 — screencapture 가 macOS 화면 기록 권한에 막힌다(could not create image from display).
원인WASD 두 개를 동시에 누를 수 있는 이상 대각선은 생긴다. 막느냐 정규화하느냐는 게임 감각의 문제라 검사로 정할 수 없다. 캡처 쪽은 무인 루프가 권한 대화상자를 넘길 방법이 없다.
고친 것tools/qa/shot.gd + tools/loop/shot.sh 로 Godot 자신의 프레임버퍼를 PNG 로 굽는다. 권한이 필요 없고, 화면에 보이는 것이 아니라 게임이 그린 것을 읽으므로 창이 겹쳐도 상관없다.
바꾼 결정8방향으로 확정 (사람). 코어 키퍼(GDD A-4)도 8방향이고, 속력은 어느 방향이든 240 이다.
남긴 것「저장됨」은 증거가 아니라서 크기·색 수·가장 넓은 한 색 비율을 같이 찍게 했다. 그림이 맞는지는 여전히 사람 몫이다.
잰 값SHOT 960x540 색 3개 가장 넓은 한 색 50.0% · 헤드리스로는 텍스처가 비어 출력 없음(3b절) · 대조군 상한 40% → 빈 씬(50%) 잡힘 / 없는 씬 → SHOT ERROR
바퀴 3
P1-1 색 네모 플레이어 · 48px 타일 · 240 px/s · WASD
2026-09-13
초록
문제단위 검사 18개가 전부 초록인데 게임이 틀린 경우가 실제로 나왔다. 대조군으로 노드가 속도를 절반으로 만들게 했더니 검사는 하나도 안 빨개졌다. 그리고 대조군 자동화가 REDTEAM 9 잡음, 1 놓침 을 냈다 — 「검사를 지우면 테스트 바닥이 잡는다」가 놓쳤다.
원인순수 계산(PlayerMotion)만 검사하면 노드가 그 값을 실제로 쓰는지는 아무도 안 본다. 놓침 쪽은 검사가 8 → 18 로 늘었는데 계약의 바닥은 아직 mintests.sh 8 이라, 하나 지워 17 이 돼도 바닥 위였다. 가짜 게이트가 된 건 아니고 헐거워졌다.
고친 것실측 게이트 tools/tests/measure_move.gd 를 만들어 check.sh tests 가 단위 검사 뒤에 부르게 했다 — 실제로 1초 걸어서 이동 거리를 잰다. 바닥 문제는 드라이버의 bump_mintests (loop.sh) 가 초록 뒤에 8 → 18 로 올리고 다시 무장한다. 세션은 .loop/criteria.tsv 에 손대지 않았다 (red line).
바꾼 결정대각선 정규화로 사실상 8방향이 됐다 — 사람이 확정할 때까지 되돌릴 수 있게 적어 뒀다.
남긴 것화면을 눈으로 못 봤다 — screencapture 권한 문제. 다음 사람 세션에서 해결. P1-2 는 test_project_settings.gd 가 이미 강제 중이라 거의 끝나 있다 — 확인부터 한다.
잰 값TESTS 18 passed, 0 failed (바닥 8) · PARSE 9개 실패 0 · IMPORT ok · ALL GREEN · 실측(헤드리스 · 물리 60Hz · 1초) 가로 238.97 px/s · 4.979칸/s 대각 239.91 px/s · 4.998칸/s (둘 다 실제 이동 240.00 px, 기대 240 ±5) · 대조군 정규화 제거 → 339.41 잡음 · 속도 절반 → 단위 18개 전부 초록, 실측만 잡음(119.36)
바퀴 2
P0-3 계약 배선 (증거를 기계가 쓴다)
2026-09-13
초록
문제results.json 의 증거가 한 줄로 뭉쳐 있었다. 그리고 「공허한 계약」 게이트가 진짜로 도는 코드인지 확인할 수 없었다 — 무장 중에는 변조(77)가 먼저 걸려서 78 을 못 본다.
원인BSD tr 은 \x1f 를 못 읽는다. 구분자가 문자 x 가 되어 증거 줄이 안 갈렸다. macOS 의 tr 은 8진수만 받는다.
고친 것구분자를 \037 로 바꿨다. 공허 게이트는 무장을 풀고 다시 재서 죽은 코드가 아님을 확인했다. 그리고 contract.md 를 따로 두지 않고 정지 규칙·red lines 를 PROMPT.md 에 합쳤다 — 매 바퀴 읽는 문서 수를 하나라도 줄이는 쪽이 낫다.
바꾼 결정판정은 세션이 아니라 run-contract.sh 가 한다. 이것만이 results.json 을 쓴다.
남긴 것P0-4 대조군을 스크립트로 (redteam.sh) → 그다음 드라이버
잰 값ALL GREEN (기준 3개) · 대조군 5종 정상 0 / 기준 실패 1 / 변조 77 / 공허 78 / 파일 없음 78
바퀴 1
P0-1 · P0-2 Godot 뼈대 + 헤드리스 러너 + 프로젝트 로컬 격리
2026-09-13
초록
문제격리했다고 믿었는데 안 돼 있었다. XDG_DATA_HOME 등을 설정해두고 넘어갈 뻔했다.
원인**macOS Godot 은 XDG_* 를 무시한다. ~/Library/Application Support/Godot 에 그대로 쓴다. 재보니 홈의 Godot 디렉터리 mtime 이 실행 시각으로 갱신되고 있었다. 설정해두고 「됐다」고 믿는 것이 정확히 가짜 게이트다.**
고친 것tools/loop/godot.sh 가 HOME 자체를 프로젝트 안으로 돌린다. 그리고 격리 자체를 검사로 박았다 (tools/tests/test_isolation.gd) — godot.sh 를 우회한 실행은 그 자리에서 빨개진다.
바꾼 결정Godot 의 입구는 godot.sh 하나다. 다른 데서 직접 부르지 않는다. 그리고 check.sh 는 import → parse → tests 순서다 — class_name 전역 클래스가 임포트가 만드는 캐시에 들어가서, 순서를 바꾸면 멀쩡한 코드가 빨개진다.
남긴 것두 항목을 한 바퀴에 넣었다 — P0-1 의 verify 가 check.sh all 이라 P0-2 없이는 검증 자체가 성립하지 않는다. 「한 바퀴 한 항목」에서 벗어난 것이라 적어 둔다.
잰 값IMPORT ok · PARSE 5개 실패 0 · TESTS 8 passed, 0 failed · 실제 창 VIEWPORT 960x540 · WINDOW 1920x1080 · SCALE 2.00x · 대조군 5종 전부 빨개짐 → 원복 후 초록