Skip to content

feat(F-SCR-001): 판정이 근거 루브릭의 검토 상태를 들고 나간다 — 계약·레코드 쪽 (#609 ① ⓑ · #623 선행) - #624

Open
hd0rable wants to merge 2 commits into
mainfrom
feat/judgment-rubric-status-609
Open

hd0rable wants to merge 2 commits into
mainfrom
feat/judgment-rubric-status-609

Conversation

@hd0rable

Copy link
Copy Markdown
Member

#623 의 선행입니다. #609 ① ⓑ 의 계약·서버 절반이고, ai-service 쪽은 그 PR 입니다.

❗왜 이쪽이 먼저 들어가야 하나 — 뒤집으면 채점이 통째로 죽습니다

서버가 /internal/score 응답을 도메인 레코드로 바로 역직렬화하고, 그 경계 매퍼는 FAIL_ON_UNKNOWN_PROPERTIES 가 기본값(on)입니다.

AiServiceClient:239   .body(Judgment.class)

실측: rubric_status 가 실린 판정을 Judgment 로 읽으면
  UnrecognizedPropertyException: Unrecognized field "rubric_status"
  (class …domain.Judgment), not marked as ignorable
  (9 known properties: misconception_type, reason, item_id, confidence,
   source, prompt_version, escalate, grade, evidence)

한 항목이 아니라 경로 전체입니다. 그리고 운영자가 받는 문면이 AI_SERVICE_UNAVAILABLE 이라 ai-service 를 의심하게 됩니다 — 고칠 자리는 여기인데. #518 이 반대 방향으로 같은 사고를 냈습니다(우리가 모르는 키를 보내 저쪽 extra="forbid" 가 422, 알파에서 재설명이 전부 죽음).

무엇이 문제인가

status: confirmed 는 "근거자료 검토를 마쳤다" 는 뜻인데, 검토 전 기준으로 낸 판정이 레코드에서 확정 기준 판정과 똑같이 생겼습니다.

ELS   10종 · 확정 10
변액    7종 · 검토 전 7   ← 전부

S-02 에서 고를 수 있는 두 상품 중 변액을 고르면 그 세션의 이해항목 전부가 검토 전 기준으로 채점됩니다. evidence/ 가 append-only 라 나중에 되짚을 수 없습니다.

❗비어 있는 것이 두 가지입니다 — source 와 같이 읽습니다

source=SKIPPED  + 비었음   루브릭을 안 본 판정이다 — 건너뛴 항목은 채점을 안 지난다
source=MEASURED + 비었음   이 필드가 생기기 전 레코드다        ← 여기만 「모른다」

"not_applicable" 같은 값을 만들지 않는 이유는 source 가 이미 그 사실을 말하기 때문입니다. 한 사실이 두 벌이 됩니다.

그리고 「없으면 확정」으로 접지 않습니다. source 와 반대 방향입니다 — 저쪽은 없으면 MEASURED 가 사실과 같지만, 여기서 기본값을 주면 **이 필드가 생기기 전 레코드 전부가 「검토된 기준으로 판정했다」**로 읽히고, 그게 이 필드를 만드는 이유 자체를 지웁니다.

새 그물 — 들어오는 판정을 계약과 대조합니다

RiskItemWireContractTest 가 나가는 절반을 덮는데, 들어오는 절반은 아무도 안 봤습니다. 그쪽 실패가 더 큽니다.

★ 표본이 계약의 모든 필드를 든다          계약이 늘면 사람이 값을 적으면서 그 필드를 본다
❗계약이 허용하는 판정을 레코드가 읽는다     실제 클라이언트를 태운다 (MockRestServiceServer)
★ 계약에 없는 필드는 터진다              위 단정이 빈 통과가 아니라는 근거
❗레코드에만 있는 칸은 없다               상류는 계약만 보므로 그 값은 절대 안 온다

표본 값을 손으로 적는 이유는 스키마에서 만들어 내면 대조가 스스로를 증명하는 모양이 되기 때문입니다. 계약이 필드를 늘리면 사람이 값을 적으면서 그 필드가 무엇인지 보게 되고, 그 자리가 이 대조의 값입니다.

역검증

변이 결과
계약에 새 필드를 더한다(레코드엔 없음) 4건 중 2 실패 ← 이번에 겪은 그 시나리오
계약에서 rubric_status 를 뺀다 빨강
레코드를 @JsonIgnoreProperties(ignoreUnknown) 로 관대하게 「계약에 없는 필드는 터진다」가 빨강
기준선 4건 전부 초록

세 번째가 중요합니다 — 경계 매퍼가 관대해지면 첫째 대조가 아무것도 안 막는데, 그 사실을 배포에서 처음 만나게 됩니다.

❗남의 파일 둘을 건드렸습니다 — @gitIt-sehyeon

Judgment 에 칸이 늘면 적재 쪽에서 담을지 뺄지를 정하게 만든 그 단정이 빨개져서, 그 자리에서 정했습니다.

StoredEvidenceRecorder.judgmentPayload   rubricStatus 를 담는다
CanonicalJsonTest                        기대 문자열에 "rubricStatus":null 한 줄

담는 이유가 escalate 와 같습니다 — 재계산으로 못 되돌립니다. 루브릭 파일의 status 는 나중에 확정으로 바뀌고, 그러면 "이 판정이 검토 전 기준으로 나왔다" 를 되짚을 근거가 아무 데도 없습니다. 빼는 쪽이 맞다고 보시면 말씀해 주세요 — deliberatelyOmitted 에 이유와 함께 적으면 됩니다.

CanonicalJsonTest 쪽은 정규화가 바뀐 것이 아니라 판정이 바뀐 것이라, 그 파일이 source 때 적어 둔 그 모양 그대로입니다.

검증

server 전체   834건 · 실패 0 · skip 0   (새 대조 4건이 늘었다)

required 는 안 건드렸습니다. 나가는 본문(OutboundJudgment)도 그대로입니다 — 그 레코드가 필드를 골라 담아서 도메인이 늘어도 안 따라 늘어납니다(#518 이 만든 자리).

Refs #609 · #623

ai-service 쪽(#623)의 선행이다. 그쪽이 먼저 들어가면 채점 경로가 통째로 죽는다 —
서버가 /internal/score 응답을 도메인 레코드로 바로 역직렬화하는데 그 경계 매퍼는
FAIL_ON_UNKNOWN_PROPERTIES 가 기본값(on)이다.

  실측: rubric_status 가 실린 판정을 Judgment 로 읽으면
    UnrecognizedPropertyException: Unrecognized field "rubric_status"
    (9 known properties: misconception_type, reason, item_id, confidence,
     source, prompt_version, escalate, grade, evidence)

  운영자가 받는 문면은 AI_SERVICE_UNAVAILABLE 이라 ai-service 를 의심하게 된다.
  #518 이 반대 방향으로 같은 사고를 냈다(우리가 모르는 키를 보내 저쪽이 422).

무엇이 문제인가: status=confirmed 는 "근거자료 검토를 마쳤다" 는 뜻인데 검토 전 기준으로
낸 판정이 레코드에서 확정 기준 판정과 똑같이 생겼다. 지금 실물이 ELS 10종 확정 · 변액
7종 전부 검토 전이라, S-02 에서 변액을 고르면 그 세션의 이해항목 전부가 그 상태다.
evidence/ 가 append-only 라 나중에 되짚을 수 없다.

- 계약에 rubric_status 를 더했다(enum confirmed|draft · optional · required 는 안 바뀐다)
- Judgment 에 rubricStatus 칸. ❗null 을 confirmed 로 접지 않는다 — source 와 반대
  방향이다. 기본값을 주면 이 필드가 생기기 전 레코드 전부가 "검토된 기준으로 판정했다"
  로 읽히고, 그러면 이 필드를 만드는 이유 자체가 없어진다
- 비어 있는 것이 두 가지라 source 와 같이 읽는다: SKIPPED + 비었음은 "루브릭을 안 봤다",
  MEASURED + 비었음만 "모른다". 값을 더 만들지 않는 이유는 source 가 이미 그것을 말해서다
- 들어오는 판정을 계약과 대조하는 테스트를 새로 만들었다. 나가는 쪽은 RiskItemWireContractTest
  가 덮는데 들어오는 쪽은 아무도 안 봤고, 그쪽 실패가 더 크다(한 항목이 아니라 경로 전체)

역검증:
  계약에 새 필드를 더한다(레코드엔 없음)   4건 중 2 실패  ← 이번에 겪은 그 시나리오
  계약에서 rubric_status 를 뺀다          빨강
  레코드를 @JsonIgnoreProperties 로 관대하게  "계약에 없는 필드는 터진다" 가 빨강
  기준선                                  4건 전부 초록

server 전체 834건 · 실패 0 · skip 0

Refs #609 #623

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hd0rable hd0rable added 계약 모듈 간 계약 — contracts/, openapi.yaml, 스키마 server Spring (:8000) labels Sep 14, 2026
@github-actions github-actions Bot added 리뷰대기: 정세현 정세현 이 배정됐고 아직 아무것도 제출하지 않았다 리뷰대기: 오준서 오준서 이 배정됐고 아직 아무것도 제출하지 않았다 리뷰대기: 윤지석 윤지석 이 배정됐고 아직 아무것도 제출하지 않았다 labels Sep 14, 2026

@yoonjiseok yoonjiseok left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

설계·그물 다 좋습니다. 변경요청 하나 — 계약 description 의 한 문장이 참이 아닙니다. 제가 #623 에 적은 것과 27초 차이로 엇갈린 것 같습니다(제 코멘트 02:42:16 · 이 PR 02:42:43).

변이 셋을 제 쪽에서도 재현했습니다

기준선                                   JudgmentWireContractTest 4건 초록
M1) 계약에 새 필드(레코드엔 없음)          4건 중 2 실패   ← 본문과 같다
M2) 계약에서 rubric_status 를 뺀다        4건 중 2 실패
M3) 레코드를 ignoreUnknown 으로           1 실패 — 「계약에 없는 필드는 터진다」

M3 가 특히 값이 있습니다. 경계 매퍼가 관대해지는 순간 M1 이 아무것도 안 막는데, 그 사실을 배포에서 처음 만나게 됩니다 — 그물이 자기 전제를 지키는 모양입니다.

RULE → SKIPPED 정정도 확인했습니다(enum = ["MEASURED","SKIPPED"]).


❗변경요청 — 「MEASURED + 비었음만 모른다」가 참이 아닙니다

계약과 레코드 javadoc 두 곳에 같은 문장이 있습니다.

source=MEASURED 이면서 비어 있는 것만 「모른다」다

합성 세션이 그 칸에 떨어집니다.

// aggregate/SyntheticSessionLoader.java:175 — 6인자 생성자다. source 를 안 준다
out.put(itemId, new Judgment(itemId, grade, new BigDecimal("0.90"),
        new Judgment.Evidence("합성 세션 — 발화 없음", "합성 세션 — 루브릭 조항 없음"),
        "F-DSH-003 합성 세션(대시보드 구동용). 실제 판정이 아니다.", null));

// domain/Judgment.java:83 — compact ctor
source = source == null ? Source.MEASURED : source;

전수로 셌습니다. main 코드에서 Judgment 를 만드는 자리가 정확히 둘이고 갈립니다.

domain/SkippedItem.java:71            Judgment.Source.SKIPPED 를 명시한다        ✅ 깨끗하다
aggregate/SyntheticSessionLoader:175  source 를 안 줘서 MEASURED 로 접힌다        ❗

그리고 합성 세션은 SessionRepository.saveAll 로 DB 에 남습니다(:148). 즉 (MEASURED, null) 인 행이 실제로 저장되고, 그 행의 뜻은 "이 필드가 생기기 전 레코드" 가 아니라 "합성이라 채점을 안 지났다" 입니다.

왜 막는가 — 이 PR 이 고치려는 것과 같은 종류라서입니다

이 PR 의 논지가 "검토 전 판정이 확정 판정과 똑같이 생겼다" 인데, 그 해법으로 넣는 읽기 규칙이 세 번째 경우를 두 번째로 읽게 만듭니다. 게다가 description 은 계약 본문이라 읽는 쪽이 그대로 따릅니다 — #529 가 「옛 판을 지목하는 정규식」에서 밟은 거짓 양성과 같은 층입니다.

고치는 폭은 한 줄입니다

❗읽을 때 source 와 같이 본다: source=SKIPPED 이면서 비어 있는 것은 「루브릭을 안 본
판정」이고(건너뛴 항목은 채점을 안 지난다), source=MEASURED 이면서 비어 있는 것은
「이 필드가 생기기 전 레코드」다 — 단, 합성 세션(F-DSH-003)도 source 를 안 줘서
MEASURED 로 접히므로(SyntheticSessionLoader) 「옛 레코드」로 단정하지 않는다.

값을 더 만들자는 게 아닙니다. "not_applicable" 을 넣으면 같은 사실이 두 벌이 된다는 판단에 동의하고, 합성에 SKIPPED 를 주는 것도 반대입니다 — 합성은 「건너뛴」 것이 아니라 애초에 채점 경로 밖이고, 그건 집계 쪽 의미가 걸려서 여기서 곁가지로 정할 값이 아닙니다. 문장이 「전수」로 읽히지 않게만 하면 됩니다.

레코드 javadoc(domain/Judgment.java)에도 같은 표가 있으니 두 곳입니다.


나머지 — 이견 없습니다

  • 순서 판단. FAIL_ON_UNKNOWN_PROPERTIES 실측을 그대로 받습니다. #518 과 방향이 반대인 같은 경계라는 정리가 정확하고, 한 항목이 아니라 경로 전체라는 것과 문면이 AI_SERVICE_UNAVAILABLE 이라 ai-service 를 의심하게 된다는 것까지가 이 PR 을 먼저 넣을 이유입니다.
  • 들어오는 절반에 그물이 없었다는 진단. RiskItemWireContractTest 가 나가는 쪽만 덮고 있었다는 것, 그리고 그쪽 실패가 더 크다는 것 — 나가는 쪽은 422 하나지만 들어오는 쪽은 역직렬화라 경로가 통째로 죽습니다.
  • 표본 값을 손으로 적은 것. "스키마에서 만들어 내면 대조가 스스로를 증명하는 모양" 이 맞습니다. #284 가 하드코딩 dict 를 루브릭 파일로 옮긴 것과는 반대 방향인데 이유가 같습니다 — 거기는 두 벌이 문제였고 여기는 자기참조가 문제입니다.
  • StoredEvidenceRecorder 에 담는 판단. "재계산으로 못 되돌린다 — 루브릭 파일의 status 는 나중에 확정으로 바뀐다" 가 결정적입니다. 제 #623 본문이 든 근거(evidence/ append-only)를 적재 쪽에서 한 번 더 세워 주셨습니다. escalate 와 같은 층이 맞습니다.
  • CanonicalJsonTest — 정규화가 아니라 판정이 바뀐 것이고 키 순서(reason < rubricStatus < source)도 맞습니다.

한 문장만 고쳐 주시면 승인하고, 이게 머지되면 #623 의 draft 를 풀겠습니다.

@github-actions github-actions Bot added 리뷰중: 윤지석 윤지석 이 코멘트·변경요청을 냈고 아직 승인하지 않았다 and removed 리뷰대기: 윤지석 윤지석 이 배정됐고 아직 아무것도 제출하지 않았다 labels Sep 14, 2026
@yoonjiseok

Copy link
Copy Markdown
Contributor

정정 — 리뷰에 "전건은 못 돌렸다(Gradle 이 메모리로 죽어서 새 대조 4건만 돌렸다)" 고 적었는데, 다시 돌려서 전건이 통과하는 것을 확인했습니다.

server 전체   834건 · 실패 0 · skip 0      ← 본문 숫자와 같습니다

아까 BUILD FAILED 가 난 것은 제가 검증 스크립트를 pkill 로 죽이면서 자식 gradle 이 같이 죽은 것이었습니다. 환경 탓으로 적었는데 제 손이었습니다 — 「죽었다」와 「죽였다」를 안 가르고 넘겼습니다.

변경요청 내용은 그대로입니다(계약·javadoc 의 「MEASURED + 비었음만 모른다」 한 문장).

 리뷰)

계약 description 과 레코드 javadoc 이 같은 문장을 들고 있었는데, 읽기 규칙이 세 번째
경우를 두 번째로 읽게 만든다.

  domain/SkippedItem.java:71            Source.SKIPPED 를 명시한다              깨끗하다
  aggregate/SyntheticSessionLoader:175  6인자 생성자라 source 를 안 준다
                                        → compact ctor 가 MEASURED 로 접는다   ❗
                                        → sessions.saveAll 로 DB 에 남는다

즉 (MEASURED, 비었음) 인 행이 실제로 저장되고, 그 뜻은 "이 필드가 생기기 전 레코드" 가
아니라 "합성이라 채점을 안 지났다" 다. description 은 계약 본문이라 읽는 쪽이 그대로
따르므로, 「단정하지 않는다」로 고친다.

값을 더 만들지 않는다. not_applicable 은 같은 사실을 두 벌로 만들고, 합성에 SKIPPED 를
주는 것도 아니다 — 합성은 건너뛴 것이 아니라 애초에 채점 경로 밖이고 그 값의 뜻은 집계
쪽이 정할 것이다.

server 전체 834건 · 실패 0 · skip 0 (문면만 · 동작 변경 0)

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@hd0rable

Copy link
Copy Markdown
Member Author

2382611 로 고쳤습니다. 맞는 지적이고, 제가 없애려던 결함과 같은 종류를 새로 만들 뻔했습니다.

제 쪽에서도 전수로 셌습니다 — main 코드에서 Judgment 를 만드는 자리가 정확히 둘이고 갈립니다.

domain/SkippedItem.java:62            Source.SKIPPED 를 명시한다        깨끗하다
aggregate/SyntheticSessionLoader:175  6인자 생성자 — source 를 안 준다
                                      → compact ctor 가 MEASURED 로 접는다   ❗
                                      → sessions.saveAll(:148) 로 DB 에 남는다

(MEASURED, 비었음) 인 행이 실제로 저장되고, 그 뜻은 "이 필드가 생기기 전 레코드" 가 아니라 "합성이라 채점을 안 지났다" 입니다.

❗제가 「전수」라고 쓴 것이 문제의 핵심입니다

이 PR 의 논지가 "검토 전 판정이 확정 판정과 똑같이 생겼다" 인데, 그 해법으로 넣은 읽기 규칙이 세 번째 경우를 두 번째로 읽게 만들었습니다. 그리고 description 은 계약 본문이라 읽는 쪽이 그대로 따릅니다 — 잘못된 판별 규칙은 없는 것보다 나쁩니다(#557 이 그 대가였습니다).

제안하신 문면을 받았고 두 곳 다 고쳤습니다.

source=SKIPPED  + 비었음   루브릭을 안 본 판정이다
source=MEASURED + 비었음   대개 「이 필드가 생기기 전 레코드」인데, 합성 세션도
                           source 를 안 줘서 여기 접히므로 「옛 레코드」로 단정하지 않는다

값을 더 만들지 않는 판단도 같습니다 — "not_applicable" 은 같은 사실을 두 벌로 만들고, 합성에 SKIPPED 를 주는 것은 합성이 「건너뛴」 것이 아니라 애초에 채점 경로 밖이라 맞지 않습니다. 그 값의 뜻은 집계 쪽이 정할 것이라 여기서 곁가지로 정하지 않겠습니다.

그물을 안 단 이유

이 문장에는 대조를 안 붙였습니다. 틀리는 방향이 안전한 쪽이라서입니다 — 나중에 합성이 자기 값을 갖게 되면 이 문장은 「과하게 조심하는 문장」이 되지 지금처럼 틀린 판별 규칙이 되지는 않습니다. 반대 방향(제가 쓴 「…만」)이 위험한 쪽이었고, 그건 없앴습니다.

변이 재현 감사합니다

M3(ignoreUnknown 으로 관대하게)를 "그물이 자기 전제를 지키는 모양" 으로 읽어 주신 것이 정확합니다. M1 이 무는 것은 경계가 엄격하다는 전제 위에서만 참이라, 그 전제가 깨지는 순간을 M3 가 잡습니다.

server 834건 · 실패 0 · skip 0 (문면만 · 동작 변경 0)

@yoonjiseok yoonjiseok left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인합니다. 두 곳(description · 레코드 javadoc) 다 고쳐졌고 전건 재확인했습니다.

server 834 · 실패 0 · skip 0   (문면만 · 동작 변경 0)

"잘못된 판별 규칙은 없는 것보다 나쁘다" 가 정확한 정리입니다. 읽는 쪽이 그대로 따르는 자리라 그렇습니다.

그물을 안 단 이유에 동의합니다 — 다만 방향이 하나 더 있습니다

틀리는 방향이 안전한 쪽이라서다 — 나중에 합성이 자기 값을 갖게 되면 이 문장은 「과하게 조심하는 문장」이 되지 틀린 판별 규칙이 되지는 않는다

그 방향은 맞습니다. 그런데 반대편이 하나 남습니다.

합성이 자기 값을 갖는다        문장이 과해진다        안전 ✅
❗세 번째 생산자가 생긴다      문장이 다시 불완전해진다  위험 — 그 행들이 「옛 레코드」로 읽힌다

지금 main 에서 Judgment 를 만드는 자리가 정확히 둘이라 문장이 참인데, 셋째가 source 를 안 주고 생기면 제가 이번에 낸 것과 똑같은 상태가 됩니다. 그때 이 문장은 「합성 세션도」까지만 말하고 새 생산자는 안 말합니다.

붙인다면 문장이 아니라 전제에 겁니다 — 「Judgment 를 만드는 자리가 둘이고 하나만 source 를 명시한다」를 못 박는 것입니다. 늘면 빨개져서 "이 문장을 같이 보라" 고 말합니다. 제가 #617 에서 _DRAFT_IN_SCORING 을 그렇게 뒀고, 그때 배운 것이 집합을 못박는 것으로는 구성이 안 잡힌다(원소가 같은 채로 모집단이 바뀐다)였습니다 — 여기서는 반대로 모집단(생산자 수) 을 못박는 자리입니다.

이 PR 에서 하실 일은 아닙니다. #623 이 머지된 뒤에 봐도 되고, 안 걸어도 문장은 지금 참입니다. 판단에 맡깁니다.

나머지

M3 를 "M1 이 무는 것은 경계가 엄격하다는 전제 위에서만 참" 으로 정리해 주신 것 — 그물이 자기 전제를 지키는 모양이라 값이 큽니다. 이번 주에 그 형태를 세 번 봤습니다(#621 의 「건너뛴 것」 단정 · 이 PR 의 M3 · 제 #617 의 「고쳐지면 빨개진다」).

#623 은 이게 머지되면 main 을 합쳐 다시 재고 draft 를 풀겠습니다.

@github-actions github-actions Bot removed the 리뷰중: 윤지석 윤지석 이 코멘트·변경요청을 냈고 아직 승인하지 않았다 label Sep 14, 2026

@junseo2323 junseo2323 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

승인합니다. 순서 주장(이쪽이 먼저)이 이 PR 에서 제일 비싼 대목이라 그것부터 재현했고, #623 의 실물과 이름이 맞는지도 대조했습니다. evidence 에 칸을 더하는 것이 옛 레코드를 깨지 않는다는 것도 확인했습니다(④).

① 순서가 강제라는 주장 — 전이로 확인됩니다

Judgment 경계가 실제로 엄격한지를 변이로 쟀습니다.

ⓒ 레코드에 @JsonIgnoreProperties(ignoreUnknown = true) 를 붙인다
  → "★ 계약에 없는 필드는 터진다 — 위 단정이 빈 통과가 아니라는 근거다"  FAILED

관대하게 만들면 빨개진다 = 지금은 모르는 키에 실제로 터진다는 뜻입니다. 그러니 #623 이 먼저 들어오면 rubric_status 를 실은 판정이 Judgment 역직렬화에서 죽고, 그건 한 항목이 아니라 채점 경로 전체입니다. 운영자가 받는 문면이 AI_SERVICE_UNAVAILABLE 이라 ai-service 를 보러 간다는 것까지 맞습니다 — #518 의 거울상입니다.

② #623 의 실물과 이름이 맞습니다

ai-service/app/schemas.py   rubric_status: Literal["confirmed", "draft"] | None
ai-service/app/scoring.py   judgment = _pin_rubric_status(judgment, rubric)
                            model_copy(update={"rubric_status": rubric.status})
contracts (이 PR)            "rubric_status": enum [confirmed, draft] · optional

와이어 이름·값·**「모델이 채우지 않는다 — 후처리에서 고정한다」**까지 양쪽 문면이 같습니다. 저쪽 테스트가 "루브릭 파일이 진실이다" 로 파일과 대조하는 것도 봤습니다.

③ 나머지 변이

기준선                                         server 전체 BUILD SUCCESSFUL
ⓐ 계약에만 새 필드를 더한다                      4건 중 2 FAILED
   "★ 표본이 계약의 모든 필드를 든다" + "❗레코드에만 있는 칸은 없다"
ⓓ 이 브랜치 + origin/main merge                 BUILD SUCCESSFUL (충돌 0)

ⓐ 가 둘인 것이 이 대조의 모양을 잘 보여줍니다 — 표본이 먼저 걸리고, 그 다음에 방향 대조가 걸립니다. 표본 값을 손으로 적는 판단(스키마에서 만들면 자기가 자기를 증명한다)에 동의합니다.

④ evidence 에 칸을 더해도 옛 레코드는 안 깨집니다 — 확인했습니다

append-only 스트림에 payload 모양을 바꾸는 변경이라 옛 항목의 검증이 깨지는지를 봤습니다.

HashChain.verify:103   String recomputed = link(entry.prevHash(), entry.seq(), entry.payload());
                       ▲ 저장된 payload 를 다시 해시한다 — 지금 코드로 다시 만들지 않는다

새 키는 이 커밋 이후 기록에만 붙고 옛 체인은 그대로 통과합니다. 담는 판단에도 동의합니다 — escalate 와 같은 이유이고, 루브릭 파일의 status 는 나중에 confirmed 로 바뀝니다. 그때 되짚을 근거가 레코드 밖에 없으므로 재계산으로 복원이 안 됩니다. 빼자고 하지 않겠습니다.

⑤ null 을 confirmed 로 안 접는 것이 이 PR 의 요점입니다

source 와 반대 방향이라는 설명이 정확합니다. 그리고 @gitIt-sehyeon 님 지적으로 들어간 합성 세션 칸(MEASURED + null 이 전수가 아니다)까지 javadoc 에 남은 것이 좋습니다 — 그 한 문단이 없으면 다음 사람이 그 행을 「옛 레코드」로 단정합니다.

⑥ 곁 — rubricStatus 가 String 인 것

계약은 enum 인데 레코드는 String 입니다. grade·source 가 Java enum 인 것과 다른 선택이라 잠깐 멈췄는데, 이 PR 의 논지와 같은 방향이라 그대로 두는 것이 맞다고 봅니다: enum 으로 받으면 저쪽이 셋째 값을 더하는 날 채점 경로가 통째로 죽습니다 — 이 PR 이 막으려는 바로 그 사고입니다.

대신 값이 경계에서 검사되지 않으므로 오타가 오면 그대로 evidence 에 굳습니다(append-only 라 되돌릴 수 없습니다). 지금은 _pin_rubric_status 가 루브릭 파일 값을 그대로 실어서 오타가 날 자리가 없으니, 막지 않고 기록만 남깁니다.

@github-actions github-actions Bot removed the 리뷰대기: 오준서 오준서 이 배정됐고 아직 아무것도 제출하지 않았다 label Sep 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

server Spring (:8000) 계약 모듈 간 계약 — contracts/, openapi.yaml, 스키마 리뷰대기: 정세현 정세현 이 배정됐고 아직 아무것도 제출하지 않았다

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants