Skip to content

fix(reverse_sync): ac:image 포함 리스트에서 inline bold 경계 변경을 올바르게 적용합니다#903

Draft
jk-kim0 wants to merge 7 commits intomainfrom
jk/fix-reverse-sync-accuracy-and-verifier
Draft

fix(reverse_sync): ac:image 포함 리스트에서 inline bold 경계 변경을 올바르게 적용합니다#903
jk-kim0 wants to merge 7 commits intomainfrom
jk/fix-reverse-sync-accuracy-and-verifier

Conversation

@jk-kim0
Copy link
Contributor

@jk-kim0 jk-kim0 commented Mar 11, 2026

수정한 버그

failure_type 13 — Bold 경계 침범 (테스트케이스: 544377652)

**Scopes**: API 토큰으로... 형태에서 : API 토큰으로...<strong> 안으로 흡수되어
**Scopes: API 토큰으로...** 로 변환되는 문제를 수정합니다.

근본 원인

list_patcher._regenerate_list_from_parent는 parent XHTML에 <ac:image> 가 있으면
text_transfer 폴백을 사용했습니다. text_transfer는 텍스트 내용 변경은 처리하지만,
<strong> 태그의 닫힘 위치(bold 경계) 이동과 같은 구조적 변경은 처리할 수 없습니다.

변경 내용

  • list_patcher.py: _regenerate_preserving_lossy_items 추가
    • top-level <li> 항목 수가 같을 때 선택적 재생성 시도
    • lossy 요소(<ac:image>, <span style=)가 없는 항목 → mdx_block_to_inner_xhtml로 재생성 (bold 경계 수정됨)
    • lossy 요소가 있는 항목 + 텍스트 미변경 → old XHTML 보존 (<ac:image> 유지)
    • lossy 요소가 있는 항목 + 텍스트 변경 → None 반환 → 기존 text_transfer 폴백 유지
  • tests/run-tests.sh: --xhtml--page-dir 수정 (PR confluence-mdx: sidecar mapping을 텍스트 비교에서 타입 기반 순차 정렬로 교체합니다 #901 CLI 변경 대응)
  • tests/test_reverse_sync_list_patcher_image_preserve.py: 단위 테스트 추가
  • tests/reverse-sync/pages.yaml: 544377652 expected_status fail → pass 갱신

참고: 이 PR은 #901 (sidecar type-based mapping 교체)의 커밋을 포함합니다.
#901 머지 후 이 PR의 diff가 위 변경 내용만 남게 됩니다.


코드 리뷰

Critical

1. reverse_sync_cli.py: SidecarEntry 역직렬화 로직 중복

run_verifySidecarEntry / SidecarChildEntry 구성 코드를 직접 구현하고 있어
load_sidecar_mapping과 동일한 로직이 두 곳에 존재합니다.

# reverse_sync_cli.py:319–341 — load_sidecar_mapping과 동일한 코드 중복
sidecar_entries = []
for item in sidecar_data.get('mappings', []):
    children = [SidecarChildEntry(...) for ch in item.get('children', [])]
    sidecar_entries.append(SidecarEntry(..., children=children))

v4로 스키마가 바뀔 때 두 곳을 동시에 수정해야 하며, 한 곳을 빠뜨리면 조용히 오작동합니다.
load_sidecar_mapping은 이미 mapping.yaml path를 받는 형태이므로,
generate_sidecar_mapping 결과를 파일로 쓴 뒤 load_sidecar_mapping으로 읽도록 리팩토링하거나,
yaml_str → List[SidecarEntry] 파싱 함수를 공유하는 방향이 적절합니다.

2. roundtrip_verifier.py: _normalize_blank_line_after_blockquote가 strict 모드에 포함됨

_apply_minimal_normalizations는 strict/lenient 공통입니다. 그런데 새로 추가된
_normalize_blank_line_after_blockquote는 "forward converter의 체계적 출력 특성"이 아니라
"forward converter가 가끔 빈 줄을 추가한다"는 관찰 기반 보정입니다.

# roundtrip_verifier.py:91
text = _normalize_blank_line_after_blockquote(text)  # strict 모드에도 적용됨

strict 모드에서 blockquote 다음 빈 줄의 존재/부재 차이를 PASS로 처리하면,
forward converter의 blockquote 처리 버그를 역직렬화 검증이 감추게 됩니다.
lenient 모드 전용(_apply_normalizations) 또는 별도 플래그로 분리를 검토하세요.


Warning

3. list_patcher.py: new_lis 추출이 mdx_block_to_inner_xhtml 구현에 암묵적으로 의존

# list_patcher.py:114–115
new_soup = BeautifulSoup(new_inner, 'html.parser')
new_lis = [c for c in new_soup.children if hasattr(c, 'name') and c.name == 'li']

현재 mdx_block_to_inner_xhtml이 list에 대해 <li>...</li><li>...</li>(래퍼 없음)를 반환하기 때문에 동작합니다.
반면 old_lisold_list_elem.find_all('li', recursive=False)<ol>/<ul> 아래에서 추출합니다.
두 방식의 비대칭성이 문서화되지 않았으며, mdx_block_to_inner_xhtml<ol><li>...</li></ol> 형태로 변경되면
new_lis가 항상 empty → len(old_lis) != len(new_lis) → 항상 None 반환 → 이 PR의 수정이 silent regression됩니다.

적어도 함수 docstring에 이 가정을 명시하거나, new_soup.find(['ol', 'ul']) fallback을 추가하는 것이 안전합니다.

4. sidecar.py: H1 skip이 while loop — 복수 H1을 모두 건너뜀

# sidecar.py:~463
while (mdx_ptr < len(mdx_content_indexed)
       and mdx_content_indexed[mdx_ptr][1].type == 'heading'
       and mdx_content_indexed[mdx_ptr][1].content.startswith('# ')):
    mdx_ptr += 1

주석("MDX 첫 줄에 # <페이지 제목>을 자동 생성")은 하나만 건너뛰는 의도인데,
while이므로 문서 시작에 H1이 두 개 이상 있으면 모두 skip됩니다.
if로 바꾸거나 mdx_ptr += 1; break 형태가 의도에 더 부합합니다.

5. sidecar.py: SidecarChildEntry 클래스가 사용처(_align_children) 이후에 정의됨

# sidecar.py:280 — _align_children은 List[Dict] 반환
def _align_children(...) -> List[Dict]:
    ...
    child_entries.append({'xhtml_xpath': ..., 'xhtml_block_id': ..., ...})

# sidecar.py:328 — 클래스 정의가 함수 아래에 위치
@dataclass
class SidecarChildEntry: ...

_align_children이 반환하는 dict는 SidecarChildEntry의 필드와 1:1 대응하지만,
반환 타입이 List[Dict]로 선언되어 있어 정적 분석 도구가 연결을 파악할 수 없습니다.
SidecarChildEntry_align_children 앞으로 이동하고 반환 타입을 List[SidecarChildEntry]로 변경하면
가독성과 안전성이 개선됩니다.


Suggestion

6. list_patcher.py: build_list_item_patches 루프 의도가 불명확

for old_item, new_item in zip(old_items, new_items):
    if old_item != new_item:
        return _regenerate_list_from_parent(...)

return []

실질적으로 "항목 중 하나라도 변경되면 전체 재생성"이지만,
루프 형태가 "항목별로 처리한다"는 오해를 유발합니다.
현재 로직은 if old_items != new_items: return _regenerate_list_from_parent(...) 와 동치이므로,
단순화하거나 주석으로 의도를 명시하는 것이 좋습니다.

7. list_patcher.py: _split_top_level_items — 빈 줄 처리 방식 비직관적

빈 줄을 item 분리자로 사용하기 때문에,
multi-paragraph list item(\n\n 포함)이 있으면 count가 실제 <li> 수보다 많아집니다.
이는 상위의 len(old_top_items) != len(old_lis) 가드가 방어하므로 기능적 결함은 아니지만,
함수 명세에 "빈 줄은 항목 분리자로 처리되며, 항목 내 빈 줄이 있으면 count 불일치로 폴백된다"를
명시하면 혼란을 줄일 수 있습니다.

@vercel
Copy link

vercel bot commented Mar 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
querypie-docs Ready Ready Preview, Comment Mar 12, 2026 10:37am

Request Review

@jk-kim0 jk-kim0 force-pushed the jk/fix-reverse-sync-accuracy-and-verifier branch from 5c8d108 to 44c5d95 Compare March 12, 2026 06:01
@jk-kim0 jk-kim0 changed the title confluence-mdx: reverse-sync 정확도 개선 및 roundtrip_verifier 버그 수정 fix(reverse_sync): 헤딩 공백 차이만 있는 변경을 XHTML에 전파하지 않습니다 Mar 12, 2026
@jk-kim0 jk-kim0 force-pushed the jk/fix-reverse-sync-accuracy-and-verifier branch from 44c5d95 to fb7283f Compare March 12, 2026 06:38
@jk-kim0 jk-kim0 self-assigned this Mar 12, 2026
@jk-kim0 jk-kim0 marked this pull request as draft March 12, 2026 06:46
@jk-kim0 jk-kim0 force-pushed the jk/fix-reverse-sync-accuracy-and-verifier branch from fb7283f to af61903 Compare March 12, 2026 07:06
@jk-kim0 jk-kim0 changed the title fix(reverse_sync): 헤딩 공백 차이만 있는 변경을 XHTML에 전파하지 않습니다 WIP: reverse-sync 디버깅 작업 브랜치 Mar 12, 2026
@jk-kim0 jk-kim0 changed the base branch from main to jk/feat-sidecar-type-based-core March 12, 2026 07:06
@jk-kim0 jk-kim0 force-pushed the jk/fix-reverse-sync-accuracy-and-verifier branch from af61903 to 7ccbd21 Compare March 12, 2026 08:00
@jk-kim0 jk-kim0 changed the title WIP: reverse-sync 디버깅 작업 브랜치 WIP: 디버깅용 브랜치 Mar 12, 2026
@jk-kim0 jk-kim0 changed the title WIP: 디버깅용 브랜치 fix(reverse_sync): ac:image 포함 리스트에서 inline bold 경계 변경을 올바르게 적용합니다 Mar 12, 2026
jk-kim0 and others added 7 commits March 12, 2026 19:33
mapping.yaml v3 스키마를 도입하고, generate_sidecar_mapping()을
텍스트 유사도 매칭 대신 XHTML 블록 타입과 MDX 블록 타입의
호환성(_TYPE_COMPAT 테이블)을 기반으로 한 two-pointer 순차 정렬로
교체합니다.

주요 변경 사항:
- _TYPE_COMPAT: XHTML → MDX 블록 타입 호환성 매핑 테이블 추가
- _SKIP_MACROS: TOC/children 매크로 스킵 처리 추가
- SidecarChildEntry 데이터클래스: 자식 블록 정렬 정보 저장
- SidecarEntry: mdx_line_start, mdx_line_end, children 필드 추가
- generate_sidecar_mapping(): 타입 기반 두 포인터 알고리즘으로 재작성
- load_sidecar_mapping(): v3 스키마 필드 읽기 지원 추가
- 빈 단락(paragraph) XHTML 블록은 MDX 콘텐츠 블록 없음으로 처리
- 테스트 업데이트: version 2→3, Callout MDX 형식, mdx_line_start 검증

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
generate_sidecar_mapping()에서 MDX의 첫 번째 H1 헤딩(# 로 시작)은
Confluence XHTML 본문에 대응하지 않는 페이지 제목이므로, two-pointer
루프 시작 전에 건너뛰도록 예외 처리합니다.

이로 인해 사이드카 정렬이 모든 케이스에서 올바르게 동작하며,
integration test 16/16 통과 (기존 12/16 → 16/16).

544112828 expected 파일을 새 알고리즘 출력으로 갱신합니다.
(verification exact_match: true, 기존 expected는 구버전 알고리즘 기준)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- reverse_sync_cli.py: SidecarEntry 생성 시 children(SidecarChildEntry) 로드 추가
- patch_builder.py: _resolve_mapping_for_change() 단순화
  - callout 블록 조기 반환(containing 전략) 추가
  - mapping is None 시 _find_containing_mapping() 폴백 제거
  - 텍스트 불일치 시 _find_containing_mapping() 재매핑 제거
  - _resolve_child_mapping() import 복원(Phase 3 삭제 예정)
- list_patcher.py: build_list_item_patches()에서 _find_containing_mapping() 폴백 제거
- 테스트: 텍스트 폴백 제거에 따른 기대값 업데이트(skip/0 patches)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- sidecar.py: _find_text_match(), _count_child_mdx_blocks(), _strip_all_ws() 삭제
- patch_builder.py: _find_containing_mapping(), _strip_block_markers() 삭제
  - _resolve_mapping_for_change() 서명에서 id_to_mapping 파라미터 제거
  - mapping.children 존재 시 containing 전략으로 단순화
- list_patcher.py: _resolve_child_mapping() 삭제
  - build_list_item_patches() 단순화: 항목 변경 시 전체 리스트 재생성
  - 미사용 import 제거 (collapse_ws, convert_inline, strip_list_marker, strip_for_compare)
- 테스트: 삭제된 함수 테스트 클래스 제거 및 기대값 업데이트

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- `reverse_sync_cli.py`: 패치된 XHTML의 forward 변환 전, `page.v1.yaml`을
  `var/<page_id>/`로 복사합니다. Forward converter가 크로스 페이지 링크 해석 시
  `page.v1.yaml`을 XHTML과 같은 디렉터리에서 읽으므로, 이를 보장합니다.
- `roundtrip_verifier.py`: `_normalize_blank_line_after_blockquote()` 추가 —
  blockquote(`>`) 줄 바로 다음의 단일 빈 줄을 제거합니다. Forward converter가
  blockquote 뒤에 빈 줄을 체계적으로 추가하므로, 비교 시 이를 정규화합니다.

- 1454342158: `#unexpected-failure` 링크 → 정상 크로스 페이지 링크로 수정
- 1907294209: blockquote 뒤 빈 줄 차이 제거 → PASS

- feature/type-based-sidecar-mapping

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
list_patcher의 _regenerate_list_from_parent에서 parent XHTML에 <ac:image>가 있을 때
text_transfer로만 폴백하던 로직을 개선합니다.

변경 내용:
- _regenerate_preserving_lossy_items 함수 추가: top-level <li> 항목 수가 같을 때
  선택적 재생성을 시도합니다.
  - lossy 요소(ac:image, span style)가 없는 항목은 mdx_block_to_inner_xhtml로 재생성
    → inline bold/italic 경계 변경이 올바르게 적용됨
  - lossy 요소가 있는 항목은 텍스트 변경이 없으면 old XHTML에서 그대로 보존
  - lossy 항목의 텍스트도 변경된 경우 None 반환 → text_transfer 폴백 유지
- 테스트 추가: test_reverse_sync_list_patcher_image_preserve.py
- tests/run-tests.sh: --xhtml → --page-dir 수정 (PR #901 CLI 변경 대응)
- pages.yaml: 544377652 expected_status fail → pass 갱신

수정된 버그:
- failure_type 13: **Scopes**: → **Scopes: 텍스트가 Bold 안으로 흡수 (544377652)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@jk-kim0 jk-kim0 force-pushed the jk/fix-reverse-sync-accuracy-and-verifier branch from 312aa1a to 0e6e4d5 Compare March 12, 2026 10:33
@jk-kim0 jk-kim0 changed the base branch from jk/feat-sidecar-type-based-core to main March 12, 2026 10:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant