Index
2026-09-02 — Analysis / Bugfix

v1 final restore family 채점 버그 수정 — 322행 오제외 해소

scene_bench | sbench_eval.facts — restore family (from/to nested schema) 채점 계약

TL;DR

322/322
dry-run gradable
107
find 회귀 테스트 통과
3
파일 변경 (+36/-13)

1 배경 / 목적

지금까지 v1 final 캠페인(4~7모델 × 7전략 격자, 릴리즈 델타 e987b40f 등)은 전부 find family만 실측했다 — run_v1_final_shards.sh/serve_eval_vllm.sh가 --families find로 하드코딩돼 있었고, 260827 채점 계약 확정(v1final_grading_contract 카드)도 find의 flat answers[i] = {surface, concealment, on_floor, ...} 스키마를 기준으로만 설계됐다.

restore family는 "물건을 원래 자리(to)에서 다른 자리(from)로 옮겼다가 되돌리는" 과제로, 답 스키마가 answers[i] = {from: {...}, to: {...}} 형태의 nested 구조다. 다음 델타/모델 격자 확장 단계에서 restore family를 처음 채점 대상에 넣으려는 시점에, 기존 채점 함수들이 이 nested 구조를 전혀 모른다는 사실이 드러났다.

발견된 문제: surface_unlabeled()와 query_labels()의 라벨 축이 a.get("surface") / a.get("concealment")를 answer 최상위에서 직접 읽는데, restore의 answer에는 그 키가 없다(전부 from/to 안에 nested). 결과적으로 모든 restore 행이 surface=None, concealment=None으로 읽혀 C(미라벨) 규칙에 걸려 322행 전부가 평가 세트에서 자동 제외되고 있었다 — 조용한 전량 데이터 손실이라 실행 전에는 드러나지 않는 종류의 버그.

2 작업 내용

find용으로 짜여 있던 채점 헬퍼들을 family 분기로 확장했다. 원칙은 "restore의 from/to 각 지점에 find와 동일한 계약을 개별 적용"이다.

파일: src/sbench_eval/facts.py (+43/-13, 순증감 36/-13) 1) _spot_unlabeled(spot) 신설 — find의 C 규칙(표면 라벨 없음 + 바닥 아님 + hidden+fixture 아님)을 restore 지점 하나(from 또는 to)에 적용하는 헬퍼로 분리 2) surface_unlabeled(query) — query["family"] == "restore"면 answers[i].from / answers[i].to 양쪽에 _spot_unlabeled 적용 (기존엔 find 전용 flat 체크만 존재) 3) _gt_spot(spot, room_map) — find와 동일 계약 명시: 바닥 → surface='floor'(방 미채점) hidden+fixture → fixture uid를 정답 표면으로 사용(엄격) 그 외 미라벨 → None — 이전엔 spot.get("surface")만 봐서 hidden+fixture 케이스를 find처럼 fixture로 대체하지 않고 있었음 4) _label_spots(query) 신설 + query_labels() 라벨 축 갱신 — concealment / n_hidden / n_floor / n_unlabeled 4개 층화 축을 find는 answers 그대로, restore는 from+to 양면 전개해서 계산 — 이전엔 이 4축도 flat 스키마만 읽어 restore 행 전부 concealment='open', n_hidden=0 등으로 오판정(층화 자체가 무의미) 파일: scripts/run_v1_final_shards.sh, scripts/serve_eval_vllm.sh (+2/-2 각각) — --families find 하드코딩 → --families "${V1_FAMILIES:-find}" — 기본 동작(find)은 그대로 유지, restore 채점을 켜려면 호출 시 V1_FAMILIES=restore 환경변수만 얹으면 됨 (inner_cmd 패턴 유지)

대안으로 restore 전용 별도 채점 함수를 새로 만드는 방법도 있었지만, find 계약을 그대로 재사용해 두 family 간 채점 기준을 강제로 동일하게 유지하는 쪽을 택했다 (계약 불일치가 다음 회귀의 근본 원인이었으므로, 계약을 하나로 좁히는 방향).

3 결과

항목수정 전수정 후비고
restore 322행 gradable 비율0% (전량 C 제외)100% (322/322)dry-run 실측
restore GT (room, surface) 쌍미검증 (애초 로직 미비)정합 확인from/to 양면 hidden+fixture strict 포함
find 채점 회귀 테스트107 (기준)107 통과find 채점 동작 변경 없음 확인
핵심 발견: 버그는 "채점 알고리즘이 틀렸다"가 아니라 "family별 답 스키마 차이를 채점 함수가 몰랐다" 종류 — find 계약 자체는 260827에 확정된 그대로 두고, restore 쪽 진입 경로 4곳(surface_unlabeled, _gt_spot, concealment, n_hidden/n_floor/n_unlabeled)만 family 분기를 넣어 해결했다.
아직 안 한 것: 이번 커밋은 dry-run 검증까지만 — 실제 모델 채점 잡(예: find 캠페인처럼 flash/sonnet/luna/오픈 VLM × 7전략 격자)은 이 커밋에 포함되지 않는다. V1_FAMILIES=restore로 첫 실측 잡을 돌리는 건 다음 단계.

4 Takeaway

의미

지금까지 v1 final 결과 테이블(4~7모델 격자, 릴리즈 델타)은 전부 find family 단일 트랙이었다 — restore family는 데이터셋에 존재했지만 채점 계약이 없어 사실상 미사용 상태였다. 이번 수정으로 restore가 find와 동일한 채점 계약(hidden strict, floor 처리, C 미라벨 제외)을 공유하게 되어, find-only였던 v1 final 캠페인을 restore family로 확장할 수 있는 선결 조건이 풀렸다. 322행이 "발견되자마자 곧바로 100% 사용 가능"해진 건, 버그가 채점 로직의 정확도 문제가 아니라 순수하게 스키마 라우팅 누락이었다는 뜻이기도 하다.

5 Next Steps

미해결 한계

restore family의 채점 계약을 확정하는 별도 문서(find의 260827 v1final_grading_contract에 해당하는 카드)가 아직 없다 — from/to 양면 hidden+fixture strict 처리가 find와 대칭적으로 맞는지는 이번 dry-run(GT 쌍 정합)까지만 확인됐고, 실제 모델 응답을 채점해본 적은 없어 응답 파싱/매칭 단계에서 추가 엣지케이스가 나올 가능성이 있다.

다음 실험

V1_FAMILIES=restore로 소규모 모델(예: gemini-3.6-flash) 대상 첫 restore 채점 잡을 돌려 실측치를 확보하고, find 캠페인과 동일한 비용/전략 구조(cap 8, 30s 청크, 공통 전문)를 그대로 재사용할 수 있는지 확인한다. 이후 find 격자에 이미 포함된 모델들(flash-3.6/3.7, sonnet, luna, 오픈 VLM 3종)로 restore family를 확장해 find+restore 통합 결과 테이블을 만드는 것이 다음 목표.