diff --git a/confluence-mdx/docs/plans/2026-03-13-reverse-sync-reconstruction-design-review.md b/confluence-mdx/docs/plans/2026-03-13-reverse-sync-reconstruction-design-review.md
new file mode 100644
index 000000000..a8b02493e
--- /dev/null
+++ b/confluence-mdx/docs/plans/2026-03-13-reverse-sync-reconstruction-design-review.md
@@ -0,0 +1,493 @@
+# Reverse Sync 전면 재구성 설계 — 검토 평가 결과 (v5)
+
+> 검토 대상: `2026-03-13-reverse-sync-reconstruction-design.md`
+> 검토일: 2026-03-13
+> 검토 기준 버전: PR #913 head `33fa095e56cb26766995b3930d3616a58559685e`
+> 검토 관점: 설계 타당성, 코드베이스 정합성, TDD 관점의 테스트 확보 가능성
+
+---
+
+## 결론
+
+문서가 제시하는 **큰 방향**은 타당하다.
+
+- patch heuristic을 계속 누적하는 대신 MDX → XHTML 재구성 경로로 수렴시키려는 방향은 맞다
+- list를 sidecar flat entry + `children` ref 구조로 재설계한 점도 기존 `list_items` 계열 설계보다 낫다
+- callout 내부를 paragraph fallback이 아니라 재귀 파싱으로 처리하겠다는 판단도 적절하다
+
+하지만 현재 문서는 **최종 설계 승인 가능 상태는 아니다**.
+
+핵심 이유는 두 가지다.
+
+1. paragraph 재구성의 핵심 불변식 하나가 현재 코드베이스 기준으로 성립하지 않는다
+2. 테스트 설계가 일부 레벨에서 실제 fixture/loader/API와 맞지 않아, TDD 게이트로 바로 작동하지 않는다
+
+즉, 방향은 맞지만 아직 "구현 전에 바로 들어가도 되는 설계" 수준은 아니다.
+
+---
+
+## 주요 지적사항
+
+### C-1. ParagraphEditSequence의 핵심 불변식이 현재 설계대로는 성립하지 않는다
+
+**심각도:** Critical
+
+**문서 위치:**
+
+- Section 3.1.1 `_process_paragraph()`
+- Section 3.1.1 `reconstruct_paragraph()`
+
+**문서의 주장:**
+
+- XHTML 조각을 누적한 뒤 `convert_inline()`을 적용하면 `TextSegment.text`가 MDX 텍스트 조각이 된다
+- 따라서 `TextSegments`를 이어 붙인 값이 `old_mdx_text`와 정확히 일치한다
+- 그 결과 `old_text == old_mdx_text`를 강한 불변식으로 둘 수 있다
+
+**문제:**
+
+현재 코드베이스의 `convert_inline()`은 **MDX → XHTML 변환기**다. XHTML 조각을 넣었을 때 XHTML을 MDX로 역변환해주지 않는다.
+
+예를 들어:
+
+```python
+convert_inline("bold") == "bold"
+```
+
+즉 `_process_paragraph()`가 아래처럼 동작하면:
+
+```python
+cursor_xhtml += str(child) # "bold"
+children.append({
+ "kind": "text",
+ "text": convert_inline(cursor_xhtml),
+})
+```
+
+`TextSegment.text`는 `"**bold**"`가 아니라 `"bold"`로 남는다.
+
+그러면 문서가 전제한:
+
+```python
+old_text == old_mdx_text
+```
+
+는 서식이 포함된 paragraph에서 곧바로 깨진다.
+
+**왜 중요한가:**
+
+이 불변식이 깨지면 `map_anchor_positions()` 이전 단계에서 좌표계가 이미 무너진다. 즉 paragraph 설계의 중심축이 성립하지 않는다.
+
+**필요한 수정 방향:**
+
+둘 중 하나를 명확히 선택해야 한다.
+
+1. `TextSegment.text`를 "MDX 텍스트"가 아니라 "XHTML/normalized plain 기준 텍스트"로 바꾸고, `reconstruct_paragraph()` 비교식도 그 좌표계에 맞게 다시 설계한다
+2. XHTML inline fragment를 실제로 MDX inline text로 역변환하는 별도 함수/규칙을 설계하고, 그 함수의 지원 범위를 테스트로 고정한다
+
+현재 문서는 2번을 이미 해결된 것처럼 쓰고 있지만, 실제로는 해결되지 않았다.
+
+---
+
+### C-2. Level 1 / Level 2 테스트의 oracle 정의가 현재 fixture와 맞지 않는다
+
+**심각도:** High
+
+**문서 위치:**
+
+- Section 8.4 Level 1
+- Section 8.5 Level 2
+
+**문서의 주장:**
+
+- `mapping.yaml`에서 각 블록의 `xhtml_text`를 읽어 재구성 결과와 비교할 수 있다
+- `load_mapping()`으로 이를 로드할 수 있다
+
+**문제:**
+
+현재 실제 `mapping.yaml`은 `xhtml_xpath`, `xhtml_type`, `mdx_blocks` 중심이며, 문서가 예제로 사용하는 `entry.xhtml_text`는 존재하지 않는다. 실제 loader인 `load_sidecar_mapping()`도 그 필드를 읽지 않는다.
+
+즉 문서의 예제:
+
+```python
+mapping = load_mapping(...)
+assert normalize_xhtml(reconstructed) == normalize_xhtml(entry.xhtml_text)
+```
+
+는 현재 리포지토리 기준으로 성립하지 않는다.
+
+**왜 중요한가:**
+
+TDD에서 가장 중요한 것은 "무엇을 expected로 비교할 것인가"다. 그런데 Level 1/2의 expected source가 현재 정의되지 않았다.
+
+이 상태에서는:
+
+- 테스트 파일 이름은 정할 수 있어도
+- 실제 assertion이 무엇을 비교해야 하는지
+- 어느 loader를 쓸지
+- 원본 fragment를 어디서 읽어올지
+
+가 정해지지 않은 셈이다.
+
+**필요한 수정 방향:**
+
+둘 중 하나를 선택해야 한다.
+
+1. `mapping.yaml` 스키마를 확장해 테스트 oracle로 쓸 원본 XHTML fragment를 명시적으로 넣는다
+2. Level 1/2의 oracle을 `mapping.yaml`이 아니라 `page.xhtml` 또는 `expected.roundtrip.json`에서 xpath 기반으로 추출하는 방식으로 다시 설계한다
+
+현재 리포지토리 자산을 고려하면 2번이 더 현실적이다.
+
+---
+
+### C-3. `normalize_xhtml()` 설계가 현재 저장소 전제와 맞지 않는다
+
+**심각도:** High
+
+**문서 위치:**
+
+- Section 8.4 `normalize_xhtml()`
+
+**문서의 주장:**
+
+- `lxml.etree.fromstring()`으로 fragment를 파싱해 정규화한다
+
+**문제 1: 의존성 누락**
+
+현재 저장소의 `requirements.txt`에는 `lxml`이 없다.
+
+**문제 2: Confluence fragment 파싱 전제 미정의**
+
+문서의 테스트 대상에는 `ac:image`, `ri:attachment` 같은 namespace prefix가 포함된 XHTML fragment가 많다. 이들은 namespace 선언 없이 XML parser에 그대로 넣으면 실패할 가능성이 높다.
+
+이는 단순 구현 디테일이 아니라, Level 1/2 비교 함수가 실제 테스트 데이터에 적용 가능한지 여부를 가르는 전제다.
+
+**왜 중요한가:**
+
+비교 함수가 실제 fragment를 파싱하지 못하면 Level 1/2는 시작 자체가 안 된다.
+
+**필요한 수정 방향:**
+
+다음 중 하나를 문서에 명시해야 한다.
+
+1. `lxml`을 새 테스트 의존성으로 도입하고, Confluence namespace wrapper를 어떻게 붙일지 명확히 규정한다
+2. XML 정규화 대신 BeautifulSoup 기반 canonicalization 혹은 byte/string comparison 규칙으로 축소한다
+
+현재 문서는 이 전제를 해결하지 않은 채 테스트가 가능한 것처럼 서술하고 있다.
+
+---
+
+## Warning
+
+### W-1. Level 3의 `mdx_content_hash` 단독 매칭은 중복 content 페이지에서 오검증 위험이 있다
+
+**문서 위치:**
+
+- Section 8.6 Level 3
+
+문서는 아래 방식으로 MDX 블록을 찾는다.
+
+```python
+hash_to_block = {sha256_text(b.content): b for b in mdx_blocks if b.content}
+mdx_block = hash_to_block.get(sb.mdx_content_hash)
+```
+
+하지만 실제 testcase에는 **동일한 non-empty content를 가진 블록이 반복되는 페이지가 이미 존재한다**.
+
+예:
+
+- `tests/testcases/1454342158`
+- `tests/testcases/544375741`
+- `tests/testcases/544145591`
+
+이 경우 dict comprehension은 마지막 블록으로 덮어쓰므로, 다른 위치의 동일 content 블록과 잘못 매칭될 수 있다.
+
+**영향:**
+
+- 테스트가 실패해야 할 케이스를 통과시키거나
+- 반대로 맞는 구현을 오검증으로 실패시킬 수 있다
+
+**권장 수정:**
+
+- key를 `mdx_content_hash` 단독이 아니라 `(mdx_content_hash, mdx_line_range)` 또는 "hash -> list of candidate blocks"로 바꾸고
+- sidecar block의 순서나 line range를 함께 사용해 disambiguation 하도록 문서화해야 한다
+
+---
+
+### W-2. 테스트 실행 순서가 Level 0를 건너뛰고 있어 TDD 루프가 약하다
+
+**문서 위치:**
+
+- Section 8.3
+- Section 8.8
+
+문서는 테스트 수준 구조에서 Level 0를 가장 먼저 둔다.
+
+```text
+Level 0 -> Level 1 -> Level 2 -> Level 3 -> Level 4
+```
+
+그런데 실제 실행 순서 섹션은 Step 1을 Level 1부터 시작한다.
+
+이렇게 되면 helper 단위의 red/green이 빠지고, 실패 원인이:
+
+- helper 버그인지
+- block renderer 버그인지
+- document assembly 버그인지
+
+를 뒤늦게 분리하게 된다.
+
+**권장 수정:**
+
+Section 8.8의 Step 1은 Level 0여야 한다.
+
+```bash
+python3 -m pytest tests/test_reconstruction_helpers.py -v --tb=short
+```
+
+그 다음에 Level 1로 넘어가야 문서가 말하는 TDD 순서와 실제 실행 절차가 일치한다.
+
+---
+
+### W-3. "새로운 테스트 입력 파일이 필요 없다"는 표현은 과도하다
+
+**문서 위치:**
+
+- Section 8.1
+
+기존 testcase를 최대한 재사용하겠다는 방향은 좋다. 다만 현재 확인된 공백을 메우려면 최소한 다음 중 일부는 새 fixture 또는 기존 fixture 파생 샘플이 필요할 가능성이 높다.
+
+- formatted paragraph + inline image 혼합 unit fixture
+- namespace-bearing XHTML fragment normalization fixture
+- duplicate-content Level 3 disambiguation fixture
+
+즉 "대부분 기존 fixture 재사용"은 맞지만, "새로운 테스트 입력 파일이 전혀 필요 없다"는 문장은 너무 강하다.
+
+---
+
+## Suggestion
+
+### S-1. Section 8을 "설계 검증 테스트"와 "회귀 방지 테스트"로 분리하면 더 명확하다
+
+현재 Section 8은 unit/integration/e2e를 모두 포함하지만, 성격이 다른 두 가지 테스트가 섞여 있다.
+
+- 설계 자체의 타당성을 입증하는 테스트
+- 구현 후 회귀를 막는 테스트
+
+다음을 분리하면 읽는 사람이 덜 헷갈린다.
+
+- Part A: 설계 검증 테스트
+ - ParagraphEditSequence
+ - list children ref
+ - callout recursive parsing
+- Part B: 회귀 방지 테스트
+ - reconstruction coverage
+ - lossless fragment compare
+ - reverse-sync-verify
+
+---
+
+### S-2. 승인 기준을 "Phase 진입 가능"과 "구현 완료 가능"으로 나누는 편이 낫다
+
+현재 문서는 구현 계획과 테스트 계획은 자세하지만, "지금 당장 Phase 1에 착수 가능한가"를 가르는 gate가 약하다.
+
+다음처럼 나누면 더 실용적이다.
+
+- Phase 1 착수 전 필수 해소
+ - C-1 paragraph invariant
+ - C-2 Level 1/2 oracle
+ - C-3 normalization strategy
+- Phase 3 머지 전 필수 해소
+ - W-1 Level 3 duplicate hash
+ - W-2 실행 순서 정합성
+
+---
+
+## TDD 관점 평가
+
+### 좋은 점
+
+- 실제 testcase 기반 설계 원칙을 명시한 점은 좋다
+- helper → block → document → byte-equal → E2E로 내려가는 계층적 테스트 구조는 적절하다
+- E2E `reverse-sync-verify`를 최종 회귀 게이트로 유지한 판단도 맞다
+
+### 부족한 점
+
+- 가장 위험한 설계 가정(paragraph 좌표계)이 unit red test로 먼저 고정되어 있지 않다
+- Level 1/2 expected source가 불명확해 테스트를 바로 쓸 수 없다
+- Level 3 block identity가 불안정해 "pass = correct"라는 신뢰를 주지 못한다
+
+### 현재 판단
+
+**"문제 해결을 위해 충분한 테스트케이스를 확보하는 방안이 도출되어 있는가?"**라는 질문에 대한 답은:
+
+**아직 충분하지 않다.**
+
+테스트 레벨의 개수는 충분하지만, 최소 3개의 핵심 전제가 미정이다.
+
+1. paragraph sidecar의 좌표계
+2. Level 1/2의 oracle source
+3. Level 3의 block identity
+
+이 셋이 해결되어야 비로소 TDD 계획이 실제 문제 해결을 보장하는 체계가 된다.
+
+---
+
+## 권장 후속 조치
+
+### 1. 설계 문서 우선 수정
+
+다음 항목을 먼저 문서에서 확정해야 한다.
+
+- paragraph `TextSegment.text`의 기준 좌표계
+- `normalize_xhtml()` 구현 전략 또는 대체 비교 전략
+- Level 1/2에서 원본 fragment를 어디서 읽을지
+- Level 3에서 duplicate content를 어떻게 disambiguate할지
+
+### 2. TDD 진입용 최소 red test 정의
+
+구현 전에 아래 4개를 먼저 failing test로 고정하는 것이 좋다.
+
+1. formatted paragraph + inline image
+2. nested list + `inline_trailing_html`
+3. callout + nested list + code block
+4. duplicate MDX content page에서 Level 3 fragment identity 유지
+
+### 3. 승인 기준 재정의
+
+현재 문서는 "방향은 맞음" 수준이다.
+
+다음 기준을 만족하면 구현 착수 가능으로 볼 수 있다.
+
+- C-1, C-2, C-3 해소
+- Level 0를 포함한 실행 순서 재정의
+- Level 3 identity 전략 확정
+
+---
+
+## 평가 요약
+
+| 항목 | 평가 |
+|------|------|
+| 재구성 중심 방향성 | ✅ 적절 |
+| list flat mapping + `children` ref | ✅ 적절 |
+| callout 재귀 파싱 방향 | ✅ 적절 |
+| paragraph 설계 완결성 | ❌ Critical |
+| Level 1/2 테스트 oracle 정의 | ❌ High |
+| XHTML normalization 전략 | ❌ High |
+| Level 3 hash 기반 block 식별 | ⚠️ Warning |
+| TDD 실행 순서 일관성 | ⚠️ Warning |
+| "기존 fixture만으로 충분" 주장 | ⚠️ Warning |
+
+최종 판단:
+
+- **설계 방향:** 승인 가능
+- **설계 문서의 현재 완성도:** 수정 필요
+- **TDD 관점의 테스트 확보 방안:** 보강 필요
+
+---
+
+## 설계 검증 (Claude Code 검토, 2026-03-13)
+
+### 검증 요약
+
+`2026-03-13-reverse-sync-reconstruction-design.md`는 v5 리뷰에서 지적된 세 가지 Critical/High 이슈(paragraph 좌표계 혼란, oracle 출처 불명, lxml 의존성)를 모두 해소하고 구현 착수 가능한 수준으로 재작성됐다. 현재 코드베이스와 정합성도 높다. 다만 설계 내부에 검증이 필요한 가정 몇 가지가 남아 있으며, Phase 3~4에서 현실적 어려움이 예상된다.
+
+---
+
+### 1. 문제 진단의 정확성
+
+코드베이스와 대조한 결과, 설계 문서의 현재 시스템 문제 진단은 실제 코드와 일치한다.
+
+**정확한 진단:**
+
+- `patch_builder.py`의 전략 분기 누적 (`direct` / `containing` / `list` / `table` / `skip`) 및 각 전략마다 별도 fallback이 실제로 존재한다 (patch_builder.py:88-156).
+- `transfer_text_changes()` 기반 수정이 중심 경로임을 확인했다. `containing` 전략과 delete+add 쌍 처리 모두 이 함수에 의존한다 (patch_builder.py:76, 221).
+- Confluence 전용 요소(``, `` 등)를 발견하면 재생성 대신 text transfer로 폴백하는 코드가 명시적으로 존재한다 (patch_builder.py:300-309).
+- `sidecar.py`의 `SidecarBlock`에 `reconstruction` 필드가 없음을 확인했다. 설계 문서의 "현재 schema v2에 reconstruction metadata가 없다"는 진단이 정확하다.
+
+**추가로 확인한 사항:**
+
+`generate_sidecar_mapping()`(sidecar.py:306)의 4차 prefix 매칭(`[:20]`)은 text similarity가 낮은 경우 false positive를 낼 수 있다. PR 이력(#853)에서 이미 버그가 발생한 패턴이며, 설계 문서가 이 경로를 "더 이상 중심축이 되어서는 안 된다"고 명시한 것은 정확하다.
+
+---
+
+### 2. 목표 달성 가능성
+
+**달성 가능한 목표:**
+
+설계의 세 핵심 전환은 코드베이스 자산을 기반으로 달성 가능하다.
+
+- `convert_inline()` 역변환 가정 제거 → 설계가 명시적으로 "기준 좌표계는 XHTML DOM에서 추출한 normalized plain text"로 전환했다 (§3.1). 현재 `mapping_recorder.py`가 이미 `get_text()` 기반 plain text를 생성하므로 좌표계 기반은 존재한다.
+- `expected.roundtrip.json`을 primary oracle로 승격 → 현재 모든 21개 testcase에 `xhtml_fragment` 필드가 존재함을 직접 확인했다. oracle 전환의 전제가 충족된다.
+- BeautifulSoup 기반 normalizer 구축 → `mapping_recorder.py`, `xhtml_patcher.py`, `xhtml_beautify_diff.py`가 이미 BeautifulSoup을 사용한다. 공용 `xhtml_normalizer.py`로 통합하는 것은 현실적이다.
+
+**달성이 불확실한 목표:**
+
+- **paragraph + inline anchor 재주입 (§5.4)**: `old_plain_offset` 기반 offset mapping이 핵심이다. anchor가 두 개 이상이거나, 변경으로 인해 앞 anchor의 offset이 뒤 anchor에 영향을 줄 때의 순서 보장 로직이 설계에 명시되지 않았다. 구현 시 edge case가 될 가능성이 높다.
+- **list reconstruction의 zip 매칭 (§5.5)**: "sidecar list item sequence와 index 기반 zip"은 MDX와 XHTML의 list item 수가 다를 때(항목 추가/삭제) 어떻게 처리할지 명확하지 않다. 설계가 이 케이스를 암묵적으로 "child slot 수 불일치 → fail"로 처리한다면 실용적 커버리지가 낮아질 수 있다.
+
+---
+
+### 3. 아키텍처 설계의 적절성
+
+**적절한 설계 결정:**
+
+- `replace_fragment` 액션을 `xhtml_patcher.py`에 추가하는 접근은 자연스럽다. 기존 `insert` / `delete` 패턴과 일관되고, DOM 전체 교체라는 의미도 명확하다.
+- `reconstruction_planner.py`를 분리하고 `patch_builder.py`를 thin orchestration layer로 만드는 방향은 현재 `patch_builder.py`의 1개 함수가 전략 분기 + fallback + 그룹화를 모두 담당하는 문제를 직접 해결한다.
+- block identity에 `hash + line_range + order`를 함께 사용하는 방식은 v5 리뷰의 W-1 지적을 정확히 해소한다.
+- `SidecarBlock`에 `reconstruction` 필드를 추가하는 스키마 v3 설계는 테스트 oracle과 runtime metadata를 같은 artifact에 담는 좋은 설계다. 현재 `SidecarBlock.lost_info`가 비슷한 패턴으로 이미 존재하므로 확장이 자연스럽다.
+
+**검토가 필요한 설계 결정:**
+
+- **callout outer wrapper 보존 (§5.6)**: "outer wrapper 보존은 `lost_info_patcher`가 아니라 reconstruction metadata가 책임진다"는 원칙이 맞지만, 현재 `lost_info_patcher.py`가 `_STRUCTURED_MACRO_RE`로 callout macro를 처리하는 코드가 이미 있다. 두 책임 분리 경계를 구현 시 명확히 해야 한다.
+- **Opaque block의 fail-closed 정책 (§5.3-D)**: `UnsupportedReconstructionError`로 명시적 실패하는 방향은 올바르지만, 현재 `build_patches()`가 mapping을 찾지 못하면 `skip`으로 조용히 통과한다. fail-closed로 전환하면 현재 pass하던 케이스 일부가 fail로 바뀔 수 있어 Phase 5 전환 시 회귀 위험이 있다.
+
+---
+
+### 4. 위험 요소 및 미검토 에지케이스
+
+**설계에서 명시적으로 언급되지 않은 에지케이스:**
+
+1. **paragraph anchor affinity 충돌**: `affinity: "after"` anchor가 연속으로 나올 때 두 번째 anchor의 `old_plain_offset`이 첫 번째 anchor 삽입 후 shift된 DOM 기준인지 원본 기준인지 명확하지 않다. 구현 시 "모든 offset은 original XHTML 기준"으로 고정해야 한다.
+
+2. **list item 수 불일치 처리**: 설계 §5.5는 "child type과 순서를 기준으로 재귀 reconstruct"한다고 하지만, MDX에서 list item을 추가/삭제한 경우의 처리 방식이 명시되지 않았다. 이것이 실제 reverse-sync에서 가장 흔한 변경 패턴 중 하나임을 감안하면, Level 3 테스트(Phase 3 게이트) 전에 결정이 필요하다.
+
+3. **sidecar v3 빌더의 old_plain_text 생성 시점**: `reconstruction.old_plain_text`는 sidecar 빌드 시(forward convert 시점) 기록된다. 이후 page.xhtml이 Confluence에서 자체 수정되면 `old_plain_text`가 실제 XHTML과 달라질 수 있다. 현재 `source_xhtml_sha256`로 감지 가능하나, 불일치 시 처리 경로가 설계에 없다.
+
+4. **`_parse_list_items()` 및 `_build_list_tree()` public 승격 범위**: 설계가 이 private 함수들을 `parse_list_tree()` public API로 승격하겠다고 밝혔다. 현재 `_parse_list_items`가 continuation line(마커 없는 줄)을 이전 항목에 붙이는 로직(emitter.py:182-183)이 있는데, 이 동작이 reconstruction context에서도 의도된 것인지 확인이 필요하다.
+
+5. **BeautifulSoup `html.parser` 속성 순서**: `xhtml_normalizer.py` 구현 시 BeautifulSoup으로 파싱 후 재직렬화하면 attribute 순서가 바뀔 수 있다. fragment comparison에서 false negative를 방지하려면 attribute 정렬 규칙을 명시해야 한다. 현재 `mdx_to_storage_xhtml_verify.py`에 이 로직이 있을 수 있으나 통합 방법을 확인해야 한다.
+
+---
+
+### 5. 구현 복잡도 vs 기대 효과
+
+**비용 측면:**
+
+- Phase 0-2(normalizer, schema v3, clean block replacement): 비교적 낮은 복잡도. 기존 자산 재사용이 명확하고 test oracle이 이미 준비됐다.
+- Phase 3(paragraph/list anchor reconstruction): 중간-높은 복잡도. offset mapping 알고리즘, DOM 삽입 순서 보장, list item 수 불일치 처리 등 새로 작성해야 할 로직이 많다.
+- Phase 4(container reconstruction): 높은 복잡도. callout/details/ADF panel이 각각 outer wrapper 구조가 다르고, `lost_info_patcher`와의 책임 분리를 정확히 해야 한다.
+- Phase 5(planner 전환): 중간 복잡도. 하지만 fail-closed 전환 시 회귀 위험이 있어 신중한 rollout이 필요하다.
+
+**기대 효과:**
+
+- 현재 시스템의 근본적 취약점(text coordinate를 벗어난 Confluence 요소 손실)을 구조적으로 해소한다.
+- `transfer_text_changes()` fallback에 의존하는 silent corruption 경로를 제거한다.
+- test oracle이 `mapping.yaml`에서 `expected.roundtrip.json`으로 전환되어 "무엇을 기준으로 테스트하는가"가 명확해진다.
+
+**판단:**
+
+Phase 0-2의 ROI는 높다. Phase 3-4는 복잡도 대비 효과가 여전히 높지만, 에지케이스 처리 결정을 Phase 착수 전에 명확히 해야 낭비 없이 구현할 수 있다.
+
+---
+
+### 종합 판단
+
+v5 리뷰의 3개 Critical/High 이슈가 새 설계 문서에서 모두 명시적으로 해소됐다. 특히 paragraph 좌표계를 "XHTML DOM에서 추출한 normalized plain text"로 확정하고, oracle을 `expected.roundtrip.json.xhtml_fragment`로 명시한 결정이 핵심이다. 현재 코드베이스와의 정합성도 양호하다.
+
+**권고사항:**
+
+1. **Phase 3 착수 전 필수 결정**: list item 수 불일치(추가/삭제) 시 처리 방식을 명시하라. "수 불일치는 항상 fail"이라면 테스트 설계에 해당 케이스를 명시적으로 포함해야 한다.
+2. **anchor offset 기준 명문화**: `reconstruction.anchors[].old_plain_offset`이 "원본 XHTML 기준 누적 offset"임을 설계 문서에 명시하라. 구현자가 이 가정을 따르지 않으면 멀티 anchor 케이스에서 버그가 발생한다.
+3. **`lost_info_patcher` vs reconstruction metadata 경계 결정**: callout outer wrapper를 어느 쪽이 책임지는지 Phase 4 착수 전에 코드 수준 경계를 설계 문서에 추가하라.
+4. **현재 설계 문서 완성도**: 구현 착수 가능 수준이다. Phase 0-2는 즉시 착수 가능하고, Phase 3-4는 위 항목을 보완 후 착수를 권장한다.
diff --git a/confluence-mdx/docs/plans/2026-03-13-reverse-sync-reconstruction-design.md b/confluence-mdx/docs/plans/2026-03-13-reverse-sync-reconstruction-design.md
new file mode 100644
index 000000000..c3010079d
--- /dev/null
+++ b/confluence-mdx/docs/plans/2026-03-13-reverse-sync-reconstruction-design.md
@@ -0,0 +1,948 @@
+# Reverse Sync 전면 재구성 설계
+
+> 작성일: 2026-03-13
+> 대상 PR: #913
+> 연관 문서:
+> - `docs/plans/2026-03-13-reverse-sync-reconstruction-design-review.md`
+> - `docs/analysis-reverse-sync-refactoring.md`
+
+## 1. 문서 목적
+
+이 문서는 PR #913의 reverse-sync 설계를 전면 재작성한 버전이다.
+
+목표는 세 가지다.
+
+1. MDX 변경을 XHTML로 재구성하여 Confluence 문서를 안정적으로 업데이트한다.
+2. 현재의 heuristic 텍스트 패치 체인을 구조적 재구성 경로로 치환한다.
+3. 현재 저장소에 이미 존재하는 `tests/testcases/` 와 `tests/reverse-sync/` 자산을 중심으로, 구현·회귀·유지보수가 가능한 테스트 체계를 만든다.
+
+이 문서는 "방향 제안"이 아니라, 구현 착수 전에 필요한 설계 전제와 테스트 게이트를 코드베이스 기준으로 확정하는 문서다.
+
+## 2. 현재 문제와 재설계 목표
+
+현재 reverse-sync 파이프라인은 다음 흐름이다.
+
+`MDX diff -> mapping 추론 -> text/plain 기준 패치 -> patched XHTML -> forward convert -> MDX 재검증`
+
+핵심 병목은 `patch_builder.py` 와 `text_transfer.py` 에 있다.
+
+- 변경 블록을 XHTML로 다시 만드는 대신, 기존 XHTML 내부 텍스트만 이식한다.
+- list, table, callout, containing block, direct replacement 등 전략 분기가 계속 늘어난다.
+- Confluence 전용 요소(``, ``, ``, `ac:adf-extension`)는 텍스트 좌표계 밖에 있기 때문에, 텍스트만 옮기는 방식이 구조적으로 불안정하다.
+
+이번 재설계의 목표는 분명하다.
+
+- 변경된 MDX 블록은 가능한 한 "다시 emit한 XHTML fragment"로 교체한다.
+- emitter가 재현할 수 없는 Confluence 전용 정보만 sidecar metadata로 보존 후 재주입한다.
+- modified block 처리의 기본 전략을 `transfer_text_changes()` 가 아니라 `reconstruct_fragment()` 로 바꾼다.
+
+즉 새 기본 경로는 다음과 같다.
+
+`MDX diff -> changed block identify -> emit XHTML fragment -> restore preserved anchors/lost info -> replace top-level fragment -> forward verify`
+
+## 3. 리뷰에서 확정된 수정 요구
+
+리뷰 문서에서 지적한 사항 중 설계 착수 전에 반드시 확정해야 하는 항목은 아래 네 가지다.
+
+### 3.1 Paragraph 좌표계
+
+기존 문서는 `convert_inline()` 를 사실상 "XHTML -> MDX 역변환기"처럼 가정했다. 실제 코드에서는 성립하지 않는다.
+
+- `convert_inline()` 는 `mdx_to_storage.inline.convert_inline`
+- 역할은 MDX inline -> XHTML inline 변환
+- XHTML fragment를 넣어도 MDX로 돌아오지 않는다
+
+따라서 새 설계는 다음 원칙을 따른다.
+
+- paragraph/list-item anchor 매핑의 기준 좌표계는 "MDX literal"이 아니다
+- 기준 좌표계는 "XHTML DOM 에서 추출한 normalized plain text"다
+- old/new 비교는 `old_mdx_text` 가 아니라 `old_plain_text -> new_plain_text` 로 수행한다
+
+이 결정으로 XHTML -> MDX inverse 가정을 제거한다.
+
+### 3.2 테스트 oracle
+
+`mapping.yaml` 은 runtime lookup 용이지, fragment oracle 용이 아니다. 실제 저장소의 `load_sidecar_mapping()` 도 fragment 본문을 읽지 않는다.
+
+새 설계의 oracle은 다음 순서로 사용한다.
+
+1. `expected.roundtrip.json`
+ - 모든 `tests/testcases/*` 21개에 존재
+ - top-level `xhtml_fragment` 를 exact oracle로 제공
+2. `page.xhtml`
+ - sidecar에 없는 nested fragment나 sub-xpath 비교에 사용
+3. `expected.reverse-sync.patched.xhtml`
+ - 변경 시나리오 16개에 대한 golden page oracle
+
+즉 unit/integration 테스트는 `mapping.yaml` 에 의존하지 않는다.
+
+### 3.3 XHTML normalization
+
+새 비교 전략은 `lxml` 을 도입하지 않는다.
+
+이 저장소에는 이미 다음 자산이 있다.
+
+- `bin/reverse_sync/mdx_to_storage_xhtml_verify.py`
+- `xhtml_beautify_diff.py`
+- BeautifulSoup 기반 attribute stripping / layout stripping / macro stripping
+
+새 설계는 이 경로를 공용 normalizer로 승격한다.
+
+- 새 공용 모듈: `reverse_sync/xhtml_normalizer.py`
+- 구현 기반: BeautifulSoup + 기존 ignored-attribute 규칙 재사용
+- 비교 단위: page 전체와 fragment 모두 지원
+
+이로써 새 의존성 없이 테스트 가능성을 확보한다.
+
+### 3.4 block identity
+
+`mdx_content_hash` 단독 매칭은 충분하지 않다.
+
+현재 실제 데이터에서도 중복 content가 존재한다. 특히 `reverse_sync.mdx_block_parser` 기준으로는 `` 같은 동일 블록이 여러 번 잡히는 케이스가 이미 보인다.
+
+새 설계의 block identity는 아래를 함께 사용한다.
+
+- `block_index`
+- `mdx_line_range`
+- `mdx_content_hash`
+- 필요 시 동일 hash 후보군 내 상대 순서
+
+즉 lookup key는 "hash 하나"가 아니라 "hash + line range + order"다.
+
+## 4. 현재 코드베이스와 자산 분석
+
+### 4.1 코드베이스에서 재사용할 축
+
+이미 있는 구현 중 이번 설계에서 그대로 활용할 축은 다음과 같다.
+
+- `reverse_sync_cli.py`
+ - verify / push orchestration
+ - forward convert 후 strict roundtrip 검증
+- `reverse_sync.sidecar`
+ - `RoundtripSidecar`, `SidecarBlock`, `expected.roundtrip.json`
+- `reverse_sync.mapping_recorder`
+ - XHTML top-level / callout child mapping 추출
+- `mdx_to_storage.parser`, `mdx_to_storage.emitter`
+ - MDX 구조 파싱과 XHTML emission
+ - callout child 재귀 emission 가능
+ - nested list tree 구성 함수 보유
+- `reverse_sync.lost_info_patcher`
+ - 링크, 이모티콘, filename, image, ADF extension 복원 로직
+
+반대로 이번 재설계에서 더 이상 중심축이 되어서는 안 되는 부분은 다음과 같다.
+
+- `transfer_text_changes()` 기반 modified block 패치
+- `mapping.yaml` 을 fragment oracle처럼 사용하는 방식
+- block 내부 구조를 content text만으로 추론하는 방식
+
+### 4.2 테스트 자산 현황
+
+현재 확보된 테스트 자산은 설계 검증에 충분히 강하다.
+
+#### `tests/testcases/`
+
+- 총 21개 케이스
+- `page.xhtml`: 21개
+- `expected.mdx`: 21개
+- `expected.roundtrip.json`: 21개
+- `original.mdx` + `improved.mdx` + `expected.reverse-sync.*`: 16개
+- `attachments.v1.yaml`: 19개
+- `page.v1.yaml`: 19개
+- `page.v2.yaml`, `children.v2.yaml`: 각 19개
+- `page.adf`: 18개
+
+구조적 커버리지:
+
+- list: 20개
+- table: 9개
+- image: 13개
+- callout macro/ADF panel: 12개
+- `ac:adf-extension`: 3개
+- 링크: 12개
+- code macro: 4개
+
+대표 케이스:
+
+- list item + image: `544113141`, `544145591`, `692355151`, `880181257`, `883654669`
+- callout + nested list: `1454342158`, `544145591`, `692355151`, `880181257`, `883654669`
+- callout + code macro: `544112828`
+- ADF panel: `1454342158`, `544379140`, `panels`
+
+#### `tests/reverse-sync/`
+
+- 총 42개 실제 reverse-sync 회귀 케이스
+- `pages.yaml` 기준: `pass` 28개, `fail` 14개, `catalog_only` 24개
+
+구조적 커버리지:
+
+- list: 42개
+- image: 38개
+- callout: 28개
+- table: 10개
+- 링크: 19개
+- code macro: 7개
+
+특히 중요한 실사례:
+
+- paragraph/list item 내부 inline image: `544376004`
+- callout + code: `544112828`
+- 다수의 이미지/링크/callout 혼합 페이지: `544145591`, `1454342158`
+
+#### 결론
+
+새 설계는 "fixture가 부족해서 추상 설계를 해야 하는 상태"가 아니다. 오히려 반대다.
+
+- unchanged fragment oracle: 이미 충분함
+- changed-page golden oracle: 16개 존재
+- failure reproduction corpus: 42개 존재
+
+부족한 것은 fixture 양이 아니라, 이 자산을 설계 검증 단계별로 재배치하는 일이다.
+
+## 5. 제안 아키텍처
+
+### 5.1 최상위 원칙
+
+1. modified block는 whole-fragment replacement가 기본이다
+2. preserved 정보는 "text"가 아니라 "raw XHTML preservation unit" 으로 다룬다
+3. anchor 재주입은 MDX 좌표가 아니라 normalized plain-text 좌표에서 수행한다
+4. list / callout / details / ADF panel 은 child order 기반으로 재구성한다
+5. 지원 범위 밖 구조는 fuzzy patch 하지 않고 명시적으로 fail 한다
+
+### 5.2 sidecar 전략
+
+기존 `RoundtripSidecar` 를 primary runtime artifact 로 승격한다.
+
+- `mapping.yaml`
+ - 역할 축소: top-level routing, 사람이 읽는 디버그 용도
+- `expected.roundtrip.json`
+ - 역할 확대: exact fragment oracle + reconstruction metadata
+
+새 스키마는 `RoundtripSidecar schema_version = 3` 으로 정의한다.
+
+핵심 변화:
+
+- 각 `SidecarBlock` 에 reconstruction metadata 추가
+- modified block 재구성에 필요한 preserved anchor/unit 을 block 단위로 저장
+
+예시:
+
+```json
+{
+ "block_index": 12,
+ "xhtml_xpath": "p[3]",
+ "xhtml_fragment": "A B
",
+ "mdx_content_hash": "...",
+ "mdx_line_range": [40, 40],
+ "lost_info": {},
+ "reconstruction": {
+ "kind": "paragraph",
+ "old_plain_text": "A B",
+ "anchors": [
+ {
+ "anchor_id": "p[3]/ac:image[1]",
+ "raw_xhtml": "",
+ "old_plain_offset": 2,
+ "affinity": "after"
+ }
+ ]
+ }
+}
+```
+
+리스트 예시:
+
+```json
+{
+ "block_index": 8,
+ "xhtml_xpath": "ul[1]",
+ "xhtml_fragment": "",
+ "reconstruction": {
+ "kind": "list",
+ "ordered": false,
+ "items": [
+ {
+ "item_xpath": "ul[1]/li[1]",
+ "old_plain_text": "item 1",
+ "anchors": [],
+ "child_blocks": []
+ },
+ {
+ "item_xpath": "ul[1]/li[2]",
+ "old_plain_text": "item 2",
+ "anchors": [
+ {
+ "anchor_id": "ul[1]/li[2]/ac:image[1]",
+ "raw_xhtml": "",
+ "old_plain_offset": 6,
+ "affinity": "after"
+ }
+ ],
+ "child_blocks": [
+ {
+ "kind": "list",
+ "xpath": "ul[1]/li[2]/ol[1]"
+ }
+ ]
+ }
+ ]
+ }
+}
+```
+
+이 구조의 의도는 단순하다.
+
+- top-level fragment는 `xhtml_fragment` 가 책임진다
+- list/paragraph/container 내부 보존 정보는 `reconstruction` 이 책임진다
+- 테스트 oracle와 runtime metadata가 같은 artifact 안에 있게 한다
+
+### 5.3 block 분류
+
+새 재구성기는 top-level block를 네 종류로 나눈다.
+
+#### A. Clean block
+
+대상:
+
+- heading
+- code macro
+- table
+- hr
+- paragraph without preserved anchors
+
+처리:
+
+- `mdx_to_storage.emit_block()` 또는 `mdx_block_to_xhtml_element()` 로 새 fragment emit
+- block-level `lost_info` 적용
+- 기존 fragment 전체 replace
+
+#### B. Inline-anchor block
+
+대상:
+
+- paragraph 안의 `ac:image`, `ac:link` 류 preservation unit
+- list item 안의 inline image / trailing preserved node
+
+처리:
+
+1. improved MDX block를 먼저 XHTML로 emit
+2. emit 결과에서 plain text를 추출
+3. sidecar의 `old_plain_text` 와 anchor offset을 기준으로 old -> new offset 매핑
+4. 매핑된 위치에 raw anchor XHTML 삽입
+
+중요한 점:
+
+- old/new 비교는 plain-text 좌표
+- 삽입 대상은 "생성된 XHTML DOM"
+- raw 문자열 위치 삽입이 아니라 DOM walk 기반 삽입
+
+#### C. Ordered child block
+
+대상:
+
+- nested list
+- callout / details / ADF panel body
+
+처리:
+
+- original XHTML 의 child order를 sidecar에 저장
+- improved MDX 는 `mdx_to_storage.parser.parse_mdx()` 로 child blocks 파싱
+- child type과 순서를 기준으로 재귀 reconstruct
+
+여기서는 text matching을 하지 않는다. 위치와 child slot이 기준이다.
+
+#### D. Opaque block
+
+대상:
+
+- emitter가 재구성하지 못하는 custom macro
+- 현재 testcase에 없거나 metadata 규칙이 정의되지 않은 구조
+
+처리:
+
+- `UnsupportedReconstructionError`
+- verify는 fail
+- 해당 페이지를 testcase로 승격 후 설계 범위 확장
+
+이 fail-closed 정책이 중요하다. unsupported structure에서 silent corruption이 가장 위험하다.
+
+### 5.4 paragraph / list item anchor 재주입
+
+이 설계의 핵심 차별점은 "anchor를 plain-text offset에 고정"하는 것이다.
+
+#### 좌표계
+
+- `old_plain_text`: original XHTML fragment에서 DOM text를 뽑아 정규화한 값
+- `new_plain_text`: improved MDX 를 emit한 XHTML fragment에서 같은 규칙으로 뽑은 값
+- `old_plain_offset`: original plain text 기준 anchor 위치
+- `new_plain_offset`: old -> new diff로 계산된 삽입 위치
+
+#### 알고리즘
+
+1. `extract_plain_text(fragment)` 로 old/new plain text 생성
+2. `map_offsets(old_plain, new_plain, offsets)` 로 new offset 계산
+3. `insert_raw_anchor_at_plain_offset(soup, raw_xhtml, offset)` 로 DOM 삽입
+
+이 방식은 review에서 지적된 "XHTML inline fragment를 MDX text로 역변환해야 하는가" 문제를 제거한다.
+
+### 5.5 list 재구성
+
+리스트는 text queue가 아니라 tree + order 매칭으로 재구성한다.
+
+재사용 자산:
+
+- `mdx_to_storage.emitter._parse_list_items()`
+- `mdx_to_storage.emitter._build_list_tree()`
+
+다만 private 함수 직접 import는 피한다. 이번 작업에서 public helper로 승격한다.
+
+제안:
+
+- 새 public API: `mdx_to_storage.emitter.parse_list_tree(content: str) -> list[ListNode]`
+
+재구성 로직:
+
+1. improved MDX list block -> list tree 생성
+2. sidecar list item sequence와 index 기반 zip
+3. 각 item에 대해
+ - item text emit
+ - item-level anchors 재삽입
+ - child list / block child 재귀 재구성
+4. top-level `` / `` wrapper regenerate
+
+이렇게 하면 다음이 가능하다.
+
+- 동일 텍스트 item이 여러 번 나와도 안정적
+- nested list의 중복 삽입 방지
+- image가 들어간 list item도 text patch 없이 처리
+
+### 5.6 callout / details / ADF panel 재구성
+
+callout은 이번 설계에서 "containing block에 text만 이식"하지 않는다.
+
+이미 있는 자산:
+
+- `mapping_recorder.record_mapping()` 는 callout의 child xpath를 생성한다
+- `mdx_to_storage.parser.parse_mdx()` 와 `_emit_callout()` 은 child block 재귀 emission 을 지원한다
+
+따라서 새 경로는 아래와 같다.
+
+1. original callout body child order를 sidecar metadata에 저장
+2. improved MDX callout body를 `parse_mdx()` 로 child block sequence로 파싱
+3. child slot 단위로 reconstruct
+4. 최종 body를 `` 또는 `ac:adf-content` 아래에 다시 조립
+
+주의:
+
+- `macro-panel` 과 `ac:adf-extension` 은 body 구조는 같지만 outer wrapper가 다르다
+- outer wrapper 보존은 `lost_info_patcher` 가 아니라 reconstruction metadata가 책임진다
+- ADF panel raw outer fragment가 필요한 경우 sidecar에 raw wrapper를 저장한다
+
+### 5.7 patch 적용 단위
+
+modified block는 `new_inner_xhtml` 보다 `new_element_xhtml` 교체가 기본이다.
+
+이유:
+
+- top-level element 전체를 교체해야 wrapper, attribute, child structure를 한 번에 통제할 수 있다
+- innerHTML 교체만으로는 callout outer wrapper, list root tag, table root tag의 일관성을 강제하기 어렵다
+
+따라서 `xhtml_patcher.py` 에 새 액션을 추가한다.
+
+- `replace_fragment`
+ - 입력: `xhtml_xpath`, `new_element_xhtml`
+ - 의미: xpath 대상 top-level element 전체를 새 fragment로 치환
+
+기존 `insert` / `delete` 는 유지한다.
+
+### 5.8 block identity와 planner
+
+기존 `patch_builder.py` 는 전략 분기와 fallback이 많다. 새 설계는 planner를 분리한다.
+
+제안 모듈:
+
+- `reverse_sync/reconstruction_planner.py`
+ - changed block -> reconstruction strategy 결정
+- `reverse_sync/reconstruction_sidecar.py`
+ - sidecar schema v3 load/build
+- `reverse_sync/reconstructors.py`
+ - paragraph/list/container별 fragment rebuild
+- `reverse_sync/xhtml_normalizer.py`
+ - shared normalization / plain-text extraction
+
+`patch_builder.py` 는 최종적으로 orchestration thin layer가 된다.
+
+## 6. 구현 범위와 비범위
+
+### 이번 설계 범위
+
+- modified top-level block의 whole-fragment reconstruction
+- paragraph/list item inline anchor 재주입
+- nested list reconstruction
+- callout/details/ADF panel body reconstruction
+- block identity 안정화
+- golden/oracle 기반 테스트 체계 구축
+
+### 이번 설계 비범위
+
+- sidecar/rehydrator 전체를 단일 parser 체계로 통합하는 대형 리팩토링
+- testcase에 없는 custom macro 일반화
+- Confluence storage 전체에 대한 generic DOM diff 엔진
+
+parser 통합은 후속 과제로 남긴다. 이번 작업은 "reverse-sync를 구조적 재구성 경로로 전환"하는 데 집중한다.
+
+## 7. 테스트 설계
+
+테스트는 두 묶음으로 나눈다.
+
+1. 설계 검증 테스트
+2. 회귀 방지 테스트
+
+### 7.1 설계 검증 테스트
+
+#### Level 0. Helper / invariant
+
+새 파일 제안:
+
+- `tests/test_reverse_sync_xhtml_normalizer.py`
+- `tests/test_reverse_sync_reconstruction_offsets.py`
+- `tests/test_reverse_sync_reconstruction_insert.py`
+
+검증 항목:
+
+- plain-text extraction이 original/emitted fragment에서 같은 규칙으로 동작하는지
+- old -> new offset mapping이 삽입/삭제/대체에 대해 안정적인지
+- raw anchor insertion이 DOM 파괴 없이 수행되는지
+- `hash + line_range` disambiguation이 duplicate content에서도 안정적인지
+
+여기서 review의 Critical 이슈를 먼저 red test로 고정한다.
+
+필수 red cases:
+
+1. paragraph + inline image
+2. list item + image
+3. duplicate hash candidate
+4. namespace-bearing fragment normalization
+
+#### Level 1. Block reconstruction against exact fragment oracle
+
+새 파일 제안:
+
+- `tests/test_reverse_sync_reconstruct_paragraph.py`
+- `tests/test_reverse_sync_reconstruct_list.py`
+- `tests/test_reverse_sync_reconstruct_container.py`
+
+oracle:
+
+- 기본: `expected.roundtrip.json.blocks[].xhtml_fragment`
+- nested child: `page.xhtml` 에서 xpath extraction
+
+검증 방식:
+
+- unchanged MDX block를 reconstruct 했을 때 oracle fragment와 normalize-equal
+
+대표 파라미터:
+
+- list item + image: `544113141`, `544145591`, `692355151`, `880181257`, `883654669`
+- callout + list: `1454342158`, `544145591`, `692355151`, `880181257`, `883654669`
+- callout + code: `544112828`
+- ADF panel: `1454342158`, `544379140`, `panels`
+- inline paragraph image: `tests/reverse-sync/544376004`
+
+`544376004` 는 `tests/testcases` 가 아니므로, unit test에서는 해당 page에서 관련 fragment만 추출한 minimal fixture를 추가해도 된다. 이것은 review의 "새 fixture가 아예 불필요하다고 말하면 안 된다"는 지적에 대한 현실적 대응이다.
+
+#### Level 2. Changed block golden reconstruction
+
+새 파일 제안:
+
+- `tests/test_reverse_sync_reconstruction_goldens.py`
+
+oracle:
+
+- `expected.reverse-sync.patched.xhtml`
+- 필요한 경우 `expected.reverse-sync.mapping.original.yaml` / `expected.reverse-sync.result.yaml`
+
+대상:
+
+- `original.mdx` + `improved.mdx` + `expected.reverse-sync.*` 가 존재하는 16개 `tests/testcases`
+
+검증 방식:
+
+- changed block만 reconstruct + page assembly 후 `expected.reverse-sync.patched.xhtml` 와 normalize-equal
+- `expected.reverse-sync.result.yaml` 의 `status: pass` 케이스는 forward verify까지 exact pass
+
+### 7.2 회귀 방지 테스트
+
+#### Level 3. Existing sidecar / byte-equal gates
+
+기존 테스트를 유지하고 schema v3에 맞춰 확장한다.
+
+- `tests/test_reverse_sync_sidecar_v2.py`
+- `tests/test_reverse_sync_rehydrator.py`
+- `tests/test_reverse_sync_byte_verify.py`
+
+변경점:
+
+- `expected.roundtrip.json` builder/loader가 reconstruction metadata를 읽고 써야 한다
+- unchanged case에서는 여전히 21/21 byte-equal 유지
+
+#### Level 4. CLI / E2E
+
+기존 테스트를 유지하되 reconstruction path를 기본 경로로 바꾼다.
+
+- `tests/test_reverse_sync_cli.py`
+- `tests/test_reverse_sync_e2e.py`
+- `tests/test_reverse_sync_structural.py`
+
+여기에 다음을 추가한다.
+
+- `tests/reverse-sync/pages.yaml` 의 `expected_status: pass` 케이스는 새 경로에서도 계속 pass
+- `expected_status: fail` 케이스는 failure type별로 하나씩 우선 red -> green 전환
+
+우선순위는 아래 순으로 둔다.
+
+1. list/image
+2. callout/code
+3. callout/list
+4. ADF panel
+
+### 7.3 현재 자산 활용 계획 요약
+
+| 자산 | 수량 | 새 설계에서의 역할 |
+|------|------|--------------------|
+| `tests/testcases/*/page.xhtml` | 21 | exact source page, nested fragment extraction |
+| `tests/testcases/*/expected.roundtrip.json` | 21 | unchanged top-level fragment oracle |
+| `tests/testcases/*/original.mdx` | 16 | reverse-sync original input |
+| `tests/testcases/*/improved.mdx` | 16 | reverse-sync changed input |
+| `tests/testcases/*/expected.reverse-sync.patched.xhtml` | 16 | changed-page golden oracle |
+| `tests/testcases/*/expected.reverse-sync.result.yaml` | 16 | expected verify outcome |
+| `tests/testcases/*/attachments.v1.yaml` | 19 | image filename / asset context |
+| `tests/testcases/*/page.v1.yaml`, `page.v2.yaml`, `children.v2.yaml`, `page.adf` | 18~19 | forward converter context, ADF/callout/link validation |
+| `tests/reverse-sync/*` | 42 | 실사례 회귀 및 failure reproduction |
+
+## 8. 단계별 구현 계획
+
+### Phase 0. 공용 helper 추출
+
+- `xhtml_normalizer.py` 추가
+- `extract_plain_text()`, `normalize_fragment()`, `extract_fragment_by_xpath()` 구현
+- list tree helper public API 승격
+
+게이트:
+
+- Level 0 helper tests green
+
+### Phase 1. sidecar schema v3
+
+- `RoundtripSidecar` 에 reconstruction metadata 추가
+- builder/load/write/update 구현
+- `hash + line_range` 기반 identity helper 도입
+
+게이트:
+
+- existing sidecar tests green
+- unchanged 21개 `expected.roundtrip.json` roundtrip 유지
+
+### Phase 2. clean block whole-fragment replacement
+
+- heading/code/table/simple paragraph modified block를 reconstruction path로 전환
+- `replace_fragment` patch 추가
+
+게이트:
+
+- simple modified golden cases green
+- `transfer_text_changes()` 경로 없이 clean block 변경 처리 가능
+
+### Phase 3. paragraph/list anchor reconstruction
+
+- inline anchor metadata builder
+- offset mapping + DOM insertion helper
+- list item + nested list reconstruction
+
+게이트:
+
+- `544113141`, `544145591`, `692355151`, `880181257`, `883654669`
+- `544376004` helper/unit case
+
+### Phase 4. container reconstruction
+
+- callout/details/ADF panel body reconstruction
+- child slot order 기반 재귀 rebuild
+
+게이트:
+
+- `544112828`
+- `1454342158`
+- `544379140`
+- `panels`
+
+### Phase 5. planner 전환과 batch 회귀
+
+- `patch_builder.py` modified path를 reconstruction planner로 위임
+- legacy text-transfer path는 fallback 또는 제거
+
+게이트:
+
+- `tests/testcases` 16개 reverse-sync golden green
+- `tests/reverse-sync/pages.yaml` pass 케이스 유지
+
+## 9. 승인 기준
+
+이 설계는 아래를 만족해야 구현 완료로 본다.
+
+1. modified block의 기본 경로가 whole-fragment reconstruction 이다
+2. paragraph/list anchor 처리가 plain-text 좌표계 기준으로 구현된다
+3. test oracle이 `mapping.yaml` 이 아니라 `expected.roundtrip.json` / `page.xhtml` / `expected.reverse-sync.patched.xhtml` 로 확정된다
+4. XHTML normalization은 BeautifulSoup 기반 공용 helper로 통일된다
+5. duplicate content에서도 `hash + line_range` 기반 identity가 동작한다
+6. 기존 `tests/testcases` / `tests/reverse-sync` 자산을 그대로 회귀 게이트로 사용할 수 있다
+
+## 10. 최종 판단
+
+PR #913의 원래 방향은 맞다. 다만 기존 문서는 "재구성으로 간다"는 선언에 비해, 실제 구현이 의존할 좌표계, oracle, sidecar 책임 분리가 부족했다.
+
+새 설계의 핵심 차이는 다음 세 가지다.
+
+- `convert_inline()` 역변환 가정을 버리고 plain-text 좌표계를 채택한다
+- `mapping.yaml` 을 oracle 자리에서 내리고 `expected.roundtrip.json` 을 중심 artifact 로 올린다
+- 기존 testcase 자산을 설계 검증 테스트와 회귀 테스트로 분리해 사용한다
+
+이 기준으로 구현하면, 최종 목표인 "MDX 변경을 XHTML로 재구성하여 Confluence 문서를 업데이트"하는 기능을 현재 코드베이스 위에서 더 안정적으로 구현하고 유지보수할 수 있다.
+
+## 11. 기존 코드 삭제 및 정리 범위
+
+새 구현이 기본 경로가 되면, 현재 코드베이스에는 "과거 heuristic text patch 경로"가 상당 부분 중복 상태로 남는다. 이 섹션은 무엇을 삭제할 수 있고, 무엇을 단계적으로 축소해야 하는지를 명시한다.
+
+삭제 원칙은 단순하다.
+
+1. `tests/testcases` 16개 reverse-sync golden 과 `tests/reverse-sync/pages.yaml` 의 `expected_status: pass` 게이트가 새 경로에서 안정화되기 전에는 삭제하지 않는다.
+2. 새 경로가 green 이 된 뒤에는 동일 책임의 구 구현을 남겨두지 않는다.
+3. debug artifact는 기본 동작에서 제거하고, 필요하면 명시적 debug flag 뒤로 보낸다.
+
+### 11.1 완전 삭제 대상
+
+아래 모듈은 새 설계가 완료되면 역할이 완전히 대체된다.
+
+#### `bin/reverse_sync/text_transfer.py`
+
+삭제 사유:
+
+- MDX plain text와 XHTML plain text를 문자 단위로 정렬해 수정분만 이식하는 구현이다.
+- 새 설계는 modified block 기본 경로를 whole-fragment reconstruction으로 바꾸므로 더 이상 중심 경로가 아니다.
+- paragraph/list anchor 처리도 이 모듈이 아니라 plain-text offset + DOM insertion helper가 담당한다.
+
+함께 삭제/교체할 테스트:
+
+- `tests/test_reverse_sync_text_transfer.py`
+- `tests/test_reverse_sync_cli.py` 내 `align_chars`, `find_insert_pos`, `transfer_text_changes` 관련 테스트
+
+#### `bin/reverse_sync/list_patcher.py`
+
+삭제 사유:
+
+- 리스트를 item-level text patch 또는 전체 innerHTML 재생성으로 처리하는 전용 heuristic 모듈이다.
+- 새 설계에서는 list tree + sidecar reconstruction metadata 기반 재구성기로 대체된다.
+- 특히 `_regenerate_list_from_parent()` 의 `transfer_text_changes()` 폴백은 새 경로와 철학적으로 충돌한다.
+
+함께 삭제/교체할 테스트:
+
+- `tests/test_reverse_sync_patch_builder.py` 내 `build_list_item_patches`, `split_list_items` 관련 테스트
+- `tests/test_reverse_sync_cli.py` 내 `build_list_item_patches` 직접 호출 테스트
+
+#### `bin/reverse_sync/table_patcher.py`
+
+삭제 사유:
+
+- table row별 plain text patch를 containing block에 누적 적용하는 구 경로다.
+- 새 설계에서는 table도 clean block로 whole-fragment replacement 대상이다.
+- row-level text patch는 더 이상 유지할 가치가 없다.
+
+함께 삭제/교체할 테스트:
+
+- `tests/test_reverse_sync_patch_builder.py` 내 `build_table_row_patches`, `split_table_rows`, `normalize_table_row` 관련 테스트
+
+#### `bin/reverse_sync/inline_detector.py`
+
+삭제 사유:
+
+- inline marker 변화 여부를 감지해 기존 heuristic branch를 선택하기 위한 보조 모듈이다.
+- 새 경로는 inline marker 변화 감지로 분기하지 않고, block kind와 reconstruction metadata로 분기한다.
+- 따라서 `has_inline_format_change()` / `has_inline_boundary_change()` 는 planner 설계에서 더 이상 필요하지 않다.
+
+함께 삭제/교체할 테스트:
+
+- `tests/test_reverse_sync_patch_builder.py` 내 `has_inline_format_change`, `has_inline_boundary_change` 관련 테스트
+
+### 11.2 부분 삭제 또는 대폭 축소 대상
+
+아래 코드는 즉시 파일 전체를 지우기보다는, 새 경로가 기본이 된 뒤 내부 범위를 줄여야 한다.
+
+#### `bin/reverse_sync/patch_builder.py`
+
+삭제할 범위:
+
+- `_flush_containing_changes()`
+- `_resolve_mapping_for_change()`
+- `'direct' | 'containing' | 'list' | 'table' | 'skip'` 전략 분기
+- `transfer_text_changes()` 를 호출하는 modified path
+- `mdx_block_to_inner_xhtml()` 기반 `new_inner_xhtml` 패치 경로
+
+남길 가능성이 있는 범위:
+
+- insert/delete orchestration
+- alignment를 이용한 insert anchor 계산
+
+권장 최종 형태:
+
+- `patch_builder.py` 는 사실상 thin wrapper가 되거나,
+- 새 `reconstruction_planner.py` / `reconstructors.py` 로 책임을 이동한 후 삭제한다.
+
+#### `bin/reverse_sync/xhtml_patcher.py`
+
+삭제할 범위:
+
+- `old_plain_text` + `new_plain_text` modify path
+- `_apply_text_changes()` 와 그에 딸린 text-only patch 로직
+
+남겨야 할 범위:
+
+- XPath resolve
+- `insert`
+- `delete`
+- 새로 추가할 `replace_fragment`
+- CDATA 복원
+
+즉 이 모듈은 "텍스트 패처"가 아니라 "fragment-level DOM patcher"로 축소되어야 한다.
+
+함께 정리할 테스트:
+
+- `tests/test_reverse_sync_xhtml_patcher.py` 의 `new_plain_text` 중심 테스트는 제거하거나 `replace_fragment` 중심 테스트로 교체한다.
+
+#### `bin/reverse_sync/mdx_to_xhtml_inline.py`
+
+삭제 후보 범위:
+
+- `mdx_block_to_inner_xhtml()`
+
+삭제 검토 사유:
+
+- 새 설계의 기본 emitter는 `mdx_to_storage.emit_block()` 이다.
+- `mdx_block_to_inner_xhtml()` 는 innerHTML 단위 패치에 최적화된 구 계층이다.
+
+권장 방향:
+
+- 단기: `mdx_block_to_xhtml_element()` 만 compatibility wrapper로 유지 가능
+- 최종: planner가 `emit_block()` 을 직접 사용하면 모듈 전체 삭제 가능
+
+함께 정리할 테스트:
+
+- `tests/test_reverse_sync_mdx_to_xhtml_inline.py` 의 innerHTML 중심 테스트는 축소하거나 새 reconstruction helper 테스트로 대체한다.
+
+#### `bin/reverse_sync/sidecar.py` 의 mapping.yaml 계층
+
+축소 또는 삭제 대상:
+
+- `SidecarEntry`
+- `SidecarChildEntry`
+- `load_sidecar_mapping()`
+- `build_mdx_to_sidecar_index()`
+- `build_xpath_to_mapping()`
+- `generate_sidecar_mapping()`
+- `find_mapping_by_sidecar()`
+
+삭제 조건:
+
+- `RoundtripSidecar schema v3` 가 top-level routing + reconstruction metadata 책임까지 흡수한 뒤
+- `reverse_sync_cli.py` 와 `converter/cli.py` 가 더 이상 `mapping.yaml` 을 읽고 쓰지 않을 때
+
+즉 이 계층은 "즉시 삭제"가 아니라 "sidecar v3 정착 후 제거" 대상이다.
+
+#### `bin/reverse_sync_cli.py` 의 debug artifact 경로
+
+축소 또는 삭제 대상:
+
+- `reverse-sync.mapping.original.yaml`
+- `reverse-sync.mapping.patched.yaml`
+- runtime 중간 산출물로서의 `mapping.yaml`
+
+권장 방향:
+
+- 기본 verify/push 경로에서는 생성하지 않는다
+- 필요 시 `--debug-mapping` 같은 명시적 플래그 뒤로 이동한다
+
+#### `bin/converter/cli.py` 의 자동 `mapping.yaml` 생성
+
+삭제 또는 optional 화 대상:
+
+- `generate_sidecar_mapping()` 호출 블록 전체
+
+사유:
+
+- forward convert 성공 여부와 mapping.yaml 생성은 본질적으로 분리되어야 한다
+- 새 설계의 중심 artifact는 `mapping.yaml` 이 아니라 `expected.roundtrip.json` / sidecar v3 다
+
+### 11.3 유지 대상
+
+아래 모듈은 새 설계에서도 유지한다.
+
+#### 유지: `bin/reverse_sync/mapping_recorder.py`
+
+이유:
+
+- callout / ADF panel child xpath 추출
+- debug와 fixture 분석
+- nested fragment extraction helper
+
+다만 역할은 "runtime truth"가 아니라 "XHTML 분석/보조 도구"로 한정한다.
+
+#### 유지: `bin/reverse_sync/lost_info_patcher.py`
+
+이유:
+
+- 링크, emoticon, filename, image, adf-extension 복원은 여전히 필요하다
+- 다만 적용 위치는 modified fragment emit 후의 post-process 단계로 고정한다
+
+#### 유지: `bin/reverse_sync/sidecar.py` 의 roundtrip core
+
+유지 범위:
+
+- `RoundtripSidecar`
+- `SidecarBlock`
+- `build_sidecar()`
+- `load_sidecar()`
+- `write_sidecar()`
+- `verify_sidecar_integrity()`
+
+즉 sidecar 모듈 전체를 지우는 것이 아니라, 그 안의 `mapping.yaml` 서브계층만 걷어내는 방향이다.
+
+### 11.4 테스트 코드 삭제 범위
+
+새 구현으로 전환되면 다음 테스트 묶음은 제거 또는 대체되어야 한다.
+
+- `tests/test_reverse_sync_text_transfer.py`
+- `tests/test_reverse_sync_patch_builder.py` 의 heuristic branch 테스트
+- `tests/test_reverse_sync_xhtml_patcher.py` 의 `new_plain_text` 기반 modify 테스트
+- `tests/test_reverse_sync_cli.py` 내부의 text-transfer helper 직접 테스트
+- `tests/test_reverse_sync_sidecar.py` 중 `mapping.yaml` 전용 테스트
+
+대신 아래 묶음이 새 기본 세트가 된다.
+
+- `tests/test_reverse_sync_xhtml_normalizer.py`
+- `tests/test_reverse_sync_reconstruction_offsets.py`
+- `tests/test_reverse_sync_reconstruction_insert.py`
+- `tests/test_reverse_sync_reconstruct_paragraph.py`
+- `tests/test_reverse_sync_reconstruct_list.py`
+- `tests/test_reverse_sync_reconstruct_container.py`
+- `tests/test_reverse_sync_reconstruction_goldens.py`
+
+### 11.5 실제 삭제 순서
+
+삭제는 아래 순서로 진행한다.
+
+1. 새 reconstruction path 구현
+2. 새 helper / block / golden / E2E 테스트 green
+3. `patch_builder.py` 에서 구 heuristic path unreachable 상태 확인
+4. `text_transfer.py`, `list_patcher.py`, `table_patcher.py`, `inline_detector.py` 삭제
+5. 관련 테스트 삭제
+6. 마지막으로 `mapping.yaml` 계층과 debug artifact 생성 경로 제거
+
+이 순서를 지키면 "새 경로 추가 + 구 경로 잔존" 상태를 최소화할 수 있고, 유지보수 비용도 빠르게 줄일 수 있다.