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:
co-authored by
Claude Opus 5
parent
325b6008ab
commit
1be1c8ea6d
+29
-7
@@ -919,17 +919,31 @@ NOT_COMPARED = {
|
|||||||
"relations": "서버가 순서를 다시 매긴다. 목록 비교는 따로 만들어야 한다",
|
"relations": "서버가 순서를 다시 매긴다. 목록 비교는 따로 만들어야 한다",
|
||||||
}
|
}
|
||||||
|
|
||||||
# 항목 배열의 **`id` 만** 못 본다. 보내는 것은 `uuid5(slug|칸|순번)` 이고 서버는 저장하면서
|
# 항목 배열의 `id` 를 서버가 다시 매기는지는 **종류마다 다르고, 잰 종류만 안다.**
|
||||||
# 새 uuid4 를 발급한다 — C 가 V-009 의 `P-QUESTION-02` 에서 갈랐다(`text`·`order` 는 같고
|
#
|
||||||
# `id` 만 달랐다). 칸 전체를 빼면 **모든 QUESTION·REFERENCE 저장이 「차이 있음」으로 보고되거나
|
# QUESTION 다시 매긴다 — C 가 V-009 열 번째에서 봤다. 보낸 uuid5 와 받은 uuid4 가
|
||||||
# 반대로 항목이 빠진 것을 못 보게 된다.** 그래서 `id` 만 빼고 나머지는 계속 대조한다.
|
# 다르고 `text`·`order` 는 같았다
|
||||||
|
# PROJECT_DECISION **다시 안 매긴다** — A 가 B-011 운영 실행에서 봤다. 보낸 uuid5 여섯이
|
||||||
|
# 글자 그대로 돌아왔다
|
||||||
|
#
|
||||||
|
# 둘 다 `OrderedText` 배열인데 서버가 다르게 다룬다. **그러니 한 벌로 묶어 빼면 안 된다.**
|
||||||
|
# QUESTION 에서는 뺄 수밖에 없는 칸이지만 **DECISION 에서는 볼 수 있는 칸이고, 보면
|
||||||
|
# 「안 매겨졌다」는 사실이 드러난다.** 빼 버리면 서버가 언젠가 DECISION 도 재발급하기
|
||||||
|
# 시작해도 아무도 모른다.
|
||||||
|
#
|
||||||
|
# **안 잰 종류는 뺀 것이 아니라 그대로 본다.** 「못 보는 것」과 「안 봐도 되는 것」은 다르다 —
|
||||||
|
# 안 잰 채로 빼면 「안 봐도 되는 것」으로 굳는다. 차이가 나면 그때 재서 이 표에 적는다.
|
||||||
ITEM_ID_REISSUED = {
|
ITEM_ID_REISSUED = {
|
||||||
"facts", "assumptions", "unknowns", "constraints", "options",
|
"QUESTION": {"facts", "assumptions", "unknowns", "constraints", "options"},
|
||||||
"rules", "applyWhen", "exceptions", "examples", "consequences",
|
"PROJECT_DECISION": set(), # 쟀다 — 서버가 보낸 id 를 그대로 쓴다
|
||||||
}
|
}
|
||||||
ITEM_ID_NOTE = ("항목 `id` 만 못 본다 — 보낸 것은 uuid5 이고 서버가 새 uuid4 를 발급한다. "
|
ITEM_ID_NOTE = ("항목 `id` 만 못 본다 — 보낸 것은 uuid5 이고 서버가 새 uuid4 를 발급한다. "
|
||||||
"`text`·`title`·`body`·`order` 는 그대로 대조한다 — 항목이 빠지거나 "
|
"`text`·`title`·`body`·`order` 는 그대로 대조한다 — 항목이 빠지거나 "
|
||||||
"순서가 바뀌는 것은 여전히 걸린다")
|
"순서가 바뀌는 것은 여전히 걸린다")
|
||||||
|
ITEM_ID_UNMEASURED = ("이 종류는 서버가 항목 `id` 를 다시 매기는지 **아직 안 쟀다.** "
|
||||||
|
"그래서 빼지 않고 그대로 견준다 — `id` 만 다른 차이가 나오면 그것은 "
|
||||||
|
"값이 사라진 것이 아니라 **재발급을 처음 본 것**일 수 있다. "
|
||||||
|
"재서 ITEM_ID_REISSUED 에 적는다")
|
||||||
|
|
||||||
|
|
||||||
def _is_empty(v) -> bool:
|
def _is_empty(v) -> bool:
|
||||||
@@ -987,6 +1001,10 @@ def compare_saved(sent: dict, fetched: dict) -> dict:
|
|||||||
"""
|
"""
|
||||||
differences, whitespace_only, truncated = [], [], []
|
differences, whitespace_only, truncated = [], [], []
|
||||||
item_ids_skipped, empty_both = [], []
|
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():
|
for field, want in sent.items():
|
||||||
if field in NOT_COMPARED:
|
if field in NOT_COMPARED:
|
||||||
continue
|
continue
|
||||||
@@ -998,7 +1016,7 @@ def compare_saved(sent: dict, fetched: dict) -> dict:
|
|||||||
# 전부 비어 **견줄 것이 없어서** 나온 값이었다
|
# 전부 비어 **견줄 것이 없어서** 나온 값이었다
|
||||||
empty_both.append(field)
|
empty_both.append(field)
|
||||||
continue
|
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),
|
item_ids_skipped.append({"field": field, "items": len(want),
|
||||||
"note": ITEM_ID_NOTE})
|
"note": ITEM_ID_NOTE})
|
||||||
@@ -1024,6 +1042,10 @@ def compare_saved(sent: dict, fetched: dict) -> dict:
|
|||||||
"whitespaceOnly": whitespace_only,
|
"whitespaceOnly": whitespace_only,
|
||||||
"notCompared": NOT_COMPARED,
|
"notCompared": NOT_COMPARED,
|
||||||
"itemIdsNotCompared": item_ids_skipped,
|
"itemIdsNotCompared": item_ids_skipped,
|
||||||
|
# 안 잰 종류를 조용히 넘기지 않는다. 무엇을 왜 그대로 견줬는지 값에 적는다
|
||||||
|
**({"itemIdBehaviourUnmeasured": {"kind": kind, "fields": array_fields,
|
||||||
|
"note": ITEM_ID_UNMEASURED}}
|
||||||
|
if reissued is None and array_fields else {}),
|
||||||
"unexpected": unexpected,
|
"unexpected": unexpected,
|
||||||
# **실제로 견준 칸만 남는다.** 양쪽이 빈 칸은 `emptyBoth` 로 갈라 나간다.
|
# **실제로 견준 칸만 남는다.** 양쪽이 빈 칸은 `emptyBoth` 로 갈라 나간다.
|
||||||
# C 의 수정안은 이 칸을 개수로 바꿨는데 여기서는 목록을 유지하고 개수를 따로 둔다 —
|
# C 의 수정안은 이 칸을 개수로 바꿨는데 여기서는 목록을 유지하고 개수를 따로 둔다 —
|
||||||
|
|||||||
@@ -717,20 +717,21 @@ class HarnessTestPlanTest(unittest.TestCase):
|
|||||||
|
|
||||||
def test_item_ids_are_not_compared_but_text_and_order_are(self):
|
def test_item_ids_are_not_compared_but_text_and_order_are(self):
|
||||||
"""서버가 항목 `id` 를 새로 발급한다. 칸 전체를 빼면 항목이 빠진 것도 못 본다."""
|
"""서버가 항목 `id` 를 새로 발급한다. 칸 전체를 빼면 항목이 빠진 것도 못 본다."""
|
||||||
sent = {"facts": [{"id": "a5", "text": "하나", "order": 0},
|
K = {"kind": "QUESTION"} # 종류마다 다르다 — 아래 갈래 시험을 본다
|
||||||
{"id": "b5", "text": "둘", "order": 1}]}
|
sent = {**K, "facts": [{"id": "a5", "text": "하나", "order": 0},
|
||||||
same = {"facts": [{"id": "x4", "text": "하나", "order": 0},
|
{"id": "b5", "text": "둘", "order": 1}]}
|
||||||
{"id": "y4", "text": "둘", "order": 1}]}
|
same = {**K, "facts": [{"id": "x4", "text": "하나", "order": 0},
|
||||||
|
{"id": "y4", "text": "둘", "order": 1}]}
|
||||||
self.assertTrue(ss.compare_saved(sent, same)["same"])
|
self.assertTrue(ss.compare_saved(sent, same)["same"])
|
||||||
# **못 보는 것을 값에 적는다** — 빼먹은 것이 아니다
|
# **못 보는 것을 값에 적는다** — 빼먹은 것이 아니다
|
||||||
skipped = ss.compare_saved(sent, same)["itemIdsNotCompared"]
|
skipped = ss.compare_saved(sent, same)["itemIdsNotCompared"]
|
||||||
self.assertEqual("facts", skipped[0]["field"])
|
self.assertEqual("facts", skipped[0]["field"])
|
||||||
self.assertIn("order", skipped[0]["note"])
|
self.assertIn("order", skipped[0]["note"])
|
||||||
# 항목이 빠지거나 순서가 바뀌면 여전히 걸린다
|
# 항목이 빠지거나 순서가 바뀌면 여전히 걸린다
|
||||||
dropped = {"facts": [{"id": "x4", "text": "하나", "order": 0}]}
|
dropped = {**K, "facts": [{"id": "x4", "text": "하나", "order": 0}]}
|
||||||
self.assertFalse(ss.compare_saved(sent, dropped)["same"])
|
self.assertFalse(ss.compare_saved(sent, dropped)["same"])
|
||||||
swapped = {"facts": [{"id": "x4", "text": "둘", "order": 0},
|
swapped = {**K, "facts": [{"id": "x4", "text": "둘", "order": 0},
|
||||||
{"id": "y4", "text": "하나", "order": 1}]}
|
{"id": "y4", "text": "하나", "order": 1}]}
|
||||||
self.assertFalse(ss.compare_saved(sent, swapped)["same"])
|
self.assertFalse(ss.compare_saved(sent, swapped)["same"])
|
||||||
|
|
||||||
def test_decision_uses_the_contract_field_names(self):
|
def test_decision_uses_the_contract_field_names(self):
|
||||||
@@ -866,6 +867,41 @@ class HarnessTestPlanTest(unittest.TestCase):
|
|||||||
# 글이 있으면 반드시 무언가를 읽는다 — 조용히 비지 않는다
|
# 글이 있으면 반드시 무언가를 읽는다 — 조용히 비지 않는다
|
||||||
self.assertTrue(ss._question_options("제목 한 줄만 있다"))
|
self.assertTrue(ss._question_options("제목 한 줄만 있다"))
|
||||||
|
|
||||||
|
def test_item_id_handling_is_split_by_kind(self):
|
||||||
|
"""둘 다 `OrderedText` 배열인데 서버가 다르게 다룬다.
|
||||||
|
|
||||||
|
QUESTION 은 `uuid4` 로 다시 매기고(C · V-009 열 번째), **PROJECT_DECISION 은 보낸
|
||||||
|
`uuid5` 를 그대로 쓴다**(A · B-011 운영 실행). 한 벌로 묶어 빼면 DECISION 에서
|
||||||
|
**볼 수 있는 것을 안 보게 된다** — 서버가 언젠가 재발급을 시작해도 아무도 모른다.
|
||||||
|
"""
|
||||||
|
def arr(ids):
|
||||||
|
return [{"id": i, "text": f"t{n}", "order": n} for n, i in enumerate(ids)]
|
||||||
|
|
||||||
|
q = {"kind": "QUESTION", "facts": arr(["a5", "b5"])}
|
||||||
|
self.assertTrue(ss.compare_saved(q, {"kind": "QUESTION",
|
||||||
|
"facts": arr(["x4", "y4"])})["same"])
|
||||||
|
d = {"kind": "PROJECT_DECISION", "consequences": arr(["a5", "b5"])}
|
||||||
|
self.assertTrue(ss.compare_saved(d, dict(d))["same"])
|
||||||
|
# **DECISION 에서 id 가 바뀌면 보여야 한다** — 그것이 이 갈래의 값이다
|
||||||
|
moved = ss.compare_saved(d, {"kind": "PROJECT_DECISION",
|
||||||
|
"consequences": arr(["x4", "y4"])})
|
||||||
|
self.assertFalse(moved["same"])
|
||||||
|
self.assertEqual([], moved["itemIdsNotCompared"])
|
||||||
|
|
||||||
|
def test_an_unmeasured_kind_is_compared_not_skipped(self):
|
||||||
|
"""안 잰 종류는 뺀 것이 아니라 그대로 본다. 안 잰 채로 빼면
|
||||||
|
「안 봐도 되는 것」으로 굳는다 — 무엇을 왜 그대로 견줬는지 값에 적는다."""
|
||||||
|
ref = {"kind": "REFERENCE",
|
||||||
|
"rules": [{"id": "a5", "title": "t", "body": "b", "order": 0}]}
|
||||||
|
r = ss.compare_saved(ref, dict(ref))
|
||||||
|
self.assertNotIn("REFERENCE", ss.ITEM_ID_REISSUED)
|
||||||
|
self.assertEqual([], r["itemIdsNotCompared"])
|
||||||
|
self.assertEqual(["rules"], r["itemIdBehaviourUnmeasured"]["fields"])
|
||||||
|
self.assertIn("아직 안 쟀다", r["itemIdBehaviourUnmeasured"]["note"])
|
||||||
|
# 잰 종류에는 그 칸이 안 붙는다
|
||||||
|
q = {"kind": "QUESTION", "facts": [{"id": "a", "text": "t", "order": 0}]}
|
||||||
|
self.assertNotIn("itemIdBehaviourUnmeasured", ss.compare_saved(q, dict(q)))
|
||||||
|
|
||||||
def test_a_harness_plan_still_has_no_publish_path(self):
|
def test_a_harness_plan_still_has_no_publish_path(self):
|
||||||
steps = ss.plan_requests("docs/p/t/case/x.md", dict(SENT), None, None,
|
steps = ss.plan_requests("docs/p/t/case/x.md", dict(SENT), None, None,
|
||||||
harness_test=True)
|
harness_test=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user