writing-as-the-person-who-did-it 을 서브에이전트 셋으로 나눠 56편에 적용했다. 56편 중 20편만 고쳤다 — 나머지 36편은 SSOT 를 절 단위로 대조했을 때 옮길 흔적이 이미 옮겨져 있었거나 없었다. 없는 목소리를 채우지 않는다. 옮긴 것은 전부 SSOT 의 어느 절에서 왔는지 댈 수 있다. §3.4 「눈으로 찾을 일이 아니었다」— 세 계약을 파싱해 뽑은 이유 §4.3 구현하지 않기로 한 것과 빠뜨린 것은 다르다 §8.5 표의 마지막 줄을 더할 때 이 목록을 또 빠뜨렸다 §9.4 「왜 주제 링크가 탐색으로 가지?」— 우회를 남겨 두면 계속 나온 질문 §11.3 「세 버튼」을 실제 이름으로 되돌리고 두 언어가 섞인 것을 그 자리에 §12.2 막지 않은 대신 메모리에 남긴 것 §13.1 여섯 벌 인용이 어느 커밋이 짚은 말인지 §13.3 고쳐 쓴 첫 안이 거절당한 것과 사용자가 고른 말 두 쌍 §14.3 「문서가 그대로 나온다」는 구조 차이가 아니라 내용 양의 차이라는 정정 §16.1 삭제가 막힌 실제 기록 이름과 그것을 막은 프로젝트 링크 §16.9 주제 논지와 축 결론이 AI 가 써서 DB 에 직접 넣은 미검토 초안이라는 것 §17.5 바운딩 박스로 잘못 지목한 대상이 「판단 기준」이었다는 것 검증 환경의 커밋 해시 하나가 틀려 있었다(ca1cfa2 → ca1fc92). SSOT §13.1 과 부록 A 가 적은 값이고 저장소에 그 커밋이 있다. 검사 넷 전부 통과한다 — check_prose 56편 error 0 · check_body PASS · check_evidence --repo 문제 없음 · verify-tech-log-tree 프로젝트 5 error 0 warn 0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
5.8 KiB
kind, slug, title, topic, topicName, project, status, sourceRevision, source
| kind | slug | title | topic | topicName | project | status | sourceRevision | source | |||
|---|---|---|---|---|---|---|---|---|---|---|---|
| CASE | five-screens-were-quietly-empty | 계약에 선언만 있고 구현이 없어 화면 다섯 곳이 비어 있었다 | declared-but-not-implemented | 계약에 선언만 있고 구현이 없다 | TechLog | 게시 전 | tech-log@2026-09-02 |
|
계약에 선언만 있고 구현이 없어 화면 다섯 곳이 비어 있었다
홈의 「지금 집중하는 것」 영역은 화면에 나타난 적이 없었고, 프로젝트는 공개할 방법이 없었고, 어떤 기록도 다른 기록을 연결 대상으로 고를 수 없었다. 계약에는 그 연산들이 전부 선언돼 있었다. 서버에 구현이 없었고, 프론트는 계약을 믿고 불렀고, 화면은 404 를 「데이터가 없음」으로 그렸다.
관계
- 타입에는 보이는데 부를 수 없는 연산이 네 번 나왔다 같은 시기에 난 다른 부류의 누락이다. 이쪽은 서버에 구현이 없었고 그쪽은 프론트가 등록을 빠뜨렸다.
- 계약과 구현은 서버와 화면 양쪽에서 전수 대조한다 이 사건 뒤에 세운 기준이다.
- 「이 프로젝트에 열린 질문이 없습니다」 — 실제로는 넷이 있었다 같은 404 를 화면이 어떻게 그렸는지가 그 기록에 있다.
문제
계약이 「이 연산이 있다」고 말하면 프론트는 그것을 부른다. 서버에 그 컨트롤러가 없으면 404 가 돌아오고, 화면은 그 404 를 빈 목록으로 그린다.
빈 목록과 「아직 안 쓴 글」은 화면에서 같아 보인다. 그래서 다섯 화면이 비어 있는 동안 아무도 오류를 보지 못했다.
결론
다섯 화면이 비어 있었고 원인은 하나였다. 계약에 선언만 있고 구현이 없었다.
홈 「지금 집중하는 것」 : 세 슬롯이 다 비면 영역 자체를 그리지 않아 운영에서 나타난 적이 없다 프로젝트 공개 여부 : 투영의 PROJECT 행을 세우는 경로가 없어 영원히 비공개였다 문서 사이 관계 연결 : 어댑터의 RELATION 과 EVIDENCE 가 빈 목록 스텁이었다 프로젝트 활동 : 목록·생성·수정이 계약에 있고 테이블은 0행이었다 릴리즈 : 읽는 쪽만 있고 쓰는 쪽이 없었다
생성 모델 검사는 schema 와 property 만 보므로 이 구멍을 잡지 못한다. 모델은 멀쩡히 생성된다.
검증 환경
tech-log-backend : 365560e 이후
tech-log-frontend : 계약에서 생성한 타입을 그대로 사용
확인 방식 : 계약이 선언한 연산과 @RestController 매핑을 리플렉션으로 대조
재현 조건
- 계약에 연산을 하나 선언하고 컨트롤러는 만들지 않는다
- 프론트에서 그 연산을 부르는 화면을 연다 — 404 가 돌아오고 화면은 빈 목록을 그린다
ContractRouteCoverageTest를 돌린다 — 그 연산 하나를 짚는다
본문
비어 있던 다섯 화면
| 무엇이 비었나 | 왜 |
|---|---|
| 홈 「지금 집중하는 것」 | home_focus_config 는 마이그레이션이 빈 행 하나만 넣었고, getHomeFocus/updateHomeFocus 는 구현이 없었다 |
| 프로젝트 공개 여부 | 프로젝트는 RecordKind 에 없어 문서 게시 파이프라인을 타지 못하는데, 공개 화면들은 전부 public_resource_projection 의 PROJECT 행을 가시성 관문으로 쓴다 |
| 문서 사이 관계 연결 | JdbcCatalogQueryAdapter 의 RELATION/EVIDENCE 가 「슬라이스 2·5에서 채운다」는 주석과 함께 List.of() 스텁이었다 |
| 프로젝트 활동 | 계약에 목록·생성·수정이 선언돼 있었지만 구현이 없었고 project_activity 는 0행이었다 |
| 릴리즈(변경 기록) | 읽는 쪽은 있는데 쓰는 쪽이 없어, 페이지는 영원히 빈 채였다 |
홈 focus 가 가장 오래 숨었다. 세 슬롯이 다 비면 화면이 그 영역을 통째로 그리지 않으므로, 그런 영역이 있다는 사실조차 화면에서 알 수 없다.
화면은 오류를 내지 않고 빈칸을 그렸고, 저는 그것을 "아직 안 쓴 글"로 읽었습니다.
편집기가 부르던 두 목록
GET /v1/studio/questions 와 GET /v1/studio/projects/{id}/decisions 도 같은 모양이었다. 계약에 있고 모델도 생성됐는데 컨트롤러가 없었다. 화면은 그것을 「이 프로젝트에 열린 질문이 없습니다」로 그렸고, 실제로는 넷이 있었으며 공개 사이트에도 나오고 있었다.
마이그레이션 직후의 값
같은 계약이 반대 방향으로도 깨졌다. home_focus_config.default_focus_type 은 마이그레이션 직후 NULL 인데 계약은 이 필드를 required 에 enum 세 값으로 선언한다. 배포 직후 첫 요청부터 /home 이 깨졌고, HomeFocusView.resolve 가 반드시 유효한 값 하나를 정하도록 고쳤다.
계약과 컨트롤러를 전수로 맞춘다
ContractRouteCoverageTest 가 @RestController 들을 리플렉션으로 훑어 매핑을 모으고 계약이 선언한 경로와 대조한다.
- 작업본 API 로 대체된 옛 연산 51개는
SUPERSEDED_BY_WORKING_COPY_API로 명시한다 — 「구현하지 않기로 한 것」과 「빠뜨린 것」은 다르다 - 봉투 없이 바이트를 주는
/media하나만ELSEWHERE로 면제한다 - 매핑을 떼어 보고 그 연산 하나를 정확히 짚는 것을 확인했다
프론트에도 같은 가드를 뒀다. 양쪽에서 봐야 한쪽만 지웠을 때 잡힌다.
확인하지 못한 것
홈 focus 의 옛 증상은 재현할 수 없다. 세 슬롯이 다 비면 영역을 그리지 않으므로 화면에 남은 흔적이 없고, 지금 고쳐져 있다는 것만 확인했다.