선수 한 명이 지면이 되기까지 여섯 단계를 지난다. 단계마다 누가 무엇을 맡는지 정해 뒀고, 그 경계를 제일 많이 고쳤다.
리뷰를 쓰는 파이프라인은 이미 있었다. 영화와 앨범과 책을 다루는 art-review다. 선수 지면도 거의 같은 모양으로 짜고 두 자리만 뒤집었다.
하나는 점수다. 리뷰에는 별점이 있는데 선수에게는 안 매긴다. 사람의 일대기를 5점 만점으로 재고 싶지 않았고, 그 자리를 채울 게 이미 있었다. 전시대다. 무엇을 받았고 몇 번 리그 선두였나가 그 선수의 값어치를 말한다. 내가 매기는 수가 아니라 기록에 있는 수다.
다른 하나는 디자인이다. 리뷰는 작품마다 지면을 따로 디자인한다. 선수는 전부 같은 신문 판형을 쓰고 팀 색만 바뀐다. 그래서 리뷰 쪽의 Artist 단계가 여기엔 없다. 대신 맨 앞에 Analyst가 섰다. 커리어를 두고 무슨 말을 하려면 그 커리어의 기록부터 뽑아야 하는데, 그 일이 작지 않았다.
짓는 것과 짓지 않는 것
엔진은 레코드의 척추를 짓는다. 연도와 나이, 그해 소속팀, 시즌 스탯, 리그 선두 타이틀, 수상, 마일스톤, 생애 사건. 그 해를 무엇이라 부를지와 지면에서 얼마나 크게 실을지. 이 지면이 사진을 몇 장 원하는지, 산문에 적힌 숫자가 다 기록에 닿는지도 엔진이 본다.
문장과 사진은 짓지 않는다. 문장은 Writer가 쓰고 사진은 Discussant이 고른다.
여기서 하나가 따라온다. 엔진은 그걸 만들지 않으니 지우지도 않는다. 레코드를 다시 뽑을 때 지면의 글과 사진이 날아가면, 기록 하나 바뀔 때마다 사람이 쓴 걸 다시 채워야 한다. 그래서 재생성이 자기 출력을 들고 오지 않게 했다. 연도별 서사는 data/narratives/에서 다시 채우고, 사진 자리는 지면 프론트매터의 year와 frame에서 다시 센다. 엔진이 비우고 게이트가 정본에서 다시 넣는다.
문턱값을 한 곳에 모았다
판단에 쓰는 수를 쓰는 자리마다 적어 두면 같은 판단을 두 파일이 다른 수로 한다. 투수를 50이닝으로 가르는 파일과 60이닝으로 가르는 파일이 생기고, 어느 쪽이 맞는지 아무도 모른다.
문턱은 전부 src/config.mjs 하나에 뒀다. 값마다 왜 그 수인지를 옆에 적었다.
/**
* 투수인가 — **통산 투구 이닝**의 문턱.
*
* 출장 수로 재면 안 된다: 대패한 경기에서 한 이닝 던지는 야수가 흔하고(통산 1~10이닝이
* 2,029명), 반대로 등판이 적어도 선발로 350이닝을 던진 오타니 같은 이도 있다.
*/
PITCHER_MIN_IP: 50,
이 주석이 나중에 제일 쓸모 있었다. 반년 뒤에 값을 만질 때 왜 50이었는지를 다시 재지 않아도 됐다. 리그와 수상의 정의도 src/awards.mjs에 같은 식으로 모아 뒀다.
엔진을 어디서 돌리나
엔진 코드는 cosmos 저장소에 있고 결과물인 선수 번들은 사이트 저장소로 흐른다. 사이트를 엔진 쪽으로 들이지는 않는다.
주의할 데가 하나 있다. cosmos는 플러그인으로도 깔려 있어서 ~/.claude/plugins/ 아래에 복사본이 또 있다. 그쪽은 자동으로 갱신되는 캐시다. 거기서 코드를 고치거나 결과물을 쓰면 다음 갱신에 사라진다. 그래서 엔진은 늘 개발 클론에서 돌린다. 경로는 환경변수로 받고, 없으면 묻는다. 짐작하지 않는다.
여섯 단계 중 다섯은 코드가 거든다. 사람이 꼭 해야 하는 건 셋이다. 어느 선수인지 확정하고, 사진을 고르고, 나온 문장을 읽고 고친다.