fix(studio-save): 항목 id 를 다시 매기는지는 종류마다 다르다

A 가 B-011 운영 실행에서 봤다. PROJECT_DECISION 은 보낸 uuid5 여섯이 글자 그대로
돌아온다. QUESTION 은 서버가 uuid4 로 다시 발급한다(C · V-009 열 번째). 둘 다
OrderedText 배열인데 서버가 다르게 다룬다.

한 벌로 묶어 빼면 DECISION 에서 볼 수 있는 것을 안 보게 된다 — 서버가 언젠가
DECISION 도 재발급하기 시작해도 아무도 모른다. 그래서 종류로 갈랐다.

  QUESTION          다시 매긴다 → id 를 뺀다 (쟀다)
  PROJECT_DECISION  다시 안 매긴다 → 그대로 본다 (쟀다)
  그 밖              안 쟀다 → 빼지 않고 그대로 보고, 안 쟀다는 것을 값에 적는다

안 잰 채로 빼면 「안 봐도 되는 것」으로 굳는다. 「못 보는 것」과 「안 봐도 되는 것」은
다르다 — itemIdBehaviourUnmeasured 가 어느 칸을 왜 그대로 견줬는지 적는다.

python3 -m unittest discover -s scripts/tests — Ran 286 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
This commit is contained in:
DongHyeonka
2026-09-11 11:19:55 +09:00
co-authored by Claude Opus 5
parent 325b6008ab
commit 1be1c8ea6d
2 changed files with 72 additions and 14 deletions
+29 -7
View File
@@ -919,17 +919,31 @@ NOT_COMPARED = {
"relations": "서버가 순서를 다시 매긴다. 목록 비교는 따로 만들어야 한다",
}
# 항목 배열의 **`id` 만** 못 본다. 보내는 것은 `uuid5(slug|칸|순번)` 이고 서버는 저장하면서
# 새 uuid4 를 발급한다 — C 가 V-009 의 `P-QUESTION-02` 에서 갈랐다(`text`·`order` 는 같고
# `id` 만 달랐다). 칸 전체를 빼면 **모든 QUESTION·REFERENCE 저장이 「차이 있음」으로 보고되거나
# 반대로 항목이 빠진 것을 못 보게 된다.** 그래서 `id` 만 빼고 나머지는 계속 대조한다.
# 항목 배열의 `id` 를 서버가 다시 매기는지는 **종류마다 다르고, 잰 종류만 안다.**
#
# QUESTION 다시 매긴다 — C 가 V-009 열 번째에서 봤다. 보낸 uuid5 와 받은 uuid4 가
# 다르고 `text`·`order` 는 같았다
# PROJECT_DECISION **다시 안 매긴다** — A 가 B-011 운영 실행에서 봤다. 보낸 uuid5 여섯이
# 글자 그대로 돌아왔다
#
# 둘 다 `OrderedText` 배열인데 서버가 다르게 다룬다. **그러니 한 벌로 묶어 빼면 안 된다.**
# QUESTION 에서는 뺄 수밖에 없는 칸이지만 **DECISION 에서는 볼 수 있는 칸이고, 보면
# 「안 매겨졌다」는 사실이 드러난다.** 빼 버리면 서버가 언젠가 DECISION 도 재발급하기
# 시작해도 아무도 모른다.
#
# **안 잰 종류는 뺀 것이 아니라 그대로 본다.** 「못 보는 것」과 「안 봐도 되는 것」은 다르다 —
# 안 잰 채로 빼면 「안 봐도 되는 것」으로 굳는다. 차이가 나면 그때 재서 이 표에 적는다.
ITEM_ID_REISSUED = {
"facts", "assumptions", "unknowns", "constraints", "options",
"rules", "applyWhen", "exceptions", "examples", "consequences",
"QUESTION": {"facts", "assumptions", "unknowns", "constraints", "options"},
"PROJECT_DECISION": set(), # 쟀다 — 서버가 보낸 id 를 그대로 쓴다
}
ITEM_ID_NOTE = ("항목 `id` 만 못 본다 — 보낸 것은 uuid5 이고 서버가 새 uuid4 를 발급한다. "
"`text`·`title`·`body`·`order` 는 그대로 대조한다 — 항목이 빠지거나 "
"순서가 바뀌는 것은 여전히 걸린다")
ITEM_ID_UNMEASURED = ("이 종류는 서버가 항목 `id` 를 다시 매기는지 **아직 안 쟀다.** "
"그래서 빼지 않고 그대로 견준다 — `id` 만 다른 차이가 나오면 그것은 "
"값이 사라진 것이 아니라 **재발급을 처음 본 것**일 수 있다. "
"재서 ITEM_ID_REISSUED 에 적는다")
def _is_empty(v) -> bool:
@@ -987,6 +1001,10 @@ def compare_saved(sent: dict, fetched: dict) -> dict:
"""
differences, whitespace_only, truncated = [], [], []
item_ids_skipped, empty_both = [], []
kind = (sent.get("kind") or "").upper()
reissued = ITEM_ID_REISSUED.get(kind)
array_fields = [f for f, v in sent.items()
if isinstance(v, list) and any(isinstance(x, dict) for x in v)]
for field, want in sent.items():
if field in NOT_COMPARED:
continue
@@ -998,7 +1016,7 @@ def compare_saved(sent: dict, fetched: dict) -> dict:
# 전부 비어 **견줄 것이 없어서** 나온 값이었다
empty_both.append(field)
continue
if field in ITEM_ID_REISSUED and isinstance(want, list):
if reissued and field in reissued and isinstance(want, list):
# **빼먹은 것이 아니라 못 보는 것**이므로 무엇을 왜 안 보는지 값에 적는다
item_ids_skipped.append({"field": field, "items": len(want),
"note": ITEM_ID_NOTE})
@@ -1024,6 +1042,10 @@ def compare_saved(sent: dict, fetched: dict) -> dict:
"whitespaceOnly": whitespace_only,
"notCompared": NOT_COMPARED,
"itemIdsNotCompared": item_ids_skipped,
# 안 잰 종류를 조용히 넘기지 않는다. 무엇을 왜 그대로 견줬는지 값에 적는다
**({"itemIdBehaviourUnmeasured": {"kind": kind, "fields": array_fields,
"note": ITEM_ID_UNMEASURED}}
if reissued is None and array_fields else {}),
"unexpected": unexpected,
# **실제로 견준 칸만 남는다.** 양쪽이 빈 칸은 `emptyBoth` 로 갈라 나간다.
# C 의 수정안은 이 칸을 개수로 바꿨는데 여기서는 목록을 유지하고 개수를 따로 둔다 —