STARRY PASS 10 — 여섯 장의 점검표와 아직 찍지 않은 초록 도장
시험 주에는 새 기능보다 selector·증거·미실행 범위를 지키는 법
이 이야기는 원문의 D1∼D6 수행 계약을 이해하기 위한 모형극입니다. 공통 코어 주차이며 트랙 선택 시 증권을 기본으로 표시하지만, 증권 전용 기능을 구현하는 주차는 아닙니다. 현재 source audit은 통과했어도 smoke 실행은 CURRENT NOT RUN, testsExecutedByAudit=0이므로 초록 도장을 이미 얻었다고 서술하지 않습니다.
STARRY의 문을 닫을 시간이 가까워지자 키타가 반짝이는 새 간판 상자를 들고 들어왔다. 시험 기간에도 무대가 허전해 보이지 않게 기능을 하나만 더 붙이면 어떻겠느냐는 제안이었다. 니지카는 상자를 열어 보지도 않고 계산대 아래에 넣었다. 그 대신 얇은 모래시계와 여섯 장의 점검표를 탁자 위에 놓았다.
“이번 주에는 새 간판을 달지 않아.”
“고장 난 것도 아닌데요?” 키타가 물었다.
“고장 나지 않았다는 말을 짧게 확인하는 주야. 학교시험을 밀어내면서 기능을 늘리면, 프로젝트를 지킨 게 아니라 둘 다 놓친 거니까.”
료는 모래시계 옆의 숫자를 읽었다. 전체 활동 시간은 여섯 시간에서 여덟 시간, 오늘 점검은 사십오 분 상한이었다. 빨리 끝났다면 남은 시간을 채우지 않는다는 문장도 있었다. 히토리는 시간을 덜 쓰면 성의가 부족해 보이지 않겠느냐고 걱정했지만, 니지카는 이번 주의 성실함은 오래 앉아 있는 시간이 아니라 정한 범위를 넘지 않는 데 있다고 답했다.

탁자에는 월요일부터 토요일까지 여섯 칸이 그려진 지도가 펼쳐졌다. 첫 칸은 핵심 smoke, 둘째는 원자성 2분 답변, 셋째는 동시성 답변과 SQL Q19, 넷째는 멱등성 답변, 다섯째는 W13 실행계획 회수와 SQL Q20, 여섯째는 smoke 재확인과 evidence 링크 점검이었다. 지도 끝에는 굵은 선이 그어져 있었다. 오늘 이야기의 계약은 그 선을 넘지 않았다.
키타는 각 칸에 작은 종을 달려 했다. 니지카는 종 대신 상태 표식을 놓았다. source와 PDF의 범위를 맞춘 표식은 SOURCE AUDIT PASS, 실제 learner root에서 test를 돌린 표식은 비어 있는 CURRENT NOT RUN, 사람이 녹음 내용을 들어야 하는 칸은 MANUAL REVIEW REQUIRED였다. 세 표식은 서로 대신할 수 없었다.
“감사에 통과했으면 실행도 통과한 거 아닌가요?” 히토리가 물었다.
“악보의 모든 마디가 인쇄됐는지 확인한 것과 실제 공연이 끝까지 성공한 건 다른 일이야.” 니지카가 답했다. “이번 감사에서 실행한 test는 영 개야. 그러니 지금 우리가 가진 건 검사법을 정확히 읽은 결과지, 오늘의 Green 결과가 아니야.”
첫 번째 칸에서 료가 여섯 장의 점검표를 펼쳤다. 다섯 장에는 반 전체를 부르는 이름이 있었고, 마지막 한 장에는 반 이름 뒤에 특정 학생 이름까지 적혀 있었다. 니지카는 그것을 class selector와 method selector라고 설명했다.
첫 카드는 com.example.financialcore.account.api.AccountControllerTest, 둘째는 com.example.financialcore.transfer.TransferFailurePointIT, 셋째는 com.example.financialcore.transfer.SortedLockTransferIT였다. 넷째는 길어서 두 줄을 차지했다. com.example.financialcore.transfer.TransferIntegrationTest.same_key_with_different_semantic_request_conflicts_without_extra_effect. 다섯째는 com.example.financialcore.CoreSchemaIT, 여섯째는 com.example.financialcore.account.OpeningIntegrationTest였다.
“점검표 여섯 장이면 test도 여섯 개인가요?” 히토리가 물었다.
“아니. class를 부르면 그 class에서 발견된 test가 모두 대상이 될 수 있어. 우리가 감사한 staged reference 기준으로는 첫째 네 개, 둘째 두 개, 셋째 한 개, 넷째 한 method, 다섯째 한 개, 여섯째 한 개야.”
키타가 순서대로 4+2+1+1+1+1을 더해 십이라고 적었다. 여섯 historical Java source 전체에 선언된 @Test는 열여덟 개였지만, exact staged smoke envelope에 들어가는 것은 십 개였다. 그러나 니지카는 그 숫자 옆에 굵은 괄호를 쳤다.
“십은 staged source가 정확히 조립됐을 때의 역사적 계산이야. 오늘 실제 XML에서 십이 나왔다는 뜻이 아니야. 실행하지 않았고, runner도 source hash를 marker에 묶지 않아.”
여섯 카드의 출처도 한 선반에 있지 않았다. CoreSchemaIT와 OpeningIntegrationTest는 옛 학습 stage 안에만 있었고 기본 reference project의 활성 test source set에는 자동으로 들어오지 않았다. AccountControllerTest는 더 까다로웠다. 같은 전체 이름을 가진 현재 root 파일이 있었지만, W14 시점의 W6D7 staged source와 method와 assertion이 달랐다.
료가 카드에 이름이 같으니 같은 사람으로 치면 되지 않느냐고 하자, 니지카는 두 장의 신분증을 나란히 놓았다. 이름은 같아도 사진과 발급일과 내용이 다르면 같은 byte라고 할 수 없었다. selector 문자열은 찾아갈 주소일 뿐, 어느 stage source의 SHA가 compile되어 실행됐는지를 자동으로 증명하지 않았다.
“그럼 기본 선반에서 runner를 켜면 여섯 카드가 모두 정확히 돌아간다는 말도 못 하네요.” 키타가 말했다.
“맞아. stage-only 두 class를 learner root에 조립하고, 충돌하는 이름은 어느 source를 택했는지 고정하고, fresh XML과 source hash를 연결해야 그런 말을 할 수 있어.”
runner를 읽기 전에 네 사람은 여섯 historical card가 무엇을 보는지 빠르게 대조했다. CoreSchemaIT은 information_schema.tables에서 public schema의 BASE TABLE만 읽고 flyway_schema_history를 제외했다. 결과 목록은 account, business_tx, ledger_entry, idempotency_request 네 이름과 순서 없이 정확히 같아야 했다. 추가 table 하나나 누락 하나는 잡지만, column type·nullability·PK·FK·CHECK·index가 올바른지는 전혀 묻지 않았다.
“서랍 이름 네 개가 맞아도 서랍 안 칸막이가 틀릴 수 있다는 거네요.” 히토리가 말했다.
“그렇지. 이름 inventory를 전체 schema 검증이라고 부풀리면 안 돼.”
AccountControllerTest의 staged card에는 네 경로가 있었다. A-100 생성 요청은 201과 account 한 행을, 음수 opening은 400과 account 영 행을, owner 조회는 200과 일부 body 문자열을, 없는 account 조회는 404를 확인했다. MockMvc를 쓰므로 실제 network port를 연 배포 환경은 아니었고, body 일부가 들어 있다는 검사는 전체 JSON schema equality도 아니었다. 인증·권한과 audit 기록은 이 네 method의 assertion에 없었다.
키타는 카드 겉면에 ‘HTTP 성공’이라고 쓰려다가 ‘staged MockMvc 대표 경로’로 고쳤다. 짧은 표현일수록 관찰 층을 빠뜨리기 쉬웠다. status code, 일부 body, 특정 DB count를 확인했다는 사실을 그대로 남기면 충분했다.
TransferFailurePointIT와 TransferIntegrationTest는 같은 이체 이야기를 다루지만 smoke에서 맡은 장면이 달랐다. 전자는 class selector라 두 failure method를 고르고, 후자는 exact method selector라 아홉 method 중 amount-conflict 하나만 고른다. source 파일 전체를 읽으며 배운 내용과 runner가 이번에 실행하도록 요청한 내용을 같은 색으로 칠하지 않는 것이 첫 번째 점검의 핵심이었다.
히토리는 여섯 점검표 위에 ‘전체 회귀’라고 적힌 큰 표찰을 올렸다. 시험 주에 이만큼 중요한 것만 모았으니 사실상 전체라고 생각한 것이다. 료는 표찰을 뒤집어 targeted smoke라고 고쳐 썼다.
“이 여섯 장은 위험이 큰 길을 빠르게 보는 묶음이야. 선택하지 않은 test가 실패해도 이 실행에는 안 나타나.”
“토요일 설명에 전체라는 말이 있더라도요?”
“실제 owner source는 같은 여섯 --tests filter만 넘겨. 실행 범위의 이름은 source에 맞춰야 해.”
니지카는 큰 원 안에 전체 suite를 그리고, 그 안쪽에 작은 육각형으로 six-selector smoke를 그렸다. 작은 육각형이 초록이어도 원의 나머지가 초록이라는 결론은 나오지 않았다. 반대로 smoke가 빨리 깨졌다면 전체 suite까지 기다리지 않고 첫 failure를 기록할 수 있었다. 시험 주 budget에서 smoke가 필요한 이유는 넓은 확신을 주기 때문이 아니라, 좁은 경보를 빨리 울리기 때문이었다.
료는 Green 도장을 찍기 전에 runner의 아홉 줄을 읽었다. runner는 여섯 selector를 gradlew.bat test --no-daemon에 --tests로 차례로 넘겼다. native exit가 0이 아니면 즉시 멈췄다. 그 뒤 build/test-results/test 아래의 TEST-*.xml을 모두 읽고 각 suite의 class, tests, failures, errors, skipped를 모았다.
기대 class 집합과 실제 XML class 집합은 exact 비교였다. 하나가 빠지거나 예상 밖 class가 끼면 실패했다. 하지만 히토리가 test 최소 수 조건을 소리 내 읽다가 멈췄다. 벽의 공개 계약에는 tests>=6이라고 적혀 있었지만 source에는 if($tests-lt 5 ...)가 있었다.
“tests=5면 5-lt 5가 거짓이니까 통과하잖아요.”
“그래. 교재 문장은 여섯 이상을 요구하지만 현재 구현은 다섯도 받아들여. marker에 tests=5가 찍힐 수 있다는 뜻이야.”
료가 실제 staged envelope는 십인데 왜 여섯과 다섯을 따지느냐고 묻자, 니지카는 세 숫자의 역할을 다시 갈랐다. 십은 감사한 staged source에서 selector가 고르는 method 계산이고, 여섯은 공개 최소 계약이며, 다섯은 현재 runner가 실수로 허용하는 구현 하한이었다. 어느 숫자도 오늘 실행 결과가 아니었다.
tests>=6, 현재 source는 tests>=5를 허용해 test 수 5도 Green marker로 갈 수 있습니다.XML 검사대에는 다른 틈도 있었다. runner는 실행 전에 오래된 TEST-*.xml을 지우지 않았다. 예상 밖 class의 오래된 XML은 exact class 집합 비교에서 들킬 수 있지만, 기대 class 이름과 같은 오래된 XML은 합계에 섞일 가능성이 있었다. --rerun-tasks도 없었고, XML이나 marker에 source SHA와 실행 시각을 함께 기록하지 않았다.
마지막에는 Set-Content로 smoke.txt를 바로 썼다. 임시 파일을 완성한 뒤 원자 교체하는 방식이 아니었고, 실행 시작 전에 이전 smoke.txt를 지우지도 않았다. 새 실행이 중간에 실패했을 때 옛 파일이 남아 있을 수 있으므로, 파일 존재만으로 오늘 Green을 판단하면 안 됐다. caller의 exit code, fresh XML, source identity, marker 내용을 함께 묶어야 했다.

두 번째 칸에서는 네 사람이 원자성 답변을 이 분 안에 말하는 연습을 했다. 키타가 먼저 “@Transactional을 붙여서 모두 안전합니다”라고 말했다. 니지카는 초시계를 멈추고, annotation 이름 대신 실제로 본 경계를 말해 보라고 했다.
첫 번째 historical card인 OpeningIntegrationTest는 customer-1의 OPEN-100 계좌를 12_345로 개설한 뒤 반환 account ID를 받는다. 그 뒤 account 한 행, OPENING business transaction 한 행, 같은 account의 OPENING ledger 한 행을 서로 다른 COUNT(*) query로 세어 각각 1인지 확인한다. 그러나 amount와 balance column을 다시 읽지 않고, business transaction을 반환 account와 join하지 않으며, 세 SELECT가 하나의 transaction snapshot이라는 선언도 없다.
“method 이름에 Atomically가 있잖아요.” 히토리가 말했다.
“이름은 질문의 의도를 알려 주지만 assertion을 늘리지 않아. 이 test는 정상 호출 뒤 세 count를 봤어. 중간 실패를 넣어 rollback을 직접 재현한 test는 아니야.”
두 번째 원자성 card인 TransferFailurePointIT는 AFTER_CLAIM과 AFTER_BUSINESS 두 곳에서 RuntimeException을 주입한다. 각 실패 뒤 idempotency_request, TRANSFER business_tx, TRANSFER ledger의 수가 모두 0인지 본다. 하지만 두 account balance를 직접 assertion하지 않는다. audit row나 외부 시스템 effect, checked exception, process hard-kill도 이 두 test의 관찰 범위 밖이었다.
키타는 교재 답변 뼈대에 failure point 세 개라는 문장이 있는 것을 발견했다. 니지카는 그 숫자를 현재 smoke의 직접 증거에 붙이지 말라고 했다. 답변 과제는 이전 학습과 evidence를 회수하는 말하기 계약이고, exact smoke source가 직접 선택한 test 범위와 같지 않을 수 있었다.
녹음 파일 validator도 내용을 듣지 않았다. 파일이 존재하는지, 128 byte 이상인지, SHA-256이 무엇인지만 출력하고 MANUAL_REVIEW_REQUIRED라고 남겼다. 빈 파일을 거르는 기술 gate일 뿐, 위험·설계·실험·결과·한계가 정확히 들어갔는지는 사람이 들어야 했다.
료는 원자성 답변을 다섯 문장으로 다시 구성했다. 위험은 이체 중간 실패가 일부 행만 남기는 것, 설계 의도는 한 transaction 경계 안에서 effect를 묶는 것, 직접 test는 두 RuntimeException 지점에서 세 업무 표의 row count를 본다는 것, 관찰 결과는 source assertion이 기대하는 영 행, 한계는 account balance와 외부 effect를 이 card가 직접 보지 않는다는 것이었다.
니지카는 “변경 0”이라는 짧은 말을 그대로 허용하지 않았다. 무엇의 변경이 영인지 table 이름을 붙이게 했다. idempotency claim row, TRANSFER business transaction row, TRANSFER ledger row가 각각 영이라는 문장은 source와 연결되지만, 모든 database column과 외부 메시지가 영이라는 문장은 연결되지 않았다.
새 transaction으로 사후 조회했다는 면접 문장도 현재 staged source 본문에서 직접 확인되지 않으면 단정하지 않았다. annotation과 test 이름, 답변 template, 실제 assertion은 서로 다른 증거 층이었다. 이 구분을 한 뒤에야 히토리의 답변은 이 분보다 짧아졌고, 오히려 더 정확해졌다.
세 번째 칸에는 서로 반대 방향으로 달리는 스무 개의 작은 수레가 놓였다. 열 개는 A에서 B로, 열 개는 B에서 A로 향했다. SortedLockTransferIT는 열 쌍, 총 스무 task를 한 번 동시에 출발시키고 준비 latch를 최대 십 초, 각 future를 최대 삼십 초 기다렸다. 모든 task가 끝난 뒤 두 계좌 balance 합이 이만이고, transfer ledger의 signed amount 합이 영인지 확인했다.
히토리는 보고서 제목을 ‘deadlock 백 회 영 건’이라고 쓰려 했다. 료가 연필을 빼앗았다.
“우리가 본 source는 스무 task 한 번이야. test 이름에 FinishWithoutDeadlock이 있어도 모든 scheduling과 production load에서 deadlock이 불가능하다는 증명은 아니지.”
처리량, p95, retry 횟수도 재지 않았다. Future.get이 task exception을 밖으로 전달하므로 숨은 기술 실패를 성공으로 세지는 않지만, 한 fixture와 한 실행의 종료·보존 범위만 말할 수 있었다. 교재의 2분 답변에는 versionless lost update나 concurrent withdraw 20개의 성공 10·업무 거절 10·기술 실패 0 같은 이전 학습 수치도 등장했지만, 그것들은 run-w14-smoke.ps1에 추가 selector로 들어 있지 않았다.
동시성 답변을 마친 뒤 키타는 SQL Q19 문제 카드를 꺼냈다. reversal 영수증과 그 원거래 영수증을 한 줄에 보이라는 과제였다. 히토리는 reversal만 먼저 골라 INNER JOIN하면 된다고 생각했지만, 문제의 반례는 원거래가 없는 정상 transaction도 결과에서 잃지 않는 것이었다.
그들은 business_tx를 current_tx와 original_tx 두 별칭으로 놓고, current side의 모든 스물한 행을 왼쪽에 유지했다. current_tx.original_tx_id = original_tx.tx_id인 경우에만 원거래가 붙었다. fixture에서는 REQ-211과 REQ-212 두 reversal이 모두 REQ-201을 가리켰고, 나머지 열아홉 행은 NO_ORIGINAL이었다.
BROKEN_REFERENCE라는 분류 칸도 있었지만 현재 fixture의 FK가 없는 원거래 ID를 막으므로 예상 결과는 영 행이었다. 이 SQL은 W14 Ver2가 문제 계약과 격리 fixture를 바탕으로 만든 illustrative example이었다. packaged workbook이 제공한 canonical answer가 아니고, 어떤 reversal이 업무상 허용되는지를 판정하지도 않았다.
네 번째 칸에는 같은 접수번호가 적힌 두 장의 이체 신청서가 있었다. 첫 신청서의 amount는 천, 둘째는 이천이었다. TransferIntegrationTest source에는 아홉 개의 @Test가 있었지만 exact selector는 same_key_with_different_semantic_request_conflicts_without_extra_effect 한 method만 불렀다.
fixture는 두 계좌를 각각 만으로 시작했다. conflict-key와 amount 천인 첫 요청은 한 번의 이체 효과를 만들었다. 같은 key에 amount만 이천으로 바꾼 두 번째 요청은 semantic conflict가 되어 추가 효과를 만들지 않아야 했다. 최종 관찰값은 balance 구천과 만천, transfer business transaction 한 행, ledger 두 행, idempotency claim 한 행이었다.
“그럼 같은 key 스무 요청도 업무 효과 한 번이 Green이네요.” 히토리가 말했다.
“그 test는 같은 source 안에는 있지만 이 exact method selector가 실행하지 않아.” 니지카가 답했다. “JSON field 순서 replay, changed from, changed to, 반대 방향, rollback test도 마찬가지야. 파일 안에 있다는 사실과 이번 selector가 실행한다는 사실을 나눠.”
교재의 멱등성 답변에는 AtomicClaim50의 owner 1·existing 49와 same-key concurrent 20개의 업무 효과 1도 들어갔다. 그것들은 이전 evidence를 정확한 workload와 함께 회수하는 학습 문장이지 W14 smoke가 추가로 실행하는 selector가 아니었다. burst 100, stale PROCESSING recovery, 모든 semantic field conflict도 현재 selected method에서 일반화할 수 없었다.
다섯 번째 칸에서 료는 W13의 실행계획 봉투 세 개를 가져왔다. before-plan.txt, after-plan.txt, perf-manifest.json이었다. W14는 이 파일을 읽기 전용으로 다시 펼쳐 node, estimated/actual rows, buffers, sort, planning/execution time을 설명하는 날이었다. 같은 fixture와 query, parameter, machine이라는 주장 경계도 다시 말해야 했다.
“이번에 읽으면 W14 성능 개선 결과가 하나 더 생기나요?” 키타가 물었다.
“아니. predecessor evidence를 회수하는 거야. 파일이 존재하고 hash와 내용이 올바르다는 조건 아래 이전 결과를 설명할 뿐, 새 benchmark를 실행하거나 새 production claim을 만들지 않아.”
니지카는 특히 top node 이름 하나만 외우지 말라고 했다. plan tree의 node와 actual rows, buffers를 query shape과 데이터 분포에 연결해야 했다. Seq Scan은 많은 행을 읽는 선택도에서 합리적일 수 있었고, 한 parameter의 plan을 모든 account에 일반화하면 안 됐다. W13의 현재 증거 연속성과 계산 계약 문제도 W14 회수만으로 고쳐지지 않았다.
회수 worksheet에는 before_top_node, after_top_node, chosen_index, before_samples, after_samples, claim_boundary를 한 표에 놓는 예가 있었다. 목표 sample 표기는 전 삼십, 후 삼십이었다. 하지만 이 표는 원 파일을 다시 읽어 실제 값을 채울 때만 의미가 있었다. 예시 이름 idx_ledger_account_created_id나 기대 sample 수를 기억에서 복사하는 것은 current predecessor hash를 확인하는 일을 대신하지 못했다.
히토리는 “index를 썼으니 빨라졌다”라는 답을 지웠다. 대신 같은 query와 fixture에서 어떤 node가 actual rows와 buffers를 내보냈는지, median과 p95가 각각 어떻게 되었는지, hot account 같은 반례에서 왜 planner 선택이 달라질 수 있는지를 묻는 문장으로 바꿨다. W14가 하는 일은 새 숫자를 만드는 것이 아니라 기존 주장에 붙어 있던 조건을 회수하는 일이었다.
그 옆에는 SQL Q20 카드가 있었다. 업무 transaction 하나당 ledger가 몇 행인지 세는 문제였다. 히토리는 ledger_entry에서 시작해 group하면 간단하다고 생각했지만, 그러면 ledger가 영 개인 transaction은 아예 사라졌다. 그래서 business_tx 스물한 행을 왼쪽에 두고 ledger_entry를 LEFT JOIN해야 했다.
count에도 함정이 있었다. COUNT(*)는 ledger가 없어도 LEFT JOIN이 만든 NULL 확장행 하나를 셀 수 있었다. COUNT(ledger.entry_id)를 써야 실제 ledger 영 건을 영으로 유지할 수 있었다. fixture 교육 정책은 성공한 transfer라면 2, 다른 지원 success라면 1, 나머지는 0을 기대했고, REQ-302만 actual 1 대 expected 2로 어긋났다.
료는 cardinality가 맞으면 원장도 완전히 맞는 것 아니냐고 물었다. 키타가 ledger 두 장의 금액과 방향을 서로 바꾼 예를 그렸다. 잘못된 두 행도 수만 2면 cardinality_ok=true가 될 수 있었다. 이 illustrative SQL은 행 수 이상을 증명하지 않았고, 2·1·0 정책도 모든 금융 시스템의 보편 규칙이 아니었다.
여섯 번째 칸에서 네 사람은 다시 six-selector source를 읽고 evidence 연결표를 설계했다. 표의 행은 정확히 여섯 개였다. W9, W10, W11, W12의 day-7-green-gate.txt 네 경로, W13의 perf-manifest.json, W14의 smoke.txt였다. 각 행에는 relative path, byte 수, SHA-256을 같은 시점에 적어야 했다.
한 경로라도 없거나 너무 작으면 표를 쓰지 않았다. W14 smoke text에는 W14_SMOKE_GREEN, failures=0, errors=0, skipped=0 token이 있어야 했다. 임시 CSV를 만든 뒤 다시 읽어 행 수가 여섯이고 hash가 64자리인지 확인한 다음 목적지로 옮기는 것이 D6 링크 계약이었다.
그러나 키타가 빈 표에 미리 rows=6 broken=0 smoke=GREEN이라고 적자 니지카가 지웠다.
“그건 기대 marker야. 이번 source audit은 learner root smoke도, links writer도 실행하지 않았어. 계약을 정확히 설명할 수 있다는 것과 현재 파일 여섯 개가 같은 실행에 묶였다는 건 달라.”
여섯 행의 이름도 임의로 바꿀 수 없었다. evidence/w9/day-7-green-gate.txt, evidence/w10/day-7-green-gate.txt, evidence/w11/day-7-green-gate.txt, evidence/w12/day-7-green-gate.txt, evidence/w13/perf-manifest.json, evidence/w14/smoke.txt가 계약의 상대 경로였다. 경로가 존재하더라도 byte가 스무 미만이면 비어 있는 evidence로 처리했고, 저장한 CSV를 다시 읽었을 때 행 수가 여섯이 아니면 목적지로 옮기지 않았다.
그런데 이 D6 inline writer 역시 Ver2의 아홉 source teaching unit에 새 canonical file로 더해진 것은 아니었다. PDF가 제시한 workflow gate를 이해하는 장면이었다. 코드 뒤풀이에서 byte 단위로 다룬 W14 신규 owner는 scripts/run-w14-smoke.ps1 하나였고, 여섯 Java source는 historical reference, Q19와 Q20은 illustrative였다. workflow 계약과 source closure 수량을 섞지 않아야 “파일이 몇 개인가”라는 질문에도 같은 답을 할 수 있었다.
료는 links.csv가 여섯 hash를 담으면 full suite까지 증명하는지 물었다. 히토리가 먼저 고개를 저었다. 링크 표는 주어진 여섯 evidence path가 존재하고 그 byte를 다시 찾을 수 있게 할 뿐이었다. 각 predecessor claim이 올바른지, W14 smoke가 full suite인지, 오디오 내용이 좋은지는 각각의 producer와 검토 경계로 돌아가야 했다.

links.csv Green을 주장하지 않습니다.마지막으로 니지카는 초록 도장을 투명 상자에 넣고 잠갔다. 상자 바깥에는 세 줄만 남았다. SOURCE/PDF/STAGE CLOSURE PASS. TESTS EXECUTED BY AUDIT = 0. CURRENT SIX-SELECTOR SMOKE = NOT RUN.
히토리는 처음보다 빈 도장이 덜 불안해 보인다고 말했다. 비어 있다는 사실을 정확히 표시하면 과거 marker를 오늘 결과로 착각하지 않을 수 있었다. 반대로 기대 output을 미리 Green으로 색칠하면, 실행하지 않은 범위와 실패한 범위를 찾기 어려워졌다.
“이번 주에 지켜야 할 건 초록색이 아니었네요.” 키타가 말했다.
“응. 정확한 범위야.” 니지카가 여섯 점검표를 다시 묶었다. “class와 method, historical reference와 current root, source 감사와 실제 실행, targeted smoke와 full suite, artifact hash와 사람의 의미 검토. 그 경계를 지켜야 다음에 찍는 초록 도장이 진짜가 돼.”
투명 상자 안에는 실행 날짜, learner root, source manifest, fresh XML hash를 적을 빈 칸도 남겨 두었다. 다음 실행이 그 칸들을 실제 값으로 채우기 전까지, 도장은 깨끗한 면을 위로 둔 채 잠겨 있었다.