그림 1의 🎯 카드를 확대했다
Assessment를 연다
도메인 한 장에서 판정은 카드 하나였다. 그 카드를 열면 객체들이 나온다 — 전체 설계도 한 장을 먼저 보고, 누가 무엇을 하는지(협력), 무엇을 품는지(구조), 어떤 순서인지(라인), 마지막으로 실측 순서로 파고든다.
한 장 — 전체 설계도
판정 설계 전체를 한 장에
세 가지 분리가 한 그림에 있다 — 도메인 분리(세 개의 존: 빌려 쓰는 사실 / 요청 동안만 존재하는 공장 / 영속되는 답), 책임 분리(역할 4색: 재료·규칙·값·라인), 그리고 관계 문법(흐름은 실선으로 아래로, ID 참조는 점선으로 위로).
일급 컬렉션
교체 가능
스스로 지킨다
Probability만분율 정수만 존재
PassedStopMarkhasPassed()로만 비교
Estimate값 아니면 사유
DataState확률 칸에서만 파생
query노선(버전)+순번
judgedAt주입된 시각
탈 확률4660 = 46.6%
몇 대사유: 모델 없음
몇 분사유: 모델 없음
precedingBus직전 버스 요약
dataStateOK
lineage모델 4 + 근거 범위
확대 1 — 객체와 협력
객체는 필드가 아니라 메시지로 산다
실측 질문 하나 — "18:40, 농수산물시장(순번 61), 탈 수 있어?" — 가 네 번의 메시지로 답이 된다. 각 객체는 받는 메시지(메서드)가 명함이고, 판단에 필요한 규칙을 자기 안에 갖고 있다. 서비스의 if문이 아니다.
스스로를 지키는 값들 — 잘못된 값은 태어나지 못한다
우테코 문법 그대로다 — LottoNumber가 setter 없이 검증과 비교만 갖듯, 값이 자기 규칙을 독점한다.
Probability만분율 정수(0~10000)만 존재. 소수점 확률은 생성자에서 죽는다 — 같은 입력이면 비트까지 같은 답. 보수 계산(complement)도 자기 몫.
PassedStopMark통과 비교는 hasPassed(순번)로만. 맨정수를 노출하지 않아 "실수로 순번과 비교"가 컴파일되지 않는다.
Estimate값이거나, 사유 있는 미추정이거나 — 둘뿐. "그냥 없음"은 표현 자체가 불가능하다 (블랙잭 Bust 문법).
DataStatefrom(확률 칸) 단일 인자 — 다른 칸을 보는 구현이 시그니처에서 막힌다. 파생 규칙이 타입에 박제.
규칙마다 집이 있다 — 서비스 if문이 아니라
| 도메인 규칙 | 소유 객체 (받는 메시지) |
|---|---|
| 나를 지난 차 중 누가 직전인가 + 동률이면 누구 | JudgedWindow.precedingOrderFor(순번) |
| 이 관측은 만석인가 | FullnessPolicy.judge(관측) |
| 만석일 때 탈 확률 | Probability.complement() |
| 이 차가 내 정류장을 지났나 | PassedStopMark.hasPassed(순번) |
| 화면에 어떤 배지를 다나 | DataState.from(확률 칸) |
| 사유 없는 미추정 금지 | NotEstimated 생성자 |
| 불완전한 답 금지 (세 칸·계보) | Assessment 생성자 — 불변식 28개 중 12개가 생성자 검증 |
| 흐름의 순서 (규칙 아님) | 조립 라인 — 판정 규칙 소유 금지가 라인의 계약 |
행동이 적은 객체도 둘 있다 — Lineage와 PrecedingBusView. 의도다: 영수증은 계산하지 않는다. 계산은 값객체가 하고, 기록은 그 결과가 완전한지만 보증한다.
확대 2 — 카드 속 구조
판정 한 장에 무엇이 들었나
확대 3 — 답이 만들어지는 길
답은 조립 라인에서 나온다
질문 하나가 일곱 단계를 지나 위의 카드가 된다. 각 단계는 자기가 안 하는 일이 명확하다 — 그래서 모델을 갈아 끼워도, 백테스트를 돌려도 라인의 모양이 안 바뀐다.
노선 버전·정류장 순번관측을 안 본다
유일한 바깥 접촉해석을 안 한다 — 실패도 사실
바퀴(trip) 나누기판정을 안 한다
FULL·AVAILABLE·모름창 전체를 안 본다
동률이면 명시 규칙좌석을 안 본다
재료 부족이면 라인이 접음포트 조회 금지 — 봉투로만
계보와 함께추정을 안 한다 — 항상 실행
모델은 카트리지다 — 슬롯 4개
계약은 두 가지뿐 — 라벨링(관측 1건 → 라벨)과 추정(봉투 → 답). 빈 슬롯에도 이름이 있어서, v1의 "몇 대·몇 분" 칸은 0이 아니라 사유 있는 안 함이 된다.
확대 4 — 실측으로 본 판정
2분 만에 답이 뒤집힌다
8/13 저녁, 3330 농수산물시장. 실제 관측으로 조립한 판정 두 장 — 직전 버스를 고르는 규칙 한 줄이 답을 지배한다. 그래서 규칙이 코드 구석이 아니라 도메인에 박제되고, 계보가 어느 규칙이었는지 기록한다.
46.6%
직전 버스 v581 — 내 정류장 지나 두 정거장 앞 · 잔여 0석 → FULL
몇 대·몇 분: 모델 없음(사유 기록) · dataState OK
직전 버스가
바뀌었다 →
94.7%
직전 버스 v167 — 내 정류장을 막 출발 · 잔여 31석 → AVAILABLE
같은 라인, 같은 모델 — 바뀐 것은 관측뿐
실패도 답이다 — 미추정 사유 7종
미추정은 예외(throw)가 아니라 사유를 품은 값 — 블랙잭 Bust와 같은 문법. 어느 단계가 접었는지가 화면 배지(dataState)까지 그대로 이어진다.
실패 경로 전수 — 어느 단계가 접고, 화면엔 뭐가 남나
| 사유 | 접히는 단계 | 모델 호출 | 화면 상태(dataState) |
|---|---|---|---|
ROUTE_NOT_SUPPORTED | 1.5 지원 게이트 — 수집을 아예 안 함(쿼터 방어) | 안 함 | MODEL_UNAVAILABLE |
NO_OBSERVATION | 3 성형 — 빈 창. "장애"를 "앞차 없음"으로 말하지 않도록 먼저 접는다 | 안 함 | UNAVAILABLE |
FULLNESS_UNKNOWN | 4 라벨 → 5 앵커에서 드러남 | 안 함 | SEAT_DATA_UNAVAILABLE |
NO_PRECEDING_BUS | 5 선택 — 후보 없음 | 안 함 | NO_PRECEDING_BUS |
AMBIGUOUS_PRECEDING | 5 선택 — 해시까지 간 동률 + 후보 라벨 불일치일 때만 | 안 함 | AMBIGUOUS |
STALE_OBSERVATION | 5→6 경계 — 앵커 나이 > 임계(잠정 10분) | 안 함 | STALE |
MODEL_ABSENT | 6 모델 — 빈 카트리지가 정직하게 답함 | 함 | 확률 칸이면 MODEL_UNAVAILABLE, 아니면 무영향 |
규칙: 접혀도 7 조립은 예외 없이 실행돼 계보를 남긴다. dataState는 확률 칸에서만 파생 — v1에서 몇 대·몇 분이 "모델 없음"이어도 화면은 OK.
전체 필드표 펼쳐 보기 (판정 스키마의 원형)
| 칸 | 필드 | 관계 | 근거 |
|---|---|---|---|
| 질문 🙋 | query | 품는다 | 노선(버전 포함) + 정류장 순번 — 개편돼도 과거 질문의 의미가 안 흔들린다 |
judgedAt | — | 주입된 판정 시각. 시계 직접 조회 금지 — 백테스트가 과거 시각을 넣는 자리 | |
| 답 세 칸 🎲 | boardingChance | 품는다 | Estimate — 만분율 정수(4660 = 46.6%). 부동소수 금지: 같은 입력이면 비트까지 같은 답 |
busesToSend | 품는다 | v1은 NotEstimated(MODEL_ABSENT) — 조용한 0 금지 | |
expectedWait | 품는다 | 〃 | |
| 직전 버스 👉 | precedingBus | 품는다 + 이름만 | 차량 가명 · 몇 정거장 앞(beyond) · 라벨 · 근거 시각 · 동률이면 어느 규칙으로 갈랐는지 |
| 화면 배지 🚦 | dataState | 품는다(파생) | 확률 칸에서만 파생한 7값 — 몇대·몇분이 "모델 없음"이어도 OK |
| 계보 🧾 | modelRefs ×4 | 품는다 | fullness·chance·episode·wait — 라벨 정책이 바뀌면 상수가 무효라 넷 다 기록 |
evidenceRef | 이름만 | Stored(사진 범위 ID) 또는 Ephemeral(즉석 해시) — 무저장 v1도 거짓 ID를 안 만든다 | |
digests · trainedThrough | 품는다 | 재현·비교 좌표. 학습 기준일 2026-08-07(코퍼스 실물로 확정) |
보류 목록 — 실측·팀 결정으로 닫을 항목 10
| 항목 | 닫는 방법 |
|---|---|
| 신선도 임계 구체값 (잠정 10분) | 가동 3일 judgedAt−observedAt 분포로 확정 |
| 좌석 앵커 정책 (최신 vs 통과시점) | 20일 아카이브로 두 정책 라벨 대조 |
| 직전 버스 거리 상한 K | 정차 소요 실측 후 결정 — 그때까진 기록만 |
| 동률 미해소의 실제 비용 | 첫 주 판정 로그 집계 (상한 1% 확정) |
| dataState 값과 프런트 계약 일치 | Client API 명세의 enum과 대조 |
| 공휴일 달력 출처·버전 | v2 진입 시 결정 (v1은 안 읽음) |
| STALE 도달 실증 (소스 시각 정지) | 장애 구간 응답 1건 포착 시 확인 |
| 백테스트 채점 구간 부재 (학습이 8/07까지 전 구간) | 팀 결정 — 신규 수집 채점 vs 학습 구간 분할 재산출 |
| 상수의 구간 승격 (점 추정 → 신뢰구간) | 코퍼스 재집계로 즉시 가능 |
| 즉석(Ephemeral) 판정의 채점 제외 방식 | 저장 도입 시점에 결정 |