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 index c3010079d..42c0d3306 100644 --- 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 @@ -1,948 +1,532 @@ # Reverse Sync 전면 재구성 설계 -> 작성일: 2026-03-13 -> 대상 PR: #913 +> 최초 작성일: 2026-03-13 +> 갱신일: 2026-03-15 +> 기준 브랜치: `main` +> 기준 커밋: `9e0d43b91c2e47088274e13e82a5c2750e1529f9` +> 반영된 선행 PR: +> - `#913` reverse-sync 재구성 설계 초안 +> - `#914` Phase 0 공용 helper 추출 (`xhtml_normalizer`, list tree public API) +> - `#915` Phase 1 sidecar schema v3 +> - `#917` Phase 1 후속 정리 (`strict v3`, identity helper API 통일, reconstruction metadata 보강) > 연관 문서: > - `docs/plans/2026-03-13-reverse-sync-reconstruction-design-review.md` +> - `docs/plans/2026-03-15-reverse-sync-reconstruction-cleanup-scope.md` > - `docs/analysis-reverse-sync-refactoring.md` ## 1. 문서 목적 -이 문서는 PR #913의 reverse-sync 설계를 전면 재작성한 버전이다. +이 문서는 2026-03-15 기준 `main` 브랜치 상태를 반영해, reverse-sync 재구성 계획을 다시 정리한 버전이다. -목표는 세 가지다. +핵심 목적은 두 가지다. -1. MDX 변경을 XHTML로 재구성하여 Confluence 문서를 안정적으로 업데이트한다. -2. 현재의 heuristic 텍스트 패치 체인을 구조적 재구성 경로로 치환한다. -3. 현재 저장소에 이미 존재하는 `tests/testcases/` 와 `tests/reverse-sync/` 자산을 중심으로, 구현·회귀·유지보수가 가능한 테스트 체계를 만든다. +1. 이미 `main`에 반영된 기반 작업과 아직 남은 재구성 작업을 분리해서 기록한다. +2. 남은 구현을 "legacy text patch 보강"이 아니라 "fragment reconstruction 기본 경로 전환"으로 계속 밀고 갈 수 있도록 단계와 게이트를 확정한다. -이 문서는 "방향 제안"이 아니라, 구현 착수 전에 필요한 설계 전제와 테스트 게이트를 코드베이스 기준으로 확정하는 문서다. +즉 이 문서는 더 이상 PR #913 시점의 순수 제안서가 아니다. 현재 `main`이 어디까지 와 있는지, 그리고 다음 단계가 정확히 무엇인지 정의하는 기준 문서다. -## 2. 현재 문제와 재설계 목표 +## 2. 2026-03-15 기준 main의 실제 상태 -현재 reverse-sync 파이프라인은 다음 흐름이다. +### 2.1 현재 런타임 기본 경로 -`MDX diff -> mapping 추론 -> text/plain 기준 패치 -> patched XHTML -> forward convert -> MDX 재검증` +현재 `reverse_sync_cli.py` 의 verify/push 경로는 여전히 아래 흐름을 기본으로 사용한다. -핵심 병목은 `patch_builder.py` 와 `text_transfer.py` 에 있다. +`MDX diff -> mapping lookup -> patch_builder.py -> xhtml_patcher.py -> roundtrip verify` -- 변경 블록을 XHTML로 다시 만드는 대신, 기존 XHTML 내부 텍스트만 이식한다. -- list, table, callout, containing block, direct replacement 등 전략 분기가 계속 늘어난다. -- Confluence 전용 요소(``, ``, ``, `ac:adf-extension`)는 텍스트 좌표계 밖에 있기 때문에, 텍스트만 옮기는 방식이 구조적으로 불안정하다. +구체적으로는 다음 모듈이 아직 중심이다. -이번 재설계의 목표는 분명하다. +- `bin/reverse_sync/patch_builder.py` +- `bin/reverse_sync/text_transfer.py` +- `bin/reverse_sync/list_patcher.py` +- `bin/reverse_sync/table_patcher.py` +- `bin/reverse_sync/xhtml_patcher.py` -- 변경된 MDX 블록은 가능한 한 "다시 emit한 XHTML fragment"로 교체한다. -- emitter가 재현할 수 없는 Confluence 전용 정보만 sidecar metadata로 보존 후 재주입한다. -- modified block 처리의 기본 전략을 `transfer_text_changes()` 가 아니라 `reconstruct_fragment()` 로 바꾼다. +즉 "modified block를 새 XHTML fragment로 재구성한다"는 최종 목표는 아직 기본 경로가 아니다. -즉 새 기본 경로는 다음과 같다. +### 2.2 이미 main에 머지된 기반 작업 -`MDX diff -> changed block identify -> emit XHTML fragment -> restore preserved anchors/lost info -> replace top-level fragment -> forward verify` +PR `#914`, `#915`, `#917`으로 인해, 원래 설계 문서에서 제안했던 선행 기반 중 상당 부분은 이미 코드로 들어와 있다. -## 3. 리뷰에서 확정된 수정 요구 +#### Phase 0 완료: 공용 helper 추출 -리뷰 문서에서 지적한 사항 중 설계 착수 전에 반드시 확정해야 하는 항목은 아래 네 가지다. +이미 반영된 항목: -### 3.1 Paragraph 좌표계 +- `bin/reverse_sync/xhtml_normalizer.py` + - `extract_plain_text()` + - `normalize_fragment()` + - `extract_fragment_by_xpath()` +- `mdx_to_storage.emitter` 의 list tree 재사용 경로 공개 +- 관련 테스트: + - `tests/test_reverse_sync_xhtml_normalizer.py` + - `tests/test_reverse_sync_list_tree.py` -기존 문서는 `convert_inline()` 를 사실상 "XHTML -> MDX 역변환기"처럼 가정했다. 실제 코드에서는 성립하지 않는다. +의미: -- `convert_inline()` 는 `mdx_to_storage.inline.convert_inline` -- 역할은 MDX inline -> XHTML inline 변환 -- XHTML fragment를 넣어도 MDX로 돌아오지 않는다 +- XHTML 비교와 fragment 추출을 위한 공용 기반이 생겼다. +- 새 설계가 별도 `lxml` 의존성 없이 BeautifulSoup 기반으로 진행될 수 있음이 확정됐다. -따라서 새 설계는 다음 원칙을 따른다. +#### Phase 1 완료: sidecar schema v3 기반선 -- 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 가정을 제거한다. +- `bin/reverse_sync/sidecar.py` + - `ROUNDTRIP_SCHEMA_VERSION = "3"` + - `SidecarBlock.reconstruction` + - `SidecarBlock.to_dict()` / `from_dict()` + - `build_sidecar_identity_index()` + - `find_sidecar_block_by_identity()` +- `build_sidecar()` 의 reconstruction metadata 생성 고도화 + - paragraph: `anchors` + - list: `ordered`, `items` + - container 계열: `child_xpaths` +- `load_sidecar()` 는 이제 v3 strict 검증만 허용 +- 관련 테스트: + - `tests/test_reverse_sync_sidecar_v3.py` + - `expected.roundtrip.json` fixture 갱신 -### 3.2 테스트 oracle +중요한 현재 상태: -`mapping.yaml` 은 runtime lookup 용이지, fragment oracle 용이 아니다. 실제 저장소의 `load_sidecar_mapping()` 도 fragment 본문을 읽지 않는다. +- `reconstruction` 은 더 이상 완전한 placeholder-only 필드는 아니다. +- 하지만 paragraph/list의 실제 preserved anchor/unit 정보는 아직 비어 있다. +- list는 `ordered` 와 `items` 틀만 있고, item-level anchor/child block 정보는 아직 없다. +- container는 `child_xpaths` 까지 기록하지만, runtime reconstruction 에 필요한 raw preservation unit 까지는 저장하지 않는다. +- identity helper는 sidecar 레벨에서 정리됐지만, reverse-sync planner 기본 경로에는 아직 연결되어 있지 않다. -새 설계의 oracle은 다음 순서로 사용한다. +#### 보조 기반: rehydrator 존재 -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/rehydrator.py` 에는 다음 세 경로가 이미 존재한다. -- `bin/reverse_sync/mdx_to_storage_xhtml_verify.py` -- `xhtml_beautify_diff.py` -- BeautifulSoup 기반 attribute stripping / layout stripping / macro stripping +1. fast path: MDX SHA 일치 시 sidecar 원본 재조립 +2. splice path: 블록 해시 일치 시 sidecar fragment 유지, 불일치 시 emitter fallback +3. fallback path: 전체 emitter 재생성 -새 설계는 이 경로를 공용 normalizer로 승격한다. +하지만 이것은 아직 reverse-sync modified block 재구성기와 동일한 개념이 아니다. -- 새 공용 모듈: `reverse_sync/xhtml_normalizer.py` -- 구현 기반: BeautifulSoup + 기존 ignored-attribute 규칙 재사용 -- 비교 단위: page 전체와 fragment 모두 지원 +- splice 경로는 `reconstruction` metadata를 사용하지 않는다. +- inline anchor 재주입, list child order 재구성, callout/details/ADF body 재조립은 아직 없다. -이로써 새 의존성 없이 테스트 가능성을 확보한다. +즉 rehydrator는 "현재 확보된 block-level sidecar 기반"을 보여주는 보조 축이지, 이번 문서의 최종 목표를 이미 달성한 상태는 아니다. -### 3.4 block identity +### 2.3 현재 테스트 자산 -`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 테스트 자산 현황 - -현재 확보된 테스트 자산은 설계 검증에 충분히 강하다. +`main` 기준으로 확인된 자산 수치는 아래와 같다. #### `tests/testcases/` -- 총 21개 케이스 +- 디렉터리 21개 - `page.xhtml`: 21개 - `expected.mdx`: 21개 - `expected.roundtrip.json`: 21개 -- `original.mdx` + `improved.mdx` + `expected.reverse-sync.*`: 16개 +- `original.mdx`: 16개 +- `improved.mdx`: 16개 +- `expected.reverse-sync.patched.xhtml`: 16개 - `attachments.v1.yaml`: 19개 - `page.v1.yaml`: 19개 -- `page.v2.yaml`, `children.v2.yaml`: 각 19개 +- `page.v2.yaml`: 19개 +- `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이 기준이다. +- 실제 fixture 디렉터리 42개 +- 각 케이스에 `original.mdx`, `improved.mdx`, `page.xhtml` 존재 +- `pages.yaml` 전체 엔트리: 66개 +- 이 중 reverse-sync 테스트 메타데이터(`failure_type`)를 가진 실제 테스트 케이스: 42개 +- `expected_status` 기준: + - `pass`: 28개 + - `fail`: 14개 -#### D. Opaque block - -대상: - -- emitter가 재구성하지 못하는 custom macro -- 현재 testcase에 없거나 metadata 규칙이 정의되지 않은 구조 - -처리: +의미: -- `UnsupportedReconstructionError` -- verify는 fail -- 해당 페이지를 testcase로 승격 후 설계 범위 확장 +- `pages.yaml` 은 단순 카탈로그가 아니라 forward converter용 페이지 카탈로그와 reverse-sync 테스트 메타데이터를 함께 담당한다. +- 예전 문서의 `catalog_only` 요약보다, 지금은 `pages.yaml` 내 메타데이터가 실제 기준이다. -이 fail-closed 정책이 중요하다. unsupported structure에서 silent corruption이 가장 위험하다. +## 3. 원래 설계에서 유지되는 핵심 결정 -### 5.4 paragraph / list item anchor 재주입 +PR #913 시점에 제안된 방향 중, 2026-03-15 기준 `main`에서도 그대로 유지해야 하는 결정은 아래 다섯 가지다. -이 설계의 핵심 차별점은 "anchor를 plain-text offset에 고정"하는 것이다. +### 3.1 좌표계는 MDX literal이 아니라 normalized plain text다 -#### 좌표계 +이 결정은 그대로 유지한다. -- `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로 계산된 삽입 위치 +- `convert_inline()` 를 XHTML -> MDX 역변환기로 가정하지 않는다. +- anchor 위치 계산 기준은 `extract_plain_text()` 가 만든 plain text다. +- old/new 비교도 `old_mdx_text -> new_mdx_text` 가 아니라 `old_plain_text -> new_plain_text` 로 수행한다. -#### 알고리즘 +### 3.2 테스트 oracle은 `mapping.yaml` 이 아니다 -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 삽입 +현재도 runtime routing에 `mapping.yaml` 계층이 남아 있지만, fragment oracle로는 적합하지 않다. -이 방식은 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로 승격한다. +1. `expected.roundtrip.json` +2. `page.xhtml` +3. `expected.reverse-sync.patched.xhtml` -제안: +### 3.3 XHTML 비교는 BeautifulSoup 기반 공용 normalizer로 통일한다 -- 새 public API: `mdx_to_storage.emitter.parse_list_tree(content: str) -> list[ListNode]` +이 결정은 이미 `xhtml_normalizer.py` 로 구현되었다. -재구성 로직: +앞으로 새 테스트와 재구성 로직은 같은 normalizer를 공유해야 한다. -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 +### 3.4 block identity는 hash 단독으로 끝내지 않는다 -이렇게 하면 다음이 가능하다. +현재 `main`에는 `build_sidecar_identity_index()` 와 `find_sidecar_block_by_identity()` 가 들어와 있다. -- 동일 텍스트 item이 여러 번 나와도 안정적 -- nested list의 중복 삽입 방지 -- image가 들어간 list item도 text patch 없이 처리 +기본 기준은 아래와 같다. -### 5.6 callout / details / ADF panel 재구성 +- `mdx_content_hash` +- `mdx_line_range` +- 동일 hash 후보군 내 stable order -callout은 이번 설계에서 "containing block에 text만 이식"하지 않는다. +다만 planner 단계에서는 필요 시 아래까지 함께 고려한다. -이미 있는 자산: +- `block_index` +- 동일 hash 후보군 내 상대 순서 -- `mapping_recorder.record_mapping()` 는 callout의 child xpath를 생성한다 -- `mdx_to_storage.parser.parse_mdx()` 와 `_emit_callout()` 은 child block 재귀 emission 을 지원한다 +즉 현재 helper는 최소 기반선이고, planner integration 단계에서 최종 identity 규칙을 완성해야 한다. -따라서 새 경로는 아래와 같다. +### 3.5 지원 범위 밖 구조는 fail-closed가 원칙이다 -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` 아래에 다시 조립 +지원되지 않는 구조에서 fuzzy patch로 조용히 손상시키는 것이 가장 위험하다. -주의: +따라서 새 재구성 경로는 아래 원칙을 따른다. -- `macro-panel` 과 `ac:adf-extension` 은 body 구조는 같지만 outer wrapper가 다르다 -- outer wrapper 보존은 `lost_info_patcher` 가 아니라 reconstruction metadata가 책임진다 -- ADF panel raw outer fragment가 필요한 경우 sidecar에 raw wrapper를 저장한다 +- 재구성 가능한 block만 구조적으로 교체한다. +- 지원 범위 밖 구조는 명시적으로 fail 한다. +- fail 케이스는 `tests/testcases` 또는 `tests/reverse-sync` fixture로 승격해 범위를 넓힌다. -### 5.7 patch 적용 단위 +## 4. 현재 main에서 아직 해결되지 않은 문제 -modified block는 `new_inner_xhtml` 보다 `new_element_xhtml` 교체가 기본이다. +### 4.1 modified block 기본 경로가 아직 heuristic text patch다 -이유: +`patch_builder.py` 는 여전히 다음 전략 분기에 의존한다. -- top-level element 전체를 교체해야 wrapper, attribute, child structure를 한 번에 통제할 수 있다 -- innerHTML 교체만으로는 callout outer wrapper, list root tag, table root tag의 일관성을 강제하기 어렵다 +- `direct` +- `containing` +- `list` +- `table` +- `skip` -따라서 `xhtml_patcher.py` 에 새 액션을 추가한다. +그리고 핵심 경로에서 `transfer_text_changes()` 를 반복 호출한다. -- `replace_fragment` - - 입력: `xhtml_xpath`, `new_element_xhtml` - - 의미: xpath 대상 top-level element 전체를 새 fragment로 치환 +이 구조는 list, table, callout, inline image/link 같은 Confluence 전용 구조를 안정적으로 다루기 어렵다. -기존 `insert` / `delete` 는 유지한다. +### 4.2 `reconstruction` metadata가 아직 runtime reconstruction 에 충분하지 않다 -### 5.8 block identity와 planner +현재 `build_sidecar()` 는 `BlockMapping` 기반으로 metadata를 기록하지만, 아직 다음 공백이 남아 있다. -기존 `patch_builder.py` 는 전략 분기와 fallback이 많다. 새 설계는 planner를 분리한다. +- paragraph: `anchors` 는 비어 있다. +- list: `ordered` 는 기록되지만 `items` 내부 정보는 비어 있다. +- container: `child_xpaths` 는 기록되지만 preserved raw unit 은 저장하지 않는다. +- paragraph/list/container 모두 anchor offset, affinity, raw_xhtml 같은 실제 재주입 정보는 아직 없다. -제안 모듈: +즉 schema와 최소 메타 구조는 준비됐지만, runtime reconstruction 에 필요한 실제 preservation metadata 는 아직 충분하지 않다. -- `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 +### 4.3 patcher가 fragment replacement 중심으로 바뀌지 않았다 -`patch_builder.py` 는 최종적으로 orchestration thin layer가 된다. +`xhtml_patcher.py` 의 modify 경로는 아직 다음 두 방식만 사용한다. -## 6. 구현 범위와 비범위 +- `old_plain_text` + `new_plain_text` +- `new_inner_xhtml` -### 이번 설계 범위 +아직 없는 것: -- 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 기반 테스트 체계 구축 +- top-level element 전체 교체를 위한 `replace_fragment` +- DOM 삽입 기준의 inline anchor rehydration helper -### 이번 설계 비범위 +### 4.4 container 재구성이 없다 -- sidecar/rehydrator 전체를 단일 parser 체계로 통합하는 대형 리팩토링 -- testcase에 없는 custom macro 일반화 -- Confluence storage 전체에 대한 generic DOM diff 엔진 +아직 남은 핵심 공백: -parser 통합은 후속 과제로 남긴다. 이번 작업은 "reverse-sync를 구조적 재구성 경로로 전환"하는 데 집중한다. +- nested list child order 기반 재구성 +- callout body 재구성 +- details 재구성 +- ADF panel body 재구성 -## 7. 테스트 설계 +이 부분이 해결되지 않으면 `main`은 복잡한 block에서 계속 heuristic fallback에 의존하게 된다. -테스트는 두 묶음으로 나눈다. +### 4.5 mapping 계층이 여전히 런타임 기본 경로를 잡고 있다 -1. 설계 검증 테스트 -2. 회귀 방지 테스트 +현재 `SidecarEntry`, `load_sidecar_mapping()`, `build_mdx_to_sidecar_index()` 계층은 남아 있고, patch planning도 여기에 기대고 있다. -### 7.1 설계 검증 테스트 +장기 목표는 분명하다. -#### Level 0. Helper / invariant +- `RoundtripSidecar v3` 가 primary runtime artifact가 된다. +- `mapping.yaml` 계층은 디버그/보조 용도로 축소한다. -새 파일 제안: +## 5. 목표 아키텍처 -- `tests/test_reverse_sync_xhtml_normalizer.py` -- `tests/test_reverse_sync_reconstruction_offsets.py` -- `tests/test_reverse_sync_reconstruction_insert.py` +최종적으로 reverse-sync modified block 기본 경로는 아래 형태여야 한다. -검증 항목: +`MDX diff -> changed block identify -> reconstruct fragment -> restore lost info / preserved anchors -> replace top-level fragment -> forward verify` -- plain-text extraction이 original/emitted fragment에서 같은 규칙으로 동작하는지 -- old -> new offset mapping이 삽입/삭제/대체에 대해 안정적인지 -- raw anchor insertion이 DOM 파괴 없이 수행되는지 -- `hash + line_range` disambiguation이 duplicate content에서도 안정적인지 +여기서 핵심은 "텍스트 수정분 이식"이 아니라 "fragment 재구성 후 교체"다. -여기서 review의 Critical 이슈를 먼저 red test로 고정한다. +### 5.1 block 분류 -필수 red cases: +새 planner는 modified block를 네 종류로 나눈다. -1. paragraph + inline image -2. list item + image -3. duplicate hash candidate -4. namespace-bearing fragment normalization +#### A. Clean block -#### Level 1. Block reconstruction against exact fragment oracle +대상: -새 파일 제안: +- heading +- code block / code macro +- table +- hr +- preserved anchor가 없는 단순 paragraph -- `tests/test_reverse_sync_reconstruct_paragraph.py` -- `tests/test_reverse_sync_reconstruct_list.py` -- `tests/test_reverse_sync_reconstruct_container.py` +처리: -oracle: +- `mdx_to_storage.emit_block()` 으로 새 fragment 생성 +- 필요 시 `lost_info_patcher` 적용 +- `replace_fragment` 로 top-level element 전체 교체 -- 기본: `expected.roundtrip.json.blocks[].xhtml_fragment` -- nested child: `page.xhtml` 에서 xpath extraction +#### B. Inline-anchor block -검증 방식: +대상: -- unchanged MDX block를 reconstruct 했을 때 oracle fragment와 normalize-equal +- paragraph 내부 `ac:image`, `ac:link` 등 preservation unit +- list item 내부 inline image 또는 trailing preserved node -대표 파라미터: +처리: -- 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` +1. improved MDX block를 emit +2. emitted fragment의 `new_plain_text` 추출 +3. sidecar의 `old_plain_text`, `anchors` 로 offset 매핑 +4. DOM 기준으로 raw anchor XHTML 재삽입 +5. 최종 fragment replace -`544376004` 는 `tests/testcases` 가 아니므로, unit test에서는 해당 page에서 관련 fragment만 추출한 minimal fixture를 추가해도 된다. 이것은 review의 "새 fixture가 아예 불필요하다고 말하면 안 된다"는 지적에 대한 현실적 대응이다. +#### C. Ordered child block -#### Level 2. Changed block golden reconstruction +대상: -새 파일 제안: +- nested list +- callout +- details +- ADF panel body -- `tests/test_reverse_sync_reconstruction_goldens.py` +처리: -oracle: +- original child order를 sidecar metadata에 저장 +- improved MDX child block sequence를 파싱 +- child slot 단위로 재귀 reconstruct +- outer wrapper는 fragment-level로 다시 조립 -- `expected.reverse-sync.patched.xhtml` -- 필요한 경우 `expected.reverse-sync.mapping.original.yaml` / `expected.reverse-sync.result.yaml` +#### D. Opaque block 대상: -- `original.mdx` + `improved.mdx` + `expected.reverse-sync.*` 가 존재하는 16개 `tests/testcases` +- 현재 emitter가 재구성할 수 없는 custom macro +- testcase/metadata 규칙이 아직 없는 구조 -검증 방식: +처리: -- changed block만 reconstruct + page assembly 후 `expected.reverse-sync.patched.xhtml` 와 normalize-equal -- `expected.reverse-sync.result.yaml` 의 `status: pass` 케이스는 forward verify까지 exact pass +- 명시적 fail +- fixture 추가 후 범위 확장 -### 7.2 회귀 방지 테스트 +### 5.2 필요한 새 모듈 또는 책임 분리 -#### Level 3. Existing sidecar / byte-equal gates +현재 `main` 기준으로 필요한 후속 구현 축은 아래와 같다. -기존 테스트를 유지하고 schema v3에 맞춰 확장한다. +- `reverse_sync/reconstruction_planner.py` + - change -> reconstruction strategy 결정 +- `reverse_sync/reconstructors.py` + - paragraph/list/container별 fragment 재구성 +- `reverse_sync/sidecar.py` 확장 + - 실제 anchor/item/container metadata 기록 +- `reverse_sync/xhtml_patcher.py` 확장 + - `replace_fragment` 추가 + - fragment-level patch 적용기로 역할 축소 -- `tests/test_reverse_sync_sidecar_v2.py` -- `tests/test_reverse_sync_rehydrator.py` -- `tests/test_reverse_sync_byte_verify.py` +`patch_builder.py` 는 최종적으로 thin orchestration layer가 되거나, planner로 책임을 넘긴 뒤 축소되어야 한다. -변경점: +## 6. 단계별 구현 계획 -- `expected.roundtrip.json` builder/loader가 reconstruction metadata를 읽고 써야 한다 -- unchanged case에서는 여전히 21/21 byte-equal 유지 +### Phase 0. 공용 helper 추출 -#### Level 4. CLI / E2E +상태: 완료, `main` 반영됨 -기존 테스트를 유지하되 reconstruction path를 기본 경로로 바꾼다. +완료 기준: -- `tests/test_reverse_sync_cli.py` -- `tests/test_reverse_sync_e2e.py` -- `tests/test_reverse_sync_structural.py` +- `xhtml_normalizer.py` 추가 +- fragment normalize/plain-text extraction/xpath extraction 구현 +- list tree public API 확보 -여기에 다음을 추가한다. +### Phase 1. sidecar schema v3 기반선 -- `tests/reverse-sync/pages.yaml` 의 `expected_status: pass` 케이스는 새 경로에서도 계속 pass -- `expected_status: fail` 케이스는 failure type별로 하나씩 우선 red -> green 전환 +상태: 완료, `main` 반영됨 -우선순위는 아래 순으로 둔다. +완료 기준: -1. list/image -2. callout/code -3. callout/list -4. ADF panel +- `reconstruction` 필드 추가 +- strict v3 load 정착 +- `build_sidecar_identity_index()` / `find_sidecar_block_by_identity()` 도입 +- `BlockMapping` 기반 reconstruction metadata 생성 -### 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 | +- placeholder 수준의 anchor/item metadata를 실제 metadata로 채우기 +- planner 경로에서 identity helper 사용하기 -## 8. 단계별 구현 계획 +### Phase 2. clean block whole-fragment replacement -### Phase 0. 공용 helper 추출 +상태: 미완료 -- `xhtml_normalizer.py` 추가 -- `extract_plain_text()`, `normalize_fragment()`, `extract_fragment_by_xpath()` 구현 -- list tree helper public API 승격 +구현 항목: + +- `replace_fragment` patch action 추가 +- heading/code/table/simple paragraph를 fragment replacement로 전환 +- modified block에서 `new_plain_text` 기반 patch 의존도 제거 게이트: -- Level 0 helper tests green +- 기존 normalizer/sidecar 테스트 green 유지 +- simple modified 케이스에서 `expected.reverse-sync.patched.xhtml` normalize-equal -### Phase 1. sidecar schema v3 +### Phase 3. inline-anchor 및 list 재구성 -- `RoundtripSidecar` 에 reconstruction metadata 추가 -- builder/load/write/update 구현 -- `hash + line_range` 기반 identity helper 도입 +상태: 미완료 -게이트: +구현 항목: -- existing sidecar tests green -- unchanged 21개 `expected.roundtrip.json` roundtrip 유지 +- paragraph/list item anchor metadata builder +- old/new plain-text offset mapping helper +- raw anchor DOM insertion helper +- nested list tree 기반 reconstruction -### Phase 2. clean block whole-fragment replacement +우선 대상 fixture: -- heading/code/table/simple paragraph modified block를 reconstruction path로 전환 -- `replace_fragment` patch 추가 +- `tests/testcases` 내 list/image 혼합 케이스 +- `tests/reverse-sync/544376004` 게이트: -- simple modified golden cases green -- `transfer_text_changes()` 경로 없이 clean block 변경 처리 가능 - -### Phase 3. paragraph/list anchor reconstruction +- inline image가 있는 paragraph/list item 재구성 green +- duplicate hash 후보에서도 identity가 안정적으로 동작 -- inline anchor metadata builder -- offset mapping + DOM insertion helper -- list item + nested list reconstruction +### Phase 4. container 재구성 -게이트: - -- `544113141`, `544145591`, `692355151`, `880181257`, `883654669` -- `544376004` helper/unit case +상태: 미완료 -### Phase 4. container reconstruction +구현 항목: -- callout/details/ADF panel body reconstruction -- child slot order 기반 재귀 rebuild +- callout/details/ADF panel body child order 저장 +- child slot 기반 재귀 rebuild +- outer wrapper와 inner body 책임 분리 -게이트: +우선 대상 fixture: - `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` 관련 테스트 +- container fragment oracle normalize-equal +- changed-page golden 검증 통과 -### 11.2 부분 삭제 또는 대폭 축소 대상 +### Phase 5. 기본 경로 전환 및 legacy 축소 -아래 코드는 즉시 파일 전체를 지우기보다는, 새 경로가 기본이 된 뒤 내부 범위를 줄여야 한다. +상태: 미완료 -#### `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` - -권장 방향: +- `patch_builder.py` modified path를 reconstruction planner로 위임 +- `mapping.yaml` runtime 의존 축소 +- legacy text-transfer 경로를 explicit fallback 또는 제거 대상으로 전환 -- 기본 verify/push 경로에서는 생성하지 않는다 -- 필요 시 `--debug-mapping` 같은 명시적 플래그 뒤로 이동한다 +게이트: -#### `bin/converter/cli.py` 의 자동 `mapping.yaml` 생성 +- `tests/testcases` 의 16개 changed golden 통과 +- `tests/reverse-sync/pages.yaml` 의 `expected_status: pass` 케이스 유지 +- unsupported 구조는 silent corruption 없이 명시적 fail -삭제 또는 optional 화 대상: +## 7. 테스트 계획 -- `generate_sidecar_mapping()` 호출 블록 전체 +### 7.1 현재 반드시 유지할 기존 green 게이트 -사유: +- `tests/test_reverse_sync_xhtml_normalizer.py` +- `tests/test_reverse_sync_list_tree.py` +- `tests/test_reverse_sync_sidecar_v3.py` +- `tests/test_reverse_sync_rehydrator.py` + - unchanged testcase에 대한 splice byte-equal 검증 포함 -- forward convert 성공 여부와 mapping.yaml 생성은 본질적으로 분리되어야 한다 -- 새 설계의 중심 artifact는 `mapping.yaml` 이 아니라 `expected.roundtrip.json` / sidecar v3 다 +이 테스트들은 "이미 main에 들어온 기반이 깨지지 않는다"는 최소 조건이다. -### 11.3 유지 대상 +### 7.2 새로 추가할 설계 검증 테스트 -아래 모듈은 새 설계에서도 유지한다. +필수 신규 묶음: -#### 유지: `bin/reverse_sync/mapping_recorder.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` -이유: +검증 원칙: -- callout / ADF panel child xpath 추출 -- debug와 fixture 분석 -- nested fragment extraction helper +1. unchanged fragment는 `expected.roundtrip.json` 기준 exact 또는 normalize-equal +2. nested child fragment는 `page.xhtml` + xpath extraction 기준 +3. changed page는 `expected.reverse-sync.patched.xhtml` 기준 -다만 역할은 "runtime truth"가 아니라 "XHTML 분석/보조 도구"로 한정한다. +### 7.3 fixture 활용 원칙 -#### 유지: `bin/reverse_sync/lost_info_patcher.py` +| 자산 | 현재 수량 | 역할 | +|------|-----------|------| +| `tests/testcases/*/page.xhtml` | 21 | 원본 페이지, nested fragment 추출 | +| `tests/testcases/*/expected.roundtrip.json` | 21 | unchanged top-level fragment oracle | +| `tests/testcases/*/original.mdx` | 16 | reverse-sync original 입력 | +| `tests/testcases/*/improved.mdx` | 16 | reverse-sync changed 입력 | +| `tests/testcases/*/expected.reverse-sync.patched.xhtml` | 16 | changed-page golden | +| `tests/reverse-sync/*` | 42 | 실제 회귀 케이스 및 failure reproduction | +| `tests/reverse-sync/pages.yaml` | 66 entries | catalog + expected_status/failure_type/severity | -이유: +## 8. legacy 코드 정리 기준 -- 링크, emoticon, filename, image, adf-extension 복원은 여전히 필요하다 -- 다만 적용 위치는 modified fragment emit 후의 post-process 단계로 고정한다 +상세 삭제 대상과 범위는 `docs/plans/2026-03-15-reverse-sync-reconstruction-cleanup-scope.md` 에 별도로 정리한다. -#### 유지: `bin/reverse_sync/sidecar.py` 의 roundtrip core +다음 코드는 새 경로가 기본이 되기 전에는 제거하지 않는다. -유지 범위: +- `text_transfer.py` +- `list_patcher.py` +- `table_patcher.py` +- `inline_detector.py` +- `patch_builder.py` 내부 heuristic strategy 분기 +- `xhtml_patcher.py` 의 text-only modify 경로 -- `RoundtripSidecar` -- `SidecarBlock` -- `build_sidecar()` -- `load_sidecar()` -- `write_sidecar()` -- `verify_sidecar_integrity()` +반대로 아래 조건이 충족되면 정리 대상으로 넘긴다. -즉 sidecar 모듈 전체를 지우는 것이 아니라, 그 안의 `mapping.yaml` 서브계층만 걷어내는 방향이다. +1. changed golden 16개가 새 reconstruction 경로로 green +2. `expected_status: pass` 회귀 케이스 유지 +3. unsupported 구조는 명시적 fail +4. `mapping.yaml` 없이도 sidecar v3 기반 runtime planning 가능 -### 11.4 테스트 코드 삭제 범위 +그 시점부터는 legacy heuristic path를 기본 동작에서 제거하고, 필요하면 debug fallback으로만 남긴다. -새 구현으로 전환되면 다음 테스트 묶음은 제거 또는 대체되어야 한다. +## 9. 최종 승인 기준 -- `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` 전용 테스트 +이번 재구성 작업은 아래를 만족해야 완료로 본다. -대신 아래 묶음이 새 기본 세트가 된다. +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. `RoundtripSidecar v3` 의 `reconstruction` 필드가 실제 runtime metadata를 담는다. +5. `build_sidecar_identity_index()` / `find_sidecar_block_by_identity()` 기준 identity 가 planner에 통합되고, duplicate content에서도 안정적으로 동작한다. +6. unsupported 구조에서 silent corruption 없이 fail-closed가 유지된다. -- `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` +## 10. 판단 -### 11.5 실제 삭제 순서 +2026-03-15 기준 `main`은 재구성 설계의 출발점이 아니라 이미 Phase 0과 Phase 1을 흡수한 상태다. 따라서 앞으로의 계획은 "설계를 시작한다"가 아니라 "이미 들어온 기반선 위에서 modified path를 실제 fragment reconstruction으로 전환한다"여야 한다. -삭제는 아래 순서로 진행한다. +정리하면 현재의 정확한 방향은 다음과 같다. -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 생성 경로 제거 +- `xhtml_normalizer` 와 sidecar v3는 이미 완료된 기반선으로 본다. +- `#917` 이후 sidecar는 strict v3 와 개선된 metadata shape 를 가지지만, 아직 runtime reconstruction 에 필요한 preservation 정보는 충분하지 않다. +- reverse-sync 기본 경로는 아직 legacy patch 체인에 있으므로 Phase 2 이후가 본 작업이다. +- 다음 구현의 승패는 sidecar metadata를 실제 재구성 정보로 채우고, patcher/planner를 fragment replacement 중심으로 바꾸는 데 달려 있다. -이 순서를 지키면 "새 경로 추가 + 구 경로 잔존" 상태를 최소화할 수 있고, 유지보수 비용도 빠르게 줄일 수 있다. +이 기준으로 문서와 구현을 맞추면, 현재 `main`의 실제 상태와 앞으로의 작업 범위가 일관되게 연결된다. diff --git a/confluence-mdx/docs/plans/2026-03-15-reverse-sync-reconstruction-cleanup-scope.md b/confluence-mdx/docs/plans/2026-03-15-reverse-sync-reconstruction-cleanup-scope.md new file mode 100644 index 000000000..3bd73c857 --- /dev/null +++ b/confluence-mdx/docs/plans/2026-03-15-reverse-sync-reconstruction-cleanup-scope.md @@ -0,0 +1,428 @@ +# Reverse Sync 재구성 후 삭제 대상 범위 + +> 작성일: 2026-03-15 +> 기준 브랜치: `main` +> 기준 커밋: `9e0d43b91c2e47088274e13e82a5c2750e1529f9` +> 선행 문서: +> - `docs/plans/2026-03-13-reverse-sync-reconstruction-design.md` +> - `docs/plans/2026-03-13-reverse-sync-reconstruction-design-review.md` + +## 1. 문서 목적 + +이 문서는 reverse-sync 재구성 경로가 기본 동작이 된 뒤 정리할 legacy 코드와 테스트 범위를 별도로 기록한다. + +기존 설계 문서에서 삭제 대상 설명을 분리한 이유는 두 가지다. + +1. 설계 본문은 "무엇을 새로 만들 것인가"에 집중하고, 이 문서는 "무엇을 언제 제거할 것인가"를 다룬다. +2. 삭제 범위는 파일 단위, 함수 단위, 테스트 단위까지 내려가므로 별도 추적 문서가 더 적합하다. + +이 문서는 즉시 삭제 지시서가 아니다. 아래의 게이트를 통과한 뒤 단계적으로 적용하는 cleanup 범위 문서다. + +## 2. 삭제 시작 조건 + +아래 네 조건이 충족되기 전에는 본 문서의 대상 코드를 기본 경로에서 제거하지 않는다. + +1. `tests/testcases` 의 16개 changed golden 이 새 reconstruction 경로로 green 이다. +2. `tests/reverse-sync/pages.yaml` 의 `expected_status: pass` 케이스가 유지된다. +3. unsupported 구조는 silent corruption 없이 명시적 fail 로 전환된다. +4. runtime planning 이 `mapping.yaml` 없이 `RoundtripSidecar v3` 중심으로 동작한다. + +조건 충족 전 허용 범위: + +- 새 reconstruction 경로 옆에 legacy fallback 을 일시적으로 유지 +- debug flag 뒤에서 legacy path 호출 +- 비교 실험용 테스트 유지 + +조건 충족 후 원칙: + +- 동일 책임의 구 경로를 기본 동작에 남기지 않는다. +- 삭제가 어렵다면 최소한 import 경로와 CLI 기본 동작에서는 분리한다. +- cleanup 은 기능 PR과 별도 PR 로 분리할 수 있다. + +## 3. 대상 분류 + +정리 대상은 네 묶음이다. + +1. heuristic text transfer 계층 +2. modified patch planning 계층 +3. `mapping.yaml` runtime 계층 +4. 관련 테스트와 debug artifact 생성 경로 + +## 4. 완전 삭제 대상 + +이 섹션의 항목은 새 reconstruction 경로가 정착하면 역할이 전면 대체되는 코드다. + +### 4.1 `bin/reverse_sync/text_transfer.py` + +현재 책임: + +- `transfer_text_changes(mdx_old, mdx_new, xhtml_text)` 로 MDX plain text 변경을 XHTML plain text에 이식 +- 문자 단위 정렬과 부분 치환을 통해 XHTML 구조는 유지하고 텍스트만 바꾸는 전략 제공 + +삭제 이유: + +- 새 설계의 기본 경로는 text transfer 가 아니라 whole-fragment reconstruction 이다. +- inline anchor 보존은 plain-text offset + DOM insertion 으로 대체된다. +- callout, list, table, inline image/link 같이 XHTML 고유 구조가 중요한 블록에서 이 함수는 구조를 이해하지 못한다. + +현재 의존 경로: + +- `bin/reverse_sync/patch_builder.py` +- `bin/reverse_sync/list_patcher.py` +- `tests/test_reverse_sync_text_transfer.py` +- `tests/test_reverse_sync_cli.py` 의 helper 테스트 일부 + +함께 삭제 또는 교체할 테스트: + +- `tests/test_reverse_sync_text_transfer.py` +- `tests/test_reverse_sync_cli.py` 내 `align_chars`, `find_insert_pos`, `transfer_text_changes` 관련 테스트 +- `tests/test_reverse_sync_patch_builder.py` 중 `new_plain_text` 결과를 전제로 하는 케이스 일부 + +삭제 시점: + +- Phase 3 완료 후 +- paragraph/list anchor reconstruction 이 도입되고 +- `new_plain_text` modify path 가 기본 경로에서 제거된 뒤 + +### 4.2 `bin/reverse_sync/list_patcher.py` + +현재 책임: + +- list 변경을 item-level patch 또는 `new_inner_xhtml` 재생성으로 처리 +- 부모 list 매핑을 찾은 뒤 경우에 따라 `transfer_text_changes()` 로 폴백 +- `build_list_item_patches()` 를 통해 list 전용 patch 생성 + +삭제 이유: + +- 새 설계에서 list 는 list tree + sidecar reconstruction metadata + child order 기반으로 재구성한다. +- 현재 구현은 list 자체를 구조적으로 이해하기보다 flat item patch 와 text transfer 조합에 가깝다. +- nested list, duplicate item, inline image 가 섞인 항목에서 전략이 계속 분기한다. + +현재 의존 경로: + +- `bin/reverse_sync/patch_builder.py` +- `tests/test_reverse_sync_patch_builder.py` +- `tests/test_reverse_sync_cli.py` + +함께 삭제 또는 교체할 테스트: + +- `tests/test_reverse_sync_patch_builder.py` 의 `build_list_item_patches` 관련 테스트 전부 +- `tests/test_reverse_sync_cli.py` 의 `build_list_item_patches` 직접 호출 테스트 + +삭제 시점: + +- Phase 3 의 nested list reconstruction 완료 후 +- list/image 혼합 fixture 가 새 경로에서 green 인 뒤 + +### 4.3 `bin/reverse_sync/table_patcher.py` + +현재 책임: + +- table 변경을 row 단위 plain-text patch 로 생성 +- `build_table_row_patches()` 와 보조 유틸로 표 변경을 containing patch 로 환원 + +핵심 함수: + +- `build_table_row_patches()` +- `split_table_rows()` +- `normalize_table_row()` +- `is_markdown_table()` + +삭제 이유: + +- 새 설계에서 table 은 clean block 이며 whole-fragment replacement 대상이다. +- row-level text patch 는 Confluence 링크, macro, attribute 를 보존하는 구조적 방법이 아니다. +- table 은 emitter + lost-info 복원 경로로 처리하는 편이 단순하고 일관적이다. + +현재 의존 경로: + +- `bin/reverse_sync/patch_builder.py` +- `tests/test_reverse_sync_patch_builder.py` + +함께 삭제 또는 교체할 테스트: + +- `tests/test_reverse_sync_patch_builder.py` 의 table patch 전용 테스트 +- table row normalization 을 전제로 하는 heuristic 검증 테스트 + +삭제 시점: + +- Phase 2 clean block replacement 완료 후 +- table golden 이 새 경로에서 green 인 뒤 + +### 4.4 `bin/reverse_sync/inline_detector.py` + +현재 책임: + +- inline 마커 변경과 경계 이동을 감지 +- legacy patch 경로에서 text patch 와 `new_inner_xhtml` 재생성 간 분기 기준 제공 + +핵심 함수: + +- `has_inline_format_change()` +- `has_inline_boundary_change()` + +삭제 이유: + +- 새 planner 는 inline marker 변화 자체가 아니라 block kind 와 reconstruction metadata 를 기준으로 분기한다. +- inline code/bold/link 의 변경은 fragment reconstruction 이 직접 처리해야 하며, 별도 heuristic 감지기 없이도 동작해야 한다. + +현재 의존 경로: + +- `tests/test_reverse_sync_patch_builder.py` +- legacy patch 의사결정 로직 + +함께 삭제 또는 교체할 테스트: + +- `tests/test_reverse_sync_patch_builder.py` 의 `has_inline_format_change` 관련 테스트 +- `tests/test_reverse_sync_patch_builder.py` 의 `has_inline_boundary_change` 관련 테스트 + +삭제 시점: + +- Phase 2 이후 direct/list heuristic 분기가 planner 로 대체된 뒤 + +## 5. 대폭 축소 대상 + +이 섹션의 항목은 파일 전체 삭제가 아니라, legacy 책임을 덜어내고 일부 핵심만 남길 가능성이 큰 코드다. + +### 5.1 `bin/reverse_sync/patch_builder.py` + +현재 책임: + +- diff 와 mapping 을 결합해 patch 목록 생성 +- `direct`, `containing`, `list`, `table`, `skip` 전략 선택 +- insert/delete 생성 +- 일부 modified block 에 `new_inner_xhtml` 생성 +- 나머지는 `new_plain_text` 기반 text patch 생성 + +삭제 또는 이동 대상 책임: + +- `_flush_containing_changes()` +- `_resolve_mapping_for_change()` +- `transfer_text_changes()` 호출 기반 modified path +- list/table 전용 분기 +- `find_mapping_by_sidecar()` 중심 mapping lookup 분기 + +남길 가능성이 있는 책임: + +- add/delete orchestration +- alignment 를 이용한 insert anchor 계산 + +권장 최종 형태: + +- `reconstruction_planner.py` 로 전략 판단 이동 +- `patch_builder.py` 는 thin wrapper 또는 insert/delete 전용 helper 로 축소 +- 축소가 애매하면 파일 삭제 후 planner 내부로 흡수 + +함께 정리할 테스트: + +- `tests/test_reverse_sync_patch_builder.py` 의 heuristic branch 검증 대다수 +- `tests/test_reverse_sync_structural.py` 중 구 patch shape 에 의존하는 assertion + +### 5.2 `bin/reverse_sync/xhtml_patcher.py` + +현재 책임: + +- `insert`, `delete`, `modify` patch 적용 +- `modify` 에서 두 경로를 지원 + - `old_plain_text` + `new_plain_text` + - `new_inner_xhtml` +- CDATA 복원 + +삭제 대상 책임: + +- `new_plain_text` 기반 text-only modify path +- `_apply_text_changes()` 와 그에 딸린 text patch helper + +남겨야 할 책임: + +- XPath resolve +- `insert` +- `delete` +- 새 `replace_fragment` +- CDATA 복원 + +권장 최종 형태: + +- "텍스트 패처"가 아니라 "fragment-level DOM patcher" +- modify 는 `replace_fragment` 중심으로 단순화 + +함께 정리할 테스트: + +- `tests/test_reverse_sync_xhtml_patcher.py` 의 `new_plain_text` 중심 테스트 +- `tests/test_reverse_sync_mdx_to_xhtml_inline.py` 에서 patch shape 을 전제하는 일부 테스트 + +### 5.3 `bin/reverse_sync/mdx_to_xhtml_inline.py` + +현재 책임: + +- `mdx_block_to_inner_xhtml()` +- `mdx_block_to_xhtml_element()` +- innerHTML 교체에 맞춘 block 변환 + +삭제 후보 책임: + +- `mdx_block_to_inner_xhtml()` +- innerHTML 패치에 최적화된 변환 로직 전반 + +이유: + +- 새 설계의 기본 emitter 는 `mdx_to_storage.emit_block()` 이다. +- innerHTML 단위 치환은 clean block replacement 와 container reconstruction 의 최종 방향과 맞지 않는다. + +남길 가능성이 있는 책임: + +- migration 기간의 compatibility wrapper +- 특정 테스트에서만 쓰는 임시 adapter + +함께 정리할 테스트: + +- `tests/test_reverse_sync_mdx_to_xhtml_inline.py` 의 innerHTML 중심 테스트 +- `tests/test_reverse_sync_cli.py` 내 `new_inner_xhtml` 생성 전제 테스트 + +### 5.4 `bin/reverse_sync/sidecar.py` 의 `mapping.yaml` 계층 + +현재 책임: + +- block-level roundtrip sidecar 와 별개로 `mapping.yaml` 계층을 함께 보유 +- `SidecarEntry`, `SidecarChildEntry` +- `load_sidecar_mapping()` +- `build_mdx_to_sidecar_index()` +- `build_xpath_to_mapping()` +- `generate_sidecar_mapping()` +- `find_mapping_by_sidecar()` + +왜 축소 대상인가: + +- `RoundtripSidecar v3` 가 runtime primary artifact 로 올라가면 이 계층은 중복 책임이 된다. +- 현재는 reverse-sync CLI 와 converter 에서 legacy routing artifact 로 사용한다. +- `#917`에서 strict v3 sidecar 정리는 이미 끝났으므로, 남은 cleanup 초점은 v2 호환이 아니라 `mapping.yaml` runtime 계층 제거다. + +정리 방향: + +- sidecar core 와 mapping layer 를 논리적으로 분리 +- 최종적으로 mapping layer 는 debug 전용 또는 optional artifact + +함께 정리할 테스트: + +- `tests/test_reverse_sync_sidecar.py` 의 mapping.yaml 관련 절반 이상 +- `tests/test_lost_info_collector.py` 의 `generate_sidecar_mapping()` 연동 테스트 일부 +- `tests/test_reverse_sync_e2e.py` 의 mapping.yaml 생성 전제 경로 + +## 6. CLI 와 호출 경로 정리 대상 + +### 6.1 `bin/reverse_sync_cli.py` + +현재 legacy 연동: + +- `generate_sidecar_mapping()` 호출 +- `SidecarEntry`, `SidecarChildEntry` import +- mapping artifact 생성 및 사용 +- `patch_builder.py` + `xhtml_patcher.py` 기반 patch 적용 + +정리 목표: + +- runtime 기본 경로에서 `mapping.yaml` 생성 제거 +- reconstruction planner + sidecar v3 중심 orchestration 으로 대체 +- mapping artifact 는 `--debug-mapping` 같은 명시적 옵션 뒤로 이동 가능 + +### 6.2 `bin/converter/cli.py` + +현재 legacy 연동: + +- forward converter 성공 후 `generate_sidecar_mapping()` 호출 + +정리 목표: + +- converter 성공과 mapping.yaml 생성의 결합 해제 +- sidecar v3 또는 debug artifact 생성 여부를 옵션으로 분리 + +## 7. 테스트 정리 대상 + +cleanup 이후 제거 또는 대체해야 할 테스트 묶음을 명시한다. + +### 7.1 제거 대상 + +- `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_mdx_to_xhtml_inline.py` 의 innerHTML 중심 테스트 다수 +- `tests/test_reverse_sync_cli.py` 의 text-transfer helper 직접 테스트 +- `tests/test_reverse_sync_sidecar.py` 의 mapping.yaml 전용 테스트 다수 + +### 7.2 대체 대상 + +아래 테스트 묶음으로 대체한다. + +- `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` + +## 8. 유지 대상 + +cleanup 이후에도 유지해야 할 코드를 분리해 둔다. + +### 8.1 유지: `bin/reverse_sync/mapping_recorder.py` + +이유: + +- XHTML 분석 도구로는 계속 유용하다. +- callout/ADF child xpath 추출, fixture 분석, debug helper 역할이 남는다. + +단, 역할은 runtime truth 가 아니라 analysis/debug helper 로 한정한다. + +### 8.2 유지: `bin/reverse_sync/lost_info_patcher.py` + +이유: + +- link, image, emoticon, filename, ADF extension 복원은 reconstruction 이후에도 필요하다. +- 다만 text patch helper 와 묶이지 않고 fragment post-process 단계로 고정해야 한다. + +### 8.3 유지: `bin/reverse_sync/sidecar.py` 의 roundtrip core + +유지 범위: + +- `RoundtripSidecar` +- `SidecarBlock` +- `build_sidecar()` +- `load_sidecar()` +- `write_sidecar()` +- `verify_sidecar_integrity()` +- `build_sidecar_identity_index()` +- `find_sidecar_block_by_identity()` + +즉 sidecar 모듈 전체 삭제가 아니라, 그 안의 legacy mapping 계층 분리가 목표다. + +## 9. 실제 정리 순서 + +권장 순서는 아래와 같다. + +1. Phase 2 clean block replacement 구현 +2. Phase 3 inline-anchor/list reconstruction 구현 +3. Phase 4 container reconstruction 구현 +4. changed golden 16개와 `expected_status: pass` 회귀 케이스 green 확보 +5. `patch_builder.py` 의 modified path 를 reconstruction planner 로 전환 +6. `xhtml_patcher.py` 에서 `new_plain_text` modify path 제거 +7. `text_transfer.py`, `inline_detector.py` 삭제 +8. `list_patcher.py`, `table_patcher.py` 삭제 +9. `mdx_to_xhtml_inline.py` 축소 또는 삭제 +10. `mapping.yaml` runtime 계층을 CLI 기본 동작에서 분리 +11. 관련 테스트 삭제 및 reconstruction 테스트로 교체 + +이 순서를 지키는 이유: + +- 먼저 새 경로를 green 으로 만든 뒤 구 경로를 제거해야 한다. +- `mapping.yaml` 계층은 가장 마지막까지 남을 가능성이 높다. +- 테스트 삭제가 코드 삭제보다 먼저 오면 회귀 검출 능력을 잃는다. + +## 10. 판단 + +삭제 범위는 넓지만, 모든 항목이 같은 시점에 제거되는 것은 아니다. 실제로는 아래 순서로 흡수된다. + +- 가장 먼저 사라질 것: `text_transfer.py`, `inline_detector.py` +- 다음으로 사라질 것: `list_patcher.py`, `table_patcher.py` +- 마지막까지 남을 수 있는 것: `patch_builder.py` 일부, `xhtml_patcher.py` 일부, `mapping.yaml` 계층 + +즉 cleanup 의 핵심은 "파일 수를 줄이는 것"이 아니라, runtime 기본 경로에서 heuristic text patch 체인을 걷어내고 sidecar v3 + reconstruction 중심 구조로 책임을 재배치하는 것이다.