fix: 모순 검출 그룹핑 키의 주어·객체 축을 유니코드 정규형으로 접는다 - #334
Conversation
업스트림 #325 (1/2) 세 축 중 관계 축만 유니코드 정규형을 접고 있었다. untyped 객체 축은 원문 그대로라, 같은 값이 NFC/NFD 로 혼용 저술되면 화면상 구별되지 않는 두 값을 모순으로 신고했다. 사용자에게는 같은 문자열 두 개가 모순이라고 표시되므로 자기 완결적 오진이다. - `_group_key` 진입부에서 객체를 한 번 NFC 로 접고, 접은 문자열을 `literal_types.normalize` 와 raw fallback 양쪽에 쓴다. NFD 로 저술된 typed 리터럴은 파싱에 실패해 `("raw", …)` 로 degrade 하므로, fallback 만 접으면 `("scalar", …)` 인 NFC 쌍둥이와 끝내 만나지 못한다. amount·ordinal 단위가 한글(억/조/호/위)이라 typed 경로가 오히려 더 취약하다. - 병합된 그룹의 대표는 `_representative` 로 뽑는다. 단순 `min` 은 결정론적이지만 한글에서 조합형 자모(U+1100~)가 완성형(U+AC00~)보다 항상 작아 혼용 그룹에서 늘 NFD 를 고른다 — NFC 편집기에서 타이핑·복사해도 매치되지 않는 쪽이다. 완성형을 우선하되 동률은 사전순으로 깨 결정론을 유지한다. NFC 는 정준 등가만 접는다. NFKC 도 casefold 도 쓰지 않으므로 전각과 대소문자는 계속 별개 값으로 남는다. `common._canonical_value` 는 amount 인용 정규화가 얹혀 있어 재사용하지 않는다. 자체 부과 불변식은 낮춘다. 전-NFD typed KB 는 출력이 바뀔 수 있다 — 두 NFD 행이 전에는 서로 다른 raw 문자열로 갈렸다가 이제 파싱돼 한 스칼라로 접힌다. 이는 회귀가 아니라 #116 의 교차표기 등가(억↔조)가 그 KB 에서 처음 작동하기 시작하는 것이다. 이슈 AC 가 요구하는 불변식은 NFC-only KB 바이트 동일성이고 그건 유지된다. 테스트: `tests/unit/test_conflict_unicode.py` 10건.
업스트림 #325 (2/2) 주어 축이 원문 그대로라, 같은 주체의 참 모순이 NFC/NFD 로 혼용 저술되면 두 개의 단일 그룹으로 쪼개져 검출되지 않았다. unsound 다 — finalize 의 모순 게이트가 같은 함수를 쓰므로, 게이트가 통과시킨 KB 에 실제 모순이 남고 사람이 볼 기회가 없다. - 그룹 키의 주어 축을 NFC 로 접고, 리포트 키는 그 (접은 주어, 정규 관계) 쌍에서 실제로 관측된 원문 주어로 복원한다. 맵 세분도는 전역이 아니라 쌍별이다. 전역으로 잡으면 폴딩이 아무것도 병합하지 않는 입력에서도 다른 관계의 주어 표기가 바뀌어 바이트 동일성이 깨진다. - `collect_conflicts` 를 추가해 병합된 원문 표기를 별도 채널로 노출한다. 주어와 객체 두 축 모두에 채널을 둔다 — 정보 손실이 양쪽에서 동일하다. `detect_conflicts` 의 반환 형태는 그대로다(핀 36건이 그 dict 을 직접 assert). - 폴딩이 병합한 표기를 리포트에 노출한다. 대표 복원만으로는 리포트의 문자열을 grep 해도 그 뒤의 모든 행이 나오지 않는다. 개수만으로는 조치할 수 없으므로 (두 표기가 화면상 동일하다) 표기 자체를 `ascii()` 로 이스케이프해 함께 찍는다. - 표기 혼용 안내는 supersede 안내를 대체하지 않고 그 위에 덧붙인다. 혼용 판정은 "폴딩이 이 모순을 만들어냈다" 가 아니라 "이 쌍에 표기가 섞여 있다" 이므로, 폴딩 이전에도 검출되던 참 모순에 NFD 행이 하나 끼는 경우가 있다. 그때 supersede 안내를 걷어내면 사용자는 표기만 통일하고 재실행하게 되고, 남은 진짜 모순의 올바른 조치는 두 번째 실행에서야 알게 된다. 두 조치는 서로를 대체하지 않는다. typed 스펙 조회의 `unicodedata.normalize("NFC", relation)` 은 `_fold` 로 표기를 통일하고, 그것이 관계 축 폴딩이 아니라 NFC 키로 저장된 스펙 dict 을 찾기 위한 조회 정규화(#210)임을 주석으로 남긴다. 그룹핑에 쓰는 관계명은 `canon` 그대로다. 범위에서 뺀 것 — 관계 축. 폴딩 자체는 기계적으로 가능하고 #210 핀도 살아남지만 (대표 복원으로 관계명이 verbatim 유지된다) 그룹핑 의미가 바뀌므로, #210 의 "no silent NFC coercion" 이 어디까지 의도한 것인지는 메인테이너 판정 사안이다. 후속 이슈로 올린다. 남는 결함 — `factlog/common.py:1277` 의 `dedup_engine_atoms` 는 원문 3튜플로 dedup 하므로 엔진은 NFC 주어와 NFD 주어를 서로 다른 개체로 `accepted.dl` 에 넣는다. 즉 이 수정 이후 체커는 엔진이 `accepted.dl` 만 보고서는 볼 수 없는 모순을 보고한다. 게이트 방향으로는 안전하나(체커가 더 엄격하다) 엔진 쪽 결함은 그대로 남는다. 테스트: 25건 추가(누계 35건). NFC-only KB 의 stderr 전문 동일성 핀은 별칭 없는 경로·별칭 있는 경로·무모순 경로·단일값 관계 미선언 조기 return 을 덮는다.
justinjoy
left a comment
There was a problem hiding this comment.
결론: CHANGE REQUEST
핵심 메커니즘은 옳고, 그 근거도 재현된다. 폴딩을 literal_types.normalize 앞에 놓아야 한다는 판단이 특히 정확하다 — normalize("amount", NFD('amount(5400,"억")'), units) → None, normalize("ordinal", NFD("제3호"), None) → None 을 확인했다. 주어 축 거짓 음성도 실재한다(main 은 NFC/NFD 로 갈린 모순에 {} 를 반환).
검증도 상당 부분 사실이다: 668 → 703 tests (+35), 핀 36건 통과, test_conflict_canonical.py/test_conflict_scalar.py diff 정확히 0줄, tests/test_conflicts.sh 30 pass, detect_conflicts 반환 형태 보존, check_conflicts.py:268-270 의 대표 키 유일성 논증도 옳다(대표는 자기 폴드 클래스의 원소이므로 서로 다른 폴드끼리 충돌할 수 없다). 뮤테이션 6종(주어 축 폴딩 제거 → 10 fail, typed 경로 객체 폴딩 제거 → 3, raw fallback 객체 폴딩 제거 → 4, _representative → min() → 7, 페어별 variant 맵 → 전역 → 1, any_mixed 트레일러 제거 → 2)도 전부 죽는다. #336 과의 머지도 clean 이고(두 PR 이 서로 겹치지 않는 영역만 건드린다) 병합 트리에서 conflict+literal 293건 전부 통과 — 머지 순서는 자유다.
대표 선택의 결정론은 반증에 실패했다 — 주어 축은 폴드 클래스당 NFC 원소가 구조적으로 최대 1개라 동률 자체가 불가능하고, 객체 축(typed 스칼라로 NFC 가 여럿 공존)에서는 사전순 min 으로 깨끗이 degrade 한다. 집합 삽입 순서 20,000회 퍼징에서 순서 의존 대표 0건. 이 부분이 이 PR 에서 가장 강하다.
그런데 본문의 대표 검증 주장 하나가 거짓이고, 그 뒤의 결함이 100% NFC KB 에 확신에 찬 거짓 진단을 출력한다. 그리고 이 PR 이 길게 논증한 "관계 축은 후속" 이라는 결론이 사실과 다르다.
Blocker
1. len(raws) > 1 을 유니코드 병합의 증거로 쓰는데, typed 스칼라 #116 병합도 같은 조건을 만족한다.
tools/check_conflicts.py:374-376:
raws = object_variants[key][obj]
if len(raws) > 1:
any_mixed = Truetyped 관계의 객체 그룹 키는 ("scalar", …) 이므로 amount(5400,"억") 과 amount(0.54,"조") 가 유니코드와 무관하게 #116 교차표기 등가로 한 그룹이 된다. 분해 코드포인트가 0개인 KB 에서 재현:
CONFLICT: single-valued '매출' on '갑사' has 2 values: amount(0.54,"조"), amount(1,"조")
value 'amount(0.54,"조")' spellings: 'amount(0.54,"조")', 'amount(5400,"억")'
...
Some string(s) above were merged across Unicode normalization forms: they render
identically but differ byte-wise ... Unify them to one form in facts/candidates.csv as well
이 문단의 모든 주장이 이 입력에 대해 거짓이다 — 두 문자열은 똑같이 렌더링되지 않고(5400억 vs 0.54조), 정규형을 가로질러 병합되지도 않았고, 수행할 "별도의 수리" 도 없다. 'amount(0.54,"조")' 은 평범한 NFC 한글을 이유 없이 이스케이프한 것이다.
차분 퍼징 c6d359d vs PR, 100% NFC 입력만 4000회 → 출력 불일치 31건(0.8%). 본문의 "NFC-only 바이트 불변, 6000회 불일치 0" 은 성립하지 않는다. 퍼저가 교차표기 등가인 두 amount 를 같은 (주어, 관계) 모순 쌍 안에 넣은 적이 없는 것으로 보인다.
이게 표기 문제에 그치지 않는 이유: finalize.py:228-231 이 이 stderr 을 그대로 전달하고, factlog 의 소비자는 대개 LLM 에이전트다. "candidates.csv 에서 표기를 통일하라" 를 읽고 존재하지 않는 정규화 결함을 찾아 나선다. #325 의 수용 기준도 무효화한다.
검증한 수정(바이트 불변 복원, 703 전건 green 유지):
if len({_fold(r) for r in raws}) < len(raws):네 부류(스칼라 병합만/유니코드 병합만/혼합/단일)를 정확히 판별하는 것을 확인했다. value … spellings: 줄에도 같은 가드가 필요하고, :386 트레일러 문구도 스칼라 병합을 포함하는 것처럼 읽히지 않게 고쳐야 한다.
2. 바이트 불변 핀이 구조적으로 실패할 수 없게 만들어져 있다.
tests/unit/test_conflict_unicode.py:391-406 의 픽스처는 amount(5400,"억")(5.4e11) 과 amount(1,"조")(1e12) 로, 의도적으로 합쳐지지 않는 값이다. 따라서 모든 객체 그룹이 단일 원소이고 len(raws) > 1 에 도달 자체가 불가능하다. 증거: 위 B1 수정을 넣어도 테스트 파일을 한 줄도 안 고치고 703 → 703 으로 통과한다. 신규 35건 중 이 동작을 어느 방향으로도 고정하는 것이 없다.
세 번째 행 amount(0.54,"조")(=5400억 과 합쳐짐)를 그 핀에 추가하고 stderr 이 여전히 2줄 baseline 인지 단언해달라. 지금은 실패하고 B1 수정 후 통과한다.
3. 관계 축은 그룹핑 축이 아니라 멤버십 게이트다 — 이 PR 이 고치는 것보다 넓은 거짓 음성을 남기면서, 잘못된 근거를 코드와 테스트에 고정한다.
:241 이 멤버십 집합을 만들고 :249 가 적용한다:
sv = {_canonicalize(r, aliases) for r in single_valued}
...
if canon not in sv: continue별칭 파일이 없으면 _canonicalize 가 관계명을 그대로 반환하고(:66-67), common.py:668-685 의 _relation_names_from 은 정책 관계명을 NFC 정규화하지 않는다. 즉 :249 는 policy/single-valued.md 텍스트와 facts/candidates.csv 텍스트의 원문 바이트 비교다. 실측:
policy_sv = {NFC("소속")}
rows = [NFD(김철수)/NFD(소속)/A사, NFD(김철수)/NFD(소속)/B사]
NFD rows vs NFC policy : {} ← 조용히 건너뜀, exit 0
NFC rows vs NFC policy : {('김철수','소속'): ['A사','B사']}
두 행의 주어 표기가 동일하므로 이건 이 PR 이 고치는 케이스가 아니다 — 균일하게 NFD 인 KB 하나면 성립하는, 더 큰 거짓 음성이다. 그리고 그건 _fold docstring 이 스스로 인용하는 시나리오다(:81-83 "macOS filesystems and IMEs routinely emit Hangul decomposed"). 이 행들은 :257 의 새 폴딩에 도달조차 하지 않는다.
따라서 :221-226 의 근거 서술이 위험을 잘못 지목한다:
The real reason is scope: folding the relation axis changes grouping semantics…
grouping 이전에 membership 을 바꾼다. 그리고 TestRelationAxisOutOfScope(tests/unit/test_conflict_unicode.py:213-238)가 이걸 가린다 — :228-230 이 single_valued={NFC("소속"), NFD("소속")} 을 넘기는데, 실제 policy/single-valued.md 가 그런 집합을 만들지 않는다. 핀이 grouping 만 훈련시키고 멤버십 우회는 보이지 않게 둔다.
수정은 2줄이고 핀 안전하다. 멤버십만 접는 패치(그룹핑 키는 그대로)를 적용해 확인했다:
sv = {_fold(_canonicalize(r, aliases)) for r in single_valued}
...
if _fold(canon) not in sv:우회가 닫히고, 관계는 여전히 원문 NFD 로 보고되므로 이 PR 자신의 test_mixed_relation_without_aliases_still_splits 와 test_nfd_relation_name_reported_verbatim 을 포함한 #210 핀이 전부 살아남으며, 전체 스위트가 PR head 와 동일하다.
최소 수용선: 2줄 멤버십 폴드를 받거나, :221-226 과 저 테스트 클래스를 "미룬 축이 이 PR 이 고치는 것보다 심한 멤버십 우회를 안고 있다" 로 정정하라. 현재 문구를 그대로 싣는 것은 잘못된 계약을 코드와 후속 이슈에 고정한다.
4. 객체 축 폴딩이 모순을 완전한 무음으로 해소하고, 그 방향의 발산 서술이 반대로 적혀 있다.
:263-264 의 if len(groups) <= 1: continue 가 variant 맵이 만들어지기 전에 쌍을 버린다:
rows = [연구소/소속/NFC(한국대학교), 연구소/소속/NFD(한국대학교)]
main : {('연구소','소속'): ['한국대학교','한국대학교']} # 조치 불가능한 거짓 양성
PR head : conflicts={} subject_variants={} object_variants={} # 어디에도 아무것도 없음
이 PR 이 세운 원칙(:367-369, "코드포인트 없는 카운트는 독자가 조치할 수 없다")이 적용돼야 하는 자리인데, 공개 장치(:270-274)는 모순이 살아남을 때만 발화한다. 폴딩이 전부 해소해버린 경우가 바로 공개가 유일한 신호인 경우다.
하류 영향이 구체적이다. finalize.py:228 이 compile 전에 게이트하므로 수정 전에는 이 KB 가 막혔는데, 수정 후에는 compile 되고 common.py:1263-1281 의 dedup_engine_atoms 가 원문 3튜플로 키를 잡는다:
relation("연구소", "소속", "한국대학교").
relation("연구소", "소속", "한국대학교").
바이트가 다르고 시각적으로 동일한 relation/3 원자 2개가 accepted.dl 에 들어간다 — dedup_engine_atoms docstring 이 막으려고 존재하는 바로 그 "부풀려진 중복 카운트" 가 finalize 정상 경로로 도달 가능해지고, 메시지는 없다.
그래서 :228-235 의 서술이 부정확하다:
For the gate that direction is safe — the checker is the stricter of the two
주어 축은 참, 객체 축은 거짓이다(체커가 더 관대해진다). 발산 공개에 이만큼 성실한 PR 이므로 이 누락은 nit 이 아니다. 객체 축이 관대해지는 것 자체는 의미론적으로 옳다 — common.py:1727-1742 의 _canonical_value(#213 초크포인트)가 이미 NFC 를 값 동등성으로 정한 것과 일치한다. 문제는 그게 무음이고 서술이 반대라는 점이다.
수정: object_variants/raw_subjects 는 이미 계산돼 있으니 len(groups) == 1 인 쌍에 대해서도 보존해 exit-0 권고로 표면화하라. NFC-only KB 는 혼용 표기가 없으므로 바이트 불변 핀은 유지된다.
5. 새 권고가 도구가 되돌리는 수리를 지시한다.
:386-394 는 "facts/candidates.csv 에서도 한 형태로 통일하라" 고 한다. 그런데 merge_candidates.py:293 은 source 컬럼만 NFC 정규화하고, :944 의 write_facts(root, rows) 는 runs/*.json 에서 새로 재구성한 목록을 쓴다 — candidates.csv 에서 되살리는 것은 status 뿐이고 그것도 원문 3튜플을 키로 한다(:866, :905, :924). 즉 주어 표기를 손으로 고치면 다음 merge_candidates 실행에서 폐기되고 NFD 원문이 돌아온다. 권고가 영원히 루프한다. 같은 메시지의 supersede 쪽 절반은 :858-860 이 명시적으로 durable 하게 만드는 것과 대조된다.
지속되는 수리는 소스를 고치고 재수집하는 것이다 — #336 이 유사한 케이스에 대해 마이그레이션 문서로 지시하는 것과 같다. 텍스트 수정이지만 이 PR 의 간판 주장이 "조치 가능성" 이다.
Should fix
-
어느 형태로 통일할지 말해주지 않는다.
:371,376이'김...', '김철수'를 NFC/NFD 라벨 없이 찍는다. 도구는 답을 이미 안다 —_representative(:109)가 NFC 원소를 골랐고 CONFLICT 줄에 이스케이프 없이 찍혀 있다. 라벨을 붙이거나 "위에 표시된 NFC 형태로 통일하라" 로 적어달라. 지금은 한국어 사용자가김철수를 손으로 디코딩해야 자기 CSV 의 어느 행을 고칠지 안다. -
skills/factlog/SKILL.md:223-232미갱신. "Contradiction detection" 절이CONFLICT줄과 supersede 수리만 서술하고 새 병합 공개와 두 번째 수리를 언급하지 않는다. 바로 그 출력의 에이전트 대면 계약이다. -
factlog status와의 발산이 양방향이 된다.cli.py:1760-1768과corroboration.py:68-73은 원문 그룹핑이다. 실측:KB factlog status게이트(PR) 주어 NFC/NFD, 객체 다름 0 conflicts 1 conflict 객체 NFC/NFD, 값 동일 1 conflict 0 conflicts 종류가 새롭진 않으니 후속으로 충분하나,
status는 사용자가finalize전에 읽는 줄이라 "status 0, finalize 실패" 는 나쁜 첫 경험이다. 이상적으로는 두 소비자가collect_conflicts를 호출하게 하는 방향.
Nit
_folddocstring(:79-81)이 "precomposed vs decomposed" 만 서술하는데 NFC 는 canonical singleton 도 접는다. 실측 병합:Ω(U+2126↔U+03A9),K(U+212A↔U+004B),Å(U+212B↔U+00C5). 유니코드가 스스로 동등하다고 정의하고 폐용 처리한 문자들이라 결함은 아니지만, "they render identically" 라는 런타임 문구가 여기서도 부정확하다.object_variants내부 dict 을 정렬 순으로 만들어달라(:271).main이conflicts의 정렬 목록을 순회하므로 출력에는 영향이 없지만, 24가지 행 순열에서 내부 dict 삽입 순서가 6가지로 갈리는 것을 측정했다 — 결정론이 전부인 모듈의 새 공개 반환값에 남는 함정이다.- 한 쌍 안에서 typed 스펙이 갈리면 출력이 퇴화한다(
:255+:271-272). 별칭 키로 keyed 된typed-relations.md로 도달 가능: main 은has 2 values: X, X, PR 은has 1 values: X. 선행 퇴화를 재형성한 것이라 이 PR 을 막진 않지만 기록이 필요하다. - 그룹핑 루프 비용이 약 2배다(20만 행: 0.332s → 0.648s NFC, 0.292s → 0.732s NFD). 행마다 주어·객체를 무조건
_fold하기 때문. 현실 KB 에서 절대 비용이 무시할 수준이라 최적화하지 말 것 — 기록만. - 본문의 "tests/*.sh 40개" 는 실제로 41개다(main, PR head 양쪽). 실질 주장(미변경)은 참이다.
- 대표가 NFC 원소가 아예 없는 그룹에서는 원문 NFD 를 반환한다. provenance 로는 방어 가능한 선택이나,
_representativedocstring 의 "grep 가능성" 근거는 전-NFD KB 에서 성립하지 않는다. 해를 끼치는 케이스는 구성하지 못했다.
확인 못 한 것
ruff clean 은 이 환경에 ruff 가 없어 어느 방향으로도 재현하지 못했다. CI 가 green 이므로 판단에 반영하지 않았다.
Closes #325.
무엇을 고쳤나
tools/check_conflicts.py의 그룹핑 키에서 주어 축과 객체 축을 NFC 로 접는다. 커밋 둘로 나눴다 — 객체 축(1/2), 주어 축(2/2). 각 커밋 시점 green.finalize게이트가 통과한다. 사람이 볼 기회가 없어진다.이슈 본문의 두 가지가 사실과 달랐다
"typed 경로는 이미 스칼라라 무관" 이 사실과 반대였다. typed 객체가 NFD 로 저술되면
literal_types.normalize가 파싱에 실패해 raw 로 degrade 하고, 태그가("scalar", …)vs("raw", …)로 갈려 폴딩이 닿지 않는다. typed 관계는 억·조·호·만 같은 한글 단위를 쓰므로 NFD 에 더 취약하다.그래서 이슈 제안의 "untyped fallback 만 폴드" 대신
_group_key진입부에서 객체를 한 번 접어normalize호출과 raw fallback 양쪽에 쓴다. 폴드 지점이 2→1 로 줄고 커버리지는 넓어진다. 전-NFD KB 에서억↔조교차표기 등가(#116)가 처음으로 작동하기 시작한다 — 회귀가 아니다."관계 축은
_canonicalize가 NFC 로 접는다" 도 조건부였다.:66의if not aliases: return relation때문에 별칭 맵이 있을 때만 접히고, 그것도 NFC 폴딩이 아니라 별칭 정준화의 부수 효과다. 별칭 없는 KB 에서 관계명을 혼용하면 같은 종류의 거짓 음성이 난다. 자세한 정정은 이슈 본문에 댓글로 남겼다.관계 축을 이번 범위에서 뺀 이유
"#210 핀 때문에 불가능" 이라는 근거는 쓰지 않는다 — 실측으로 거짓이다. 3축 전부 폴드 + 대표 복원을 적용해도
test_nfd_relation_key_preserved_when_no_aliases를 포함한 핀 36건이 그대로 통과한다(핀을 한 줄도 고치지 않고 확인).빼는 진짜 이유는 범위 분리다. 보고되는 키는 대표 복원으로 verbatim 유지되지만 그룹핑 의미가 바뀌므로, #210 이 "no silent NFC coercion" 으로 어디까지 의도했는지는 메인테이너가 판정할 사안이다. 후속 이슈로 올린다.
표시 계약
폴딩 키당 원문 대표를 유지하되, 대표 선택을 NFC 우선으로 했다. 한글에서 조합형 자모(U+1100~)가 완성형(U+AC00~)보다 항상 작아 단순
min()은 혼용 그룹에서 늘 NFD 를 고르는데, 그건 NFC 편집기에서 타이핑·붙여넣기로 검색해도 매치되지 않는 쪽이다.폴딩이 실제로 병합한 그룹은 리포트에 드러낸다:
그리고 supersede 안내는 모순이 있으면 무조건 출력한다. 혼용 표기 안내는 그 자리를 뺏지 않고 위에 추가된다 — 표기만 통일하고 재실행하게 만들면 남은 진짜 모순의 조치를 두 번째 실행에서야 알게 된다.
검증
pytest tests/unit— 690 passed / 13 skipped (baseline 655/13, 신규 35건)test_conflict_canonical.py·test_conflict_scalar.py한 줄도 안 고침(빈 diff)uvx ruff@0.15.17 check .— clean (CI 핀 버전)main()의 stdout·stderr 전문 동일성 핀 4건(별칭 없음/있음, 무모순, 단일값 미선언 조기 return) + 차분 퍼징 6000회 불일치 0tests/*.sh하니스 40개 —upstream/main과 동일남는 발산 (기록)
factlog/common.py:1277의dedup_engine_atoms는 원문 3튜플로 dedup 하므로 엔진은 NFC 주어와 NFD 주어를 다른 개체로accepted.dl에 넣는다. 즉 이 수정 이후 체커는 "엔진이accepted.dl만 보고서는 볼 수 없는 모순" 을 보고한다. 게이트 방향으로는 안전하나(체커가 더 엄격) 엔진 쪽 결함은 남는다.후속으로 분리한 것
dedup_engine_atoms의 원문 3튜플 deduptools/corroboration.py의 경쟁 버킷이 세 축 전부 원문 키