From 70dae4fee2408dfd0c1b45ee5dfe7a1cbf0e34a4 Mon Sep 17 00:00:00 2001 From: JK Date: Sat, 25 Apr 2026 23:25:08 +0900 Subject: [PATCH 1/5] =?UTF-8?q?confluence-mdx:=20reverse-sync=20=EB=8C=80?= =?UTF-8?q?=EC=B2=B4=20=EA=B3=84=ED=9A=8D=20=EB=AC=B8=EC=84=9C=EB=A5=BC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 2026-04-25 기준의 새 reverse-sync replacement plan을 작성합니다. - 기존 3월 phase/cleanup/review 계획 문서에 superseded 안내를 추가합니다. - capability registry, planner/strategy/proof 분리, verifier taxonomy 재정의를 중심으로 새 계획을 정리합니다. Co-Authored-By: Claude Opus 4.6 --- ...verse-sync-reconstruction-design-review.md | 4 + ...3-13-reverse-sync-reconstruction-design.md | 4 + ...verse-sync-reconstruction-cleanup-scope.md | 4 + ...026-04-25-reverse-sync-replacement-plan.md | 462 ++++++++++++++++++ 4 files changed, 474 insertions(+) create mode 100644 confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md 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 index a8b02493e..7b9e5587b 100644 --- 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 @@ -1,5 +1,9 @@ # Reverse Sync 전면 재구성 설계 — 검토 평가 결과 (v5) +> 상태: Superseded review +> 대체 문서: `2026-04-25-reverse-sync-replacement-plan.md` +> 비고: 이 문서는 2026-03 시점 설계안에 대한 검토 기록입니다. 현재 계획 논의는 새 replacement plan을 기준으로 진행합니다. + > 검토 대상: `2026-03-13-reverse-sync-reconstruction-design.md` > 검토일: 2026-03-13 > 검토 기준 버전: PR #913 head `33fa095e56cb26766995b3930d3616a58559685e` 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 efc0208cc..16570429b 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,5 +1,9 @@ # Reverse Sync 전면 재구성 설계 +> 상태: Superseded +> 대체 문서: `2026-04-25-reverse-sync-replacement-plan.md` +> 비고: 이 문서는 2026-03 시점의 phase 계획과 당시의 코드 상태를 보존하기 위한 기록입니다. 현재 작업 계획은 새 replacement plan을 기준으로 합니다. + > 최초 작성일: 2026-03-13 > 갱신일: 2026-03-19 (4차 — Phase 5 진행 현황 반영 및 설계 기준선 교정) > 기준 브랜치: `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 index ac3ffdcb5..89d1b94b8 100644 --- 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 @@ -1,5 +1,9 @@ # Reverse Sync 재구성 후 삭제 대상 범위 +> 상태: Superseded +> 대체 문서: `2026-04-25-reverse-sync-replacement-plan.md` +> 비고: 이 문서는 2026-03 시점 cleanup 가정과 삭제 범위를 담고 있습니다. 현재 cleanup 계획은 새 replacement plan의 단계별 실행 계획을 기준으로 합니다. + > 작성일: 2026-03-15 > 갱신일: 2026-03-16 > 기준 브랜치: `main` diff --git a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md new file mode 100644 index 000000000..5443d0995 --- /dev/null +++ b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md @@ -0,0 +1,462 @@ +# Reverse Sync 대체 계획 (2026-04-25) + +> 상태: Draft +> 대상: `confluence-mdx/bin/reverse_sync/`, `bin/reverse_sync_cli.py`, `tests/`, `docs/` +> 목적: 기존 reconstruction phase 문서를 현재 코드 기준으로 대체하고, 앞으로의 개선 작업을 "구현 현실"에 맞는 계획으로 다시 정의합니다. + +--- + +## 1. 이 문서가 대체하는 기존 계획 문서 + +이 문서는 아래 3개 문서를 포괄적으로 대체합니다. + +- `2026-03-13-reverse-sync-reconstruction-design.md` +- `2026-03-13-reverse-sync-reconstruction-design-review.md` +- `2026-03-15-reverse-sync-reconstruction-cleanup-scope.md` + +기존 문서들은 당시로서는 타당한 문제의식과 설계 방향을 담고 있었지만, 현재 main의 구현 상태와 다음 이유로 어긋납니다. + +1. 이미 완료된 작업을 아직 계획 단계처럼 서술하는 부분이 있습니다. +2. 삭제된 모듈(`text_transfer.py`, `list_patcher.py`, `table_patcher.py`)을 전제로 하는 cleanup 항목이 남아 있습니다. +3. `mapping.yaml`과 roundtrip sidecar v3의 실제 역할 분리가 문서 구조에 충분히 반영되지 않았습니다. +4. 현재의 핵심 위험이 "재구성 자체의 부재"보다 "patch_builder 정책 집중 + verifier 결합 + capability 경계 불명확"으로 이동했는데, 예전 문서는 이 상태를 담지 못합니다. + +이 문서는 따라서 "예전 계획의 6차 개정판"이 아니라, 현재 구현을 출발점으로 다시 세운 replacement plan입니다. + +--- + +## 2. 현재 기준선 + +현재 reverse-sync의 사실상 표준 경로는 다음과 같습니다. + +1. 원본/교정 MDX 로드 +2. `parse_mdx_blocks()` + `diff_blocks()` +3. `record_mapping(page.xhtml)` +4. `generate_sidecar_mapping()` / `mapping.yaml` +5. `build_sidecar()` / `expected.roundtrip.json` schema v3 +6. `build_patches()` +7. `patch_xhtml()` +8. patched XHTML 재기록 +9. forward converter 재실행 +10. `verify_roundtrip()` +11. pass 시에만 push 후보로 취급 + +현재 구현의 핵심 특성: + +- reverse-sync는 text patcher가 아니라 sidecar 기반 fragment-preserving system입니다. +- `patch_builder.py`가 실제 정책 엔진입니다. +- `mapping.yaml`은 lookup/lost_info 계층이고, roundtrip sidecar v3는 identity/reconstruction 계층입니다. +- list, preserved anchor, table, parameter-bearing container는 여전히 별도 capability boundary를 갖습니다. +- verifier와 forward converter의 정규화 특성 변화가 reverse-sync pass/fail에 직접 영향을 줍니다. + +이 기준선은 "무엇을 새로 만들 것인가"보다 먼저 "무엇을 더 이상 가정하지 말아야 하는가"를 정해줍니다. + +--- + +## 3. 이번 계획의 목표 + +### 3.1 최종 목표 + +reverse-sync를 "모든 MDX 변경을 무조건 Confluence에 반영하는 시스템"으로 만들겠다는 목표는 현실적이지 않습니다. 대신 다음을 목표로 삼습니다. + +1. 지원 범위를 capability 단위로 명확히 문서화합니다. +2. 지원 범위 안에서는 roundtrip proof까지 포함해 신뢰할 수 있게 만듭니다. +3. 지원 범위 밖에서는 skip/fail 이유가 코드와 문서에 일관되게 드러나게 만듭니다. +4. `patch_builder.py`의 전략 집중을 완화해 회귀 수정 비용을 줄입니다. +5. verifier/fixture/manifest 체계를 재정리해 "왜 pass/fail인지"를 추적 가능하게 만듭니다. + +### 3.2 비목표 + +이번 계획의 비목표는 다음과 같습니다. + +- Confluence XHTML의 모든 구조를 일반 emitter만으로 재생성하기 +- skip reason을 전부 제거하기 +- 테이블/복합 anchor/container를 단기간에 완전 지원하기 +- reverse-sync를 forward converter와 완전히 분리된 서브시스템으로 만들기 + +즉, 목표는 "전면 자동화"가 아니라 "지원 범위의 명확화 + 구조적 유지보수성 향상"입니다. + +--- + +## 4. 설계 원칙 + +### 4.1 fail-closed를 유지합니다 + +지원 범위가 애매한 변경은 계속 skip/fail-closed로 남겨야 합니다. coverage 없이 공격적으로 허용하는 방향은 반복 회귀를 다시 부릅니다. + +### 4.2 sidecar를 진실원으로 취급하지 않고, 계약(contract)으로 취급합니다 + +- `mapping.yaml`은 구조 lookup 계약입니다. +- roundtrip sidecar v3는 fragment identity 계약입니다. +- 둘 중 하나만으로 전체 문제를 해결하려고 하면 문서와 코드가 다시 어긋납니다. + +### 4.3 정책과 실행을 분리합니다 + +현재 `patch_builder.py`에는 다음이 함께 들어 있습니다. + +- capability 판별 +- fallback 선택 +- patch shape 생성 +- skip reason 생성 +- 일부 list/table 전용 로직 + +앞으로는 planner와 strategy 실행을 분리해야 합니다. + +### 4.4 테스트는 "설계 검증"과 "회귀 방지"를 분리합니다 + +예전 문서는 둘을 한 축에 놓는 경향이 있었고, 그 결과 oracle이 흐려졌습니다. 앞으로는 테스트를 다음으로 분리합니다. + +- 계약 테스트(contract tests) +- strategy 단위 회귀 테스트 +- page fixture 기반 통합 테스트 +- batch manifest 기반 상태 리포트 + +### 4.5 문서와 코드의 시간축을 분리합니다 + +현황 문서는 현재 코드가 하는 일을 적고, 계획 문서는 앞으로 바꿀 일을 적어야 합니다. 둘을 한 문서에 섞지 않습니다. + +--- + +## 5. 핵심 문제 재정의 + +현재 reverse-sync의 가장 큰 문제는 "아직 reconstruction이 덜 구현되었다"가 아닙니다. 현재는 다음 5개가 더 중요합니다. + +### P1. capability 경계가 코드에만 있고 문서/테스트 표면에 명시되지 않습니다 + +예: + +- 어떤 table은 replace 가능하고 어떤 table은 skip됩니다. +- 어떤 preserved anchor list는 처리되고 어떤 것은 여전히 위험합니다. +- 어떤 container는 reconstruction 경로를 타지만 parameter-bearing container는 별도 취급됩니다. + +하지만 이 차이가 문서/테스트/CLI 결과에서 일관된 capability taxonomy로 드러나지 않습니다. + +### P2. `patch_builder.py`가 planner + strategy dispatcher + partial executor 역할을 동시에 수행합니다 + +이 구조에서는 새 회귀가 나올 때마다 if/elif를 추가하는 방향으로 수렴하기 쉽습니다. + +### P3. verifier와 converter 정규화가 reverse-sync 결과 해석을 어렵게 만듭니다 + +pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicalization 변화인지, verifier normalization 차이인지 즉시 분리되지 않는 경우가 있습니다. + +### P4. fixture/manifest는 많지만 계층이 섞여 있습니다 + +- `tests/testcases/` +- `tests/reverse-sync/pages.yaml` +- unit test +- e2e regression fixture + +각 자산의 역할이 겹치고, 어떤 테스트가 "정책 계약"을 검증하는지 명확하지 않은 부분이 있습니다. + +### P5. 계획 문서가 실제 main과 엇갈리며, 삭제된/완료된 축을 계속 참조합니다 + +이 상태에서는 새 작업을 시작할 때마다 먼저 문서를 해석해야 하므로, 문서가 가이드보다 부담이 됩니다. + +--- + +## 6. 목표 아키텍처 + +이번 계획의 목표 아키텍처는 "새 시스템을 다시 처음부터 만들자"가 아니라, 현재 구조를 다음처럼 재배치하는 것입니다. + +### 6.1 Planner / Strategy / Proof 3계층 + +1. Planner + - 변경 블록을 capability 단위로 분류 + - 사용 가능한 identity source 결정 + - strategy 후보 결정 + - skip/fail reason 결정 + +2. Strategy + - direct edit + - fragment replacement + - container reconstruction + - list strategy + - table strategy + - preserved-anchor template rewrite + +3. Proof + - patch 적용 + - forward roundtrip + - result classification + - push eligibility + +현재는 이 셋이 `patch_builder.py`와 `reverse_sync_cli.py` 사이에 뒤섞여 있습니다. 앞으로는 책임 경계를 명시해야 합니다. + +### 6.2 Capability registry 도입 + +다음과 같은 capability 단위를 문서와 코드 양쪽에 명시적으로 도입합니다. + +- `paragraph_text_edit` +- `heading_text_edit` +- `inline_whitespace_adjustment` +- `clean_block_replace` +- `container_body_reconstruction` +- `preserved_anchor_template_rewrite` +- `list_visible_segment_edit` +- `table_fragment_replace` +- `unsafe_table_edit` +- `unsupported_complex_anchor_structure` + +중요한 것은 naming 그 자체보다, 각 capability가 아래를 갖는 것입니다. + +- 지원 여부 +- strategy owner +- skip/fail reason +- 대표 fixture +- regression test location + +### 6.3 Result taxonomy 표준화 + +현재 `pass/fail/no_changes/skipped_changes`는 존재하지만, 더 세밀한 해석이 필요합니다. 계획상 목표 taxonomy는 다음과 같습니다. + +- `pass` +- `pass_with_normalization` +- `no_changes` +- `skipped_capability_boundary` +- `failed_patch_generation` +- `failed_roundtrip` +- `blocked_push_safety` + +이 taxonomy는 CLI, result.yaml, 문서, 테스트 설명이 공통으로 써야 합니다. + +--- + +## 7. 단계별 실행 계획 + +### Stage 0. 문서/분류 기준선 확정 + +목표: + +- current-state 문서와 plan 문서의 역할을 분리합니다. +- capability registry 초안을 문서에 먼저 정의합니다. +- 기존 plan 문서를 superseded 상태로 전환합니다. + +산출물: + +- 현재 구현 상태 문서 +- 본 replacement plan +- 기존 plan 문서 superseded 표기 + +완료 기준: + +- 새 작업이 더 이상 3월 phase 문서를 기준으로 시작되지 않습니다. + +### Stage 1. capability matrix와 fixture inventory 정리 + +목표: + +- 현재 처리 가능한 변경을 capability 기준으로 표에 정리합니다. +- `pages.yaml`, testcase fixture, unit test 사이의 관계를 표준화합니다. + +작업: + +1. `failure_type`, label, severity를 capability/skip reason과 매핑합니다. +2. 각 capability에 대표 fixture를 1~3개씩 지정합니다. +3. `expected_status`가 무엇을 뜻하는지 문서에서 재정의합니다. +4. unsupported 영역을 명시적으로 목록화합니다. + +완료 기준: + +- "이 케이스는 왜 아직 안 되나"를 capability 이름으로 설명할 수 있습니다. +- 회귀 이슈가 들어오면 어느 capability에 속하는지 바로 분류할 수 있습니다. + +### Stage 2. `patch_builder.py` 분해 1차 — planner 추출 + +목표: + +- `patch_builder.py`에서 정책 판단과 patch 생성 책임을 분리합니다. + +작업: + +1. planner 입력/출력 구조 정의 + - change + - mapping source + - sidecar source + - capability + - chosen strategy + - skip reason +2. strategy dispatcher 인터페이스 정의 +3. 기존 `build_patches()`는 planner + strategy executor orchestration으로 축소 + +완료 기준: + +- 새 회귀 수정 시 `build_patches()` 본체에 직접 if/elif를 추가하지 않고, planner rule 또는 strategy handler 수정으로 끝낼 수 있습니다. + +### Stage 3. list / preserved anchor / container 전략 분리 + +목표: + +- 현재 가장 복잡한 capability를 독립 전략 모듈로 분리합니다. + +우선순위: + +1. list visible-segment 전략 +2. preserved-anchor template rewrite 전략 +3. parameter-bearing container reconstruction 전략 + +완료 기준: + +- 각 전략이 독립 테스트 파일과 fixture 세트를 가집니다. +- `patch_builder.py`는 분기 지점만 남고 세부 알고리즘을 직접 품지 않습니다. + +### Stage 4. table 경계 명시화 + +목표: + +- table 지원을 넓히기 전에 먼저 boundary를 명확히 합니다. + +작업: + +1. 현재 skip reason을 capability 문서와 1:1로 연결 +2. markdown table / raw HTML table / preserved-anchor table을 명확히 분리 +3. 어떤 table이 "지원 대상"인지 acceptance contract를 먼저 고정 + +완료 기준: + +- table 관련 버그가 들어왔을 때 "미지원"과 "회귀"를 혼동하지 않습니다. +- 새로운 지원 확대는 capability 추가로 표현합니다. + +### Stage 5. verifier contract 정리 + +목표: + +- patch quality 문제와 normalization 문제를 분리합니다. + +작업: + +1. `verify_roundtrip()` 결과를 reason code 중심으로 재분류 +2. `--lenient`, `--no-normalize`의 의미를 문서와 테스트에서 고정 +3. converter normalization 변화로 인한 false failure를 별도 카테고리로 분리 + +완료 기준: + +- fail 원인을 `patch mismatch` / `normalization mismatch` / `forward canonicalization drift`로 구분할 수 있습니다. + +### Stage 6. push rollout contract 정리 + +목표: + +- verify pass와 push eligibility를 같은 개념으로 취급하지 않도록 정리합니다. + +작업: + +1. push safety gate 문서화 +2. conflict / remote drift / dry-run / human confirmation 경계 정리 +3. CLI 출력과 result.yaml에 push-blocked reason 추가 + +완료 기준: + +- "verify는 통과했지만 왜 push하지 않았는가"가 결과에 명시됩니다. + +--- + +## 8. 테스트 계획 + +### 8.1 테스트 계층 + +1. Contract tests + - sidecar schema + - planner classification + - result taxonomy + +2. Strategy tests + - list visible segment + - preserved anchor rewrite + - container reconstruction + - table classification + +3. Fixture integration tests + - `tests/testcases/` + - `original.mdx` / `improved.mdx` / `expected.reverse-sync.patched.xhtml` + +4. Batch manifest tests + - `tests/reverse-sync/pages.yaml` + - capability/expected_status/failure_type alignment 검증 + +### 8.2 테스트 원칙 + +- 새 capability를 추가할 때는 representative fixture를 반드시 함께 추가합니다. +- skip reason을 바꿀 때는 result taxonomy 테스트도 함께 갱신합니다. +- verifier normalization 변경은 strategy 테스트가 아니라 verifier contract 테스트로 검증합니다. +- pages.yaml은 단순 case catalog가 아니라 regression reporting asset으로 관리합니다. + +### 8.3 우선 추가할 테스트 + +1. planner classification snapshot test +2. table skip reason classification test +3. list visible segment capability fixture set +4. verifier reason-code split test +5. result.yaml schema test + +--- + +## 9. 문서 구조 계획 + +최종적으로 reverse-sync 문서는 다음 구조를 목표로 합니다. + +- `docs/architecture.md` + - 현재 코드가 실제로 어떻게 동작하는지 설명 +- `docs/analysis-reverse-sync-refactoring.md` + - 현재 구현 상태와 구조적 리스크 분석 +- `docs/plans/2026-04-25-reverse-sync-replacement-plan.md` + - 앞으로의 변경 계획 +- (선택) capability matrix 문서 + - 지원/미지원 범위를 표로 관리 + +즉, 현황 / 분석 / 계획을 분리합니다. + +--- + +## 10. 기존 계획 문서에서 유지할 것과 버릴 것 + +### 유지할 것 + +- fail-closed 원칙 +- sidecar 기반 identity 보존 방향 +- reconstruction 중심 접근 +- 테스트 우선 사고 +- regression gate를 먼저 세우는 태도 + +### 버릴 것 + +- phase 번호 중심 서술 +- 이미 삭제된 모듈을 기준으로 한 cleanup 계획 +- `mapping.yaml` 단독 설명으로 전체 reverse-sync를 설명하려는 방식 +- 구현 완료/미완료 상태와 계획을 한 문서에 섞는 방식 + +--- + +## 11. 승인 기준 + +이 계획은 다음 상태가 되면 실행 가능하다고 봅니다. + +1. capability registry의 1차 naming이 합의됩니다. +2. planner 분리 범위가 합의됩니다. +3. table을 "당장 더 지원할 것인지"보다 "경계를 먼저 고정할 것인지"에 대한 방향이 합의됩니다. +4. verifier reason taxonomy가 합의됩니다. + +즉, 코드 착수 전 필요한 것은 "모든 세부 구현의 확정"이 아니라, 앞으로 무엇을 지원 capability로 보고 무엇을 boundary로 둘지에 대한 합의입니다. + +--- + +## 12. 리뷰에서 확인받고 싶은 쟁점 + +이 PR은 Draft이며, 아래 쟁점에 대한 리뷰를 받고 싶습니다. + +1. capability registry naming이 과한지, 아니면 필요한지 +2. table을 지원 확대보다 경계 명시화부터 하는 우선순위가 맞는지 +3. planner/strategy/proof 3계층 분리가 실제 코드 정리의 중심축으로 적절한지 +4. `pages.yaml`를 계속 manifest+metadata 겸용으로 둘지, 역할을 분리할지 +5. verifier taxonomy를 세분화하는 것이 운영상 도움이 되는지 + +--- + +## 13. 결론 + +예전 계획의 핵심 직감은 맞았습니다. reverse-sync는 heuristic patch를 계속 덧대는 방향만으로는 안정화되지 않습니다. 다만 현재 시점에는 이미 많은 reconstruction/sidecar 기반 구현이 main에 들어와 있으므로, 이제 필요한 것은 "새 phase를 더 추가하는 문서"가 아니라 다음입니다. + +- 현재 구조를 capability 기준으로 다시 설명하고 +- 정책 엔진을 분해할 수 있는 계획을 세우고 +- 지원 범위와 한계를 문서/테스트/CLI에서 같은 언어로 표현하는 것 + +이 문서는 그 전환을 위한 새로운 기준 문서입니다. From edb5ebeed83f97d77ef44a95b152af5ffaabad8d Mon Sep 17 00:00:00 2001 From: JK Date: Sun, 26 Apr 2026 00:17:14 +0900 Subject: [PATCH 2/5] =?UTF-8?q?confluence-mdx:=20reverse-sync=20=EB=8C=80?= =?UTF-8?q?=EC=B2=B4=20=EA=B3=84=ED=9A=8D=20=EB=AC=B8=EC=84=9C=EC=9D=98=20?= =?UTF-8?q?=ED=95=A9=EC=9D=98=20=EB=B0=A9=ED=96=A5=EC=9D=84=20=EB=B0=98?= =?UTF-8?q?=EC=98=81=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replacement plan을 현재 기준의 주 문서로 명시합니다 - planner/strategy/proof, table verifier normalization, pages.yaml 역할 합의를 반영합니다 - verifier taxonomy를 Accept/Review/Block/Error 기준으로 구체화합니다 Co-Authored-By: Claude Opus 4.6 --- ...026-04-25-reverse-sync-replacement-plan.md | 85 +++++++++++-------- 1 file changed, 50 insertions(+), 35 deletions(-) diff --git a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md index 5443d0995..11e35ad06 100644 --- a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md +++ b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md @@ -22,6 +22,7 @@ 4. 현재의 핵심 위험이 "재구성 자체의 부재"보다 "patch_builder 정책 집중 + verifier 결합 + capability 경계 불명확"으로 이동했는데, 예전 문서는 이 상태를 담지 못합니다. 이 문서는 따라서 "예전 계획의 6차 개정판"이 아니라, 현재 구현을 출발점으로 다시 세운 replacement plan입니다. +이 문서를 현재 기준의 주 문서로 삼고, 3월 문서들은 historical record로만 남깁니다. --- @@ -139,14 +140,14 @@ reverse-sync를 "모든 MDX 변경을 무조건 Confluence에 반영하는 시 pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicalization 변화인지, verifier normalization 차이인지 즉시 분리되지 않는 경우가 있습니다. -### P4. fixture/manifest는 많지만 계층이 섞여 있습니다 +### P4. fixture/reporting 자산은 많지만 taxonomy가 아직 정리되지 않았습니다 - `tests/testcases/` - `tests/reverse-sync/pages.yaml` - unit test - e2e regression fixture -각 자산의 역할이 겹치고, 어떤 테스트가 "정책 계약"을 검증하는지 명확하지 않은 부분이 있습니다. +실데이터 기준으로 `pages.yaml`의 역할은 이미 비교적 명확합니다. reverse-sync 입장에서는 참조용 metadata catalog이며, 테스트 구현 과정에서만 추가 필드가 얹혀 있습니다. 문제는 `pages.yaml` 자체보다, capability / expected_status / verifier outcome을 어떤 taxonomy로 읽을지 아직 정리되지 않았다는 점입니다. ### P5. 계획 문서가 실제 main과 엇갈리며, 삭제된/완료된 축을 계속 참조합니다 @@ -207,17 +208,27 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza ### 6.3 Result taxonomy 표준화 -현재 `pass/fail/no_changes/skipped_changes`는 존재하지만, 더 세밀한 해석이 필요합니다. 계획상 목표 taxonomy는 다음과 같습니다. - -- `pass` -- `pass_with_normalization` -- `no_changes` -- `skipped_capability_boundary` -- `failed_patch_generation` -- `failed_roundtrip` -- `blocked_push_safety` - -이 taxonomy는 CLI, result.yaml, 문서, 테스트 설명이 공통으로 써야 합니다. +현재 `pass/fail/no_changes/skipped_changes`는 존재하지만, 더 세밀한 운영 의미가 필요합니다. 특히 table, whitespace, column-width 같은 formatting 수준 차이를 semantic mismatch와 분리해야 합니다. 계획상 목표 taxonomy는 다음 4계층으로 둡니다. + +1. Accept + - `exact_match` + - `normalized_match` + - `formatting_only_match` +2. Review + - `semantic_diff_needs_review` + - `reconstruction_needs_review` + - `preservation_uncertain` +3. Block + - `capability_boundary_blocked` + - `missing_identity` + - `unsafe_edit` + - `ambiguous_target` +4. Error + - `patch_generation_error` + - `roundtrip_verification_error` + - `reconversion_error` + +이 taxonomy는 CLI, result.yaml, 문서, 테스트 설명이 공통으로 써야 합니다. 구현 세부 enum은 더 쪼갤 수 있지만, 문서와 운영 보고는 위 중간 해상도를 기준으로 맞춥니다. --- @@ -246,13 +257,13 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 목표: - 현재 처리 가능한 변경을 capability 기준으로 표에 정리합니다. -- `pages.yaml`, testcase fixture, unit test 사이의 관계를 표준화합니다. +- `pages.yaml`를 실데이터 기준의 reference metadata catalog로 문서화하고, 테스트 전용 부가 필드와 구분해 설명합니다. 작업: 1. `failure_type`, label, severity를 capability/skip reason과 매핑합니다. 2. 각 capability에 대표 fixture를 1~3개씩 지정합니다. -3. `expected_status`가 무엇을 뜻하는지 문서에서 재정의합니다. +3. `expected_status`와 verifier outcome이 taxonomy 어디에 매핑되는지 문서에서 재정의합니다. 4. unsupported 영역을 명시적으로 목록화합니다. 완료 기준: @@ -303,34 +314,37 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 목표: -- table 지원을 넓히기 전에 먼저 boundary를 명확히 합니다. +- table 지원을 넓히기 전에 먼저 boundary를 명확히 하고, verifier가 formatting-level 차이를 과도하게 mismatch로 보지 않도록 정리합니다. 작업: 1. 현재 skip reason을 capability 문서와 1:1로 연결 2. markdown table / raw HTML table / preserved-anchor table을 명확히 분리 3. 어떤 table이 "지원 대상"인지 acceptance contract를 먼저 고정 +4. whitespace-only / column-width-only 차이는 `formatting_only_match`로 분류하는 verifier 규칙을 정의 완료 기준: - table 관련 버그가 들어왔을 때 "미지원"과 "회귀"를 혼동하지 않습니다. +- whitespace-only / column-width-only table diff는 더 이상 semantic mismatch로 집계되지 않습니다. - 새로운 지원 확대는 capability 추가로 표현합니다. ### Stage 5. verifier contract 정리 목표: -- patch quality 문제와 normalization 문제를 분리합니다. +- patch quality 문제와 normalization 문제를 분리하고, proof 계층의 결과 언어를 고정합니다. 작업: -1. `verify_roundtrip()` 결과를 reason code 중심으로 재분류 +1. `verify_roundtrip()` 결과를 Accept / Review / Block / Error taxonomy로 재분류 2. `--lenient`, `--no-normalize`의 의미를 문서와 테스트에서 고정 -3. converter normalization 변화로 인한 false failure를 별도 카테고리로 분리 +3. converter normalization 변화로 인한 false failure를 `normalized_match` 또는 `formatting_only_match`와 구분해 분리 +4. proof 계층에서 result classification과 push eligibility를 연결하는 규칙 정의 완료 기준: -- fail 원인을 `patch mismatch` / `normalization mismatch` / `forward canonicalization drift`로 구분할 수 있습니다. +- fail 및 review 원인을 `semantic review` / `capability block` / `normalization-driven accept` / `system error`로 구분할 수 있습니다. ### Stage 6. push rollout contract 정리 @@ -371,14 +385,14 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 4. Batch manifest tests - `tests/reverse-sync/pages.yaml` - - capability/expected_status/failure_type alignment 검증 + - capability/expected_status/failure_type/result taxonomy alignment 검증 ### 8.2 테스트 원칙 - 새 capability를 추가할 때는 representative fixture를 반드시 함께 추가합니다. - skip reason을 바꿀 때는 result taxonomy 테스트도 함께 갱신합니다. - verifier normalization 변경은 strategy 테스트가 아니라 verifier contract 테스트로 검증합니다. -- pages.yaml은 단순 case catalog가 아니라 regression reporting asset으로 관리합니다. +- `pages.yaml`은 실데이터 기준 reference metadata catalog이며, 테스트에서는 regression reporting asset으로도 활용합니다. ### 8.3 우선 추가할 테스트 @@ -428,26 +442,27 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza ## 11. 승인 기준 -이 계획은 다음 상태가 되면 실행 가능하다고 봅니다. +이 계획은 아래 방향 합의를 기준선으로 삼고 실행합니다. -1. capability registry의 1차 naming이 합의됩니다. -2. planner 분리 범위가 합의됩니다. -3. table을 "당장 더 지원할 것인지"보다 "경계를 먼저 고정할 것인지"에 대한 방향이 합의됩니다. -4. verifier reason taxonomy가 합의됩니다. +1. capability registry naming을 유지합니다. +2. planner / strategy / proof 3계층 분리를 코드 정리의 중심축으로 삼습니다. +3. table은 지원 확대보다 boundary 명시와 verifier normalization 보정을 우선합니다. +4. `pages.yaml`는 실데이터 기준 reference metadata catalog로 보고, 이 점을 과도한 설계 쟁점으로 확대하지 않습니다. +5. verifier taxonomy는 세부적으로 분류합니다. -즉, 코드 착수 전 필요한 것은 "모든 세부 구현의 확정"이 아니라, 앞으로 무엇을 지원 capability로 보고 무엇을 boundary로 둘지에 대한 합의입니다. +즉, 이제 남은 것은 큰 방향의 재논의가 아니라, 위 기준선 아래에서 capability 매핑과 proof taxonomy를 실제 코드/CLI/테스트에 반영하는 일입니다. --- -## 12. 리뷰에서 확인받고 싶은 쟁점 +## 12. 이번 리뷰에서 합의된 방향 -이 PR은 Draft이며, 아래 쟁점에 대한 리뷰를 받고 싶습니다. +이 PR은 Draft이며, 아래 항목은 이번 리뷰에서 방향이 합의된 상태로 간주합니다. -1. capability registry naming이 과한지, 아니면 필요한지 -2. table을 지원 확대보다 경계 명시화부터 하는 우선순위가 맞는지 -3. planner/strategy/proof 3계층 분리가 실제 코드 정리의 중심축으로 적절한지 -4. `pages.yaml`를 계속 manifest+metadata 겸용으로 둘지, 역할을 분리할지 -5. verifier taxonomy를 세분화하는 것이 운영상 도움이 되는지 +1. capability registry naming은 유지합니다. +2. table은 현 수준의 지원을 유지하되, verifier에서 whitespace-only / column-width-only 차이를 mismatch가 아니라 `formatting_only_match`로 판정하는 쪽을 우선합니다. +3. planner / strategy / proof 3계층 분리는 실제 코드 정리의 중심축으로 채택합니다. +4. `pages.yaml`는 reverse-sync 관점에서 reference metadata catalog로 보고, 테스트 전용 부가 필드 때문에 역할 논의를 과도하게 확대하지 않습니다. +5. verifier taxonomy는 운영 판단에 직접 쓰일 수 있도록 세부적으로 분류합니다. --- From 827cf442805f3f2eb57f862fa84cf114b362c7a7 Mon Sep 17 00:00:00 2001 From: JK Date: Sun, 26 Apr 2026 11:59:57 +0900 Subject: [PATCH 3/5] =?UTF-8?q?confluence-mdx:=20reverse-sync=20visible=20?= =?UTF-8?q?segment=20=EC=84=A4=EA=B3=84=20=EB=B0=A9=ED=96=A5=EC=9D=84=20?= =?UTF-8?q?=EA=B3=84=ED=9A=8D=EC=97=90=20=EB=B0=98=EC=98=81=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit replacement plan에 visible segment 기반 planner 방향을 명시합니다. text-bearing block은 MDX visible model에서 edit sequence를 도출한 뒤, XHTML node edit operation으로 lowering하는 구조를 목표 아키텍처에 추가합니다. 또한 planner 출력, strategy 분리, paragraph/heading 확장, contract/strategy 테스트 계획까지 함께 보강합니다. Co-Authored-By: Claude Opus 4.6 --- ...026-04-25-reverse-sync-replacement-plan.md | 82 ++++++++++++++++--- 1 file changed, 72 insertions(+), 10 deletions(-) diff --git a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md index 11e35ad06..4adc62a77 100644 --- a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md +++ b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md @@ -103,7 +103,28 @@ reverse-sync를 "모든 MDX 변경을 무조건 Confluence에 반영하는 시 앞으로는 planner와 strategy 실행을 분리해야 합니다. -### 4.4 테스트는 "설계 검증"과 "회귀 방지"를 분리합니다 +### 4.4 visible segment를 planning 언어로 승격합니다 + +현재 `visible_segments.py`는 주로 list 경로의 정밀 판별 보조 모델로만 쓰이지만, 앞으로는 이 개념을 reverse-sync의 planning 언어로 확장하는 방향을 택합니다. + +- MDX 쪽 변경은 가능하면 먼저 visible segment model로 정규화합니다. +- XHTML 쪽 원본 fragment도 visible segment model 또는 그에 대응하는 visible node view로 투영합니다. +- planner는 block diff만으로 바로 patch를 만들지 않고, 우선 `visible edit sequence`를 도출합니다. +- strategy는 이 edit sequence를 그대로 DOM에 적용하지 않고, 안전한 XHTML node edit operation으로 lowering합니다. + +즉 앞으로의 중심 표현은 `plain-text diff` 자체가 아니라 `visible segment -> edit sequence -> node operation`입니다. + +### 4.5 universal emitter를 목표로 하지 않습니다 + +visible segment 중심 설계는 paragraph / heading / list / 일반 inline text 변경에는 잘 맞지만, preserved anchor / parameter-bearing container / raw HTML table / Confluence macro 구조까지 단일 모델로 무리하게 일반화하면 오히려 안전성이 떨어집니다. + +따라서 다음 원칙을 둡니다. + +- text-bearing block은 visible segment 기반 planning을 우선합니다. +- 구조 보존이 중요한 block은 sidecar/template-preserving reconstruction을 fallback 또는 authority로 유지합니다. +- 위험 구조는 `replace_fragment` 또는 `skip`으로 남깁니다. + +### 4.6 테스트는 "설계 검증"과 "회귀 방지"를 분리합니다 예전 문서는 둘을 한 축에 놓는 경향이 있었고, 그 결과 oracle이 흐려졌습니다. 앞으로는 테스트를 다음으로 분리합니다. @@ -112,7 +133,7 @@ reverse-sync를 "모든 MDX 변경을 무조건 Confluence에 반영하는 시 - page fixture 기반 통합 테스트 - batch manifest 기반 상태 리포트 -### 4.5 문서와 코드의 시간축을 분리합니다 +### 4.7 문서와 코드의 시간축을 분리합니다 현황 문서는 현재 코드가 하는 일을 적고, 계획 문서는 앞으로 바꿀 일을 적어야 합니다. 둘을 한 문서에 섞지 않습니다. @@ -164,10 +185,13 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 1. Planner - 변경 블록을 capability 단위로 분류 - 사용 가능한 identity source 결정 + - MDX/XHTML visible model 생성 여부 결정 + - visible edit sequence 도출 - strategy 후보 결정 - skip/fail reason 결정 2. Strategy + - visible edit sequence를 XHTML node edit operation으로 lowering - direct edit - fragment replacement - container reconstruction @@ -183,12 +207,33 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 현재는 이 셋이 `patch_builder.py`와 `reverse_sync_cli.py` 사이에 뒤섞여 있습니다. 앞으로는 책임 경계를 명시해야 합니다. -### 6.2 Capability registry 도입 +### 6.2 Visible segment planner와 XHTML node operation 계층 + +이번 replacement plan에서 새로 채택하는 핵심 방향은, `visible_segments.py`를 list 전용 보조 유틸에 머무르게 두지 않고 planner 계층의 공통 표현으로 승격하는 것입니다. + +목표 파이프라인은 다음과 같습니다. + +1. old/new MDX block -> visible segment model +2. original XHTML fragment -> visible segment model 또는 visible node view +3. 두 model 사이의 `visible edit sequence` 계산 +4. `visible edit sequence`를 XHTML node edit operation으로 lowering +5. 기존 `patch_xhtml()` 계층으로 적용 +6. roundtrip verifier로 proof + +여기서 중요한 점은 다음과 같습니다. + +- `visible edit sequence`는 곧바로 DOM mutation이 아닙니다. +- 중간 lowering 단계에서 capability boundary, sidecar identity, template preservation 필요 여부를 함께 판단해야 합니다. +- 결과 operation은 현재 patch 체계와 호환되는 `modify`, `delete`, `insert`, `replace_fragment` 계열로 귀결되어도 됩니다. + +즉 이 계획은 apply 계층을 버리는 것이 아니라, patch 생성 전의 정책 언어를 visible segment 중심으로 재구성하는 계획입니다. + +### 6.3 Capability registry 도입 다음과 같은 capability 단위를 문서와 코드 양쪽에 명시적으로 도입합니다. -- `paragraph_text_edit` -- `heading_text_edit` +- `paragraph_visible_segment_edit` +- `heading_visible_segment_edit` - `inline_whitespace_adjustment` - `clean_block_replace` - `container_body_reconstruction` @@ -206,7 +251,7 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza - 대표 fixture - regression test location -### 6.3 Result taxonomy 표준화 +### 6.4 Result taxonomy 표준화 현재 `pass/fail/no_changes/skipped_changes`는 존재하지만, 더 세밀한 운영 의미가 필요합니다. 특히 table, whitespace, column-width 같은 formatting 수준 차이를 semantic mismatch와 분리해야 합니다. 계획상 목표 taxonomy는 다음 4계층으로 둡니다. @@ -283,15 +328,20 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza - change - mapping source - sidecar source + - mdx visible model + - xhtml visible model 또는 visible node view + - visible edit sequence - capability - chosen strategy - skip reason -2. strategy dispatcher 인터페이스 정의 -3. 기존 `build_patches()`는 planner + strategy executor orchestration으로 축소 +2. `visible edit sequence`를 표준 planning 산출물로 고정 +3. strategy dispatcher 인터페이스 정의 +4. 기존 `build_patches()`는 planner + strategy executor orchestration으로 축소 완료 기준: - 새 회귀 수정 시 `build_patches()` 본체에 직접 if/elif를 추가하지 않고, planner rule 또는 strategy handler 수정으로 끝낼 수 있습니다. +- planner 출력만 보면 "어떤 visible 변화가 어떤 node operation 후보로 내려갔는지"를 설명할 수 있습니다. ### Stage 3. list / preserved anchor / container 전략 분리 @@ -305,10 +355,17 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 2. preserved-anchor template rewrite 전략 3. parameter-bearing container reconstruction 전략 +세부 방향: + +- list는 현재 `visible_segments.py`를 그대로 출발점으로 삼되, 결과를 `visible edit sequence`로 명시화합니다. +- paragraph / heading은 새 visible model extractor를 도입해 plain-text transfer보다 planner 기반 lowering을 우선 검토합니다. +- preserved anchor / container는 visible planning 결과만으로 충분하지 않은 경우 template-preserving reconstruction으로 안전하게 내려갑니다. + 완료 기준: - 각 전략이 독립 테스트 파일과 fixture 세트를 가집니다. - `patch_builder.py`는 분기 지점만 남고 세부 알고리즘을 직접 품지 않습니다. +- text-bearing block에 대해서는 "MDX visible model -> edit sequence -> node operation" 흐름을 설명할 수 있습니다. ### Stage 4. table 경계 명시화 @@ -371,10 +428,12 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 1. Contract tests - sidecar schema - planner classification + - visible edit sequence schema / invariants - result taxonomy 2. Strategy tests - list visible segment + - paragraph/heading visible segment lowering - preserved anchor rewrite - container reconstruction - table classification @@ -399,8 +458,9 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 1. planner classification snapshot test 2. table skip reason classification test 3. list visible segment capability fixture set -4. verifier reason-code split test -5. result.yaml schema test +4. paragraph/heading visible edit sequence snapshot test +5. verifier reason-code split test +6. result.yaml schema test --- @@ -449,6 +509,7 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 3. table은 지원 확대보다 boundary 명시와 verifier normalization 보정을 우선합니다. 4. `pages.yaml`는 실데이터 기준 reference metadata catalog로 보고, 이 점을 과도한 설계 쟁점으로 확대하지 않습니다. 5. verifier taxonomy는 세부적으로 분류합니다. +6. visible segment model을 planner 계층의 중심 표현으로 승격하되, apply 계층은 patch/DOM/sidecar 체계를 유지합니다. 즉, 이제 남은 것은 큰 방향의 재논의가 아니라, 위 기준선 아래에서 capability 매핑과 proof taxonomy를 실제 코드/CLI/테스트에 반영하는 일입니다. @@ -463,6 +524,7 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 3. planner / strategy / proof 3계층 분리는 실제 코드 정리의 중심축으로 채택합니다. 4. `pages.yaml`는 reverse-sync 관점에서 reference metadata catalog로 보고, 테스트 전용 부가 필드 때문에 역할 논의를 과도하게 확대하지 않습니다. 5. verifier taxonomy는 운영 판단에 직접 쓰일 수 있도록 세부적으로 분류합니다. +6. visible segment 기반 `edit sequence -> XHTML node operation` 흐름은 text-bearing block의 목표 설계로 채택하되, sidecar/template-preserving fallback을 함께 유지합니다. --- From bcdba08caed9d5d8d78260841d88cedad210a949 Mon Sep 17 00:00:00 2001 From: JK Date: Sun, 26 Apr 2026 13:59:20 +0900 Subject: [PATCH 4/5] =?UTF-8?q?confluence-mdx:=20reverse-sync=20=EA=B3=84?= =?UTF-8?q?=ED=9A=8D=20=EB=AC=B8=EC=84=9C=EC=97=90=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EB=B6=84=ED=95=B4=EC=95=88=EC=9D=84=20=EB=B3=B4=EA=B0=95?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit replacement plan에 visible segment planner 기준의 구체적 코드 분해안을 추가합니다. planner/lowering/strategy 모듈 분리, 초기 함수 시그니처, 테스트 파일 배치안을 문서화해 구현 착수 전 책임 경계를 더 명확히 정리합니다. Co-Authored-By: Claude Opus 4.6 --- ...026-04-25-reverse-sync-replacement-plan.md | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md index 4adc62a77..3526c41c6 100644 --- a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md +++ b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md @@ -343,6 +343,88 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza - 새 회귀 수정 시 `build_patches()` 본체에 직접 if/elif를 추가하지 않고, planner rule 또는 strategy handler 수정으로 끝낼 수 있습니다. - planner 출력만 보면 "어떤 visible 변화가 어떤 node operation 후보로 내려갔는지"를 설명할 수 있습니다. +초기 모듈 분해 초안: + +- `confluence-mdx/bin/reverse_sync/planner_types.py` + - `VisibleEdit` + - `VisibleEditSequence` + - `PlanningContext` + - `PlanningDecision` +- `confluence-mdx/bin/reverse_sync/visible_planner.py` + - `plan_visible_edit_sequence(...) -> VisibleEditSequence` + - `plan_change(...) -> PlanningDecision` +- `confluence-mdx/bin/reverse_sync/node_operations.py` + - `NodeOperation` + - `NodeOperationSequence` +- `confluence-mdx/bin/reverse_sync/operation_lowering.py` + - `lower_visible_edits_to_node_operations(...) -> NodeOperationSequence` + - `lower_node_operations_to_patches(...) -> list[dict[str, str]]` +- `confluence-mdx/bin/reverse_sync/strategy_dispatcher.py` + - `choose_strategy(...) -> str` + - `execute_strategy(...) -> list[dict[str, str]]` +- 전략 하위 모듈 + - `confluence-mdx/bin/reverse_sync/strategies/list_strategy.py` + - `confluence-mdx/bin/reverse_sync/strategies/text_block_strategy.py` + - `confluence-mdx/bin/reverse_sync/strategies/preserved_anchor_strategy.py` + - `confluence-mdx/bin/reverse_sync/strategies/container_strategy.py` + - `confluence-mdx/bin/reverse_sync/strategies/table_strategy.py` + +초기 함수 시그니처 초안: + +```python +@dataclass(frozen=True) +class VisibleEdit: + kind: Literal[ + "keep", + "insert_text", + "delete_text", + "replace_text", + "split_segment", + "merge_segment", + "formatting_only", + ] + old_segment_ids: tuple[str, ...] = () + new_segment_ids: tuple[str, ...] = () + old_text: str = "" + new_text: str = "" + meta: dict[str, Any] = field(default_factory=dict) + + +def plan_visible_edit_sequence( + *, + old_mdx: str, + new_mdx: str, + xhtml_fragment: str, + capability: str, +) -> VisibleEditSequence: + ... + + +def plan_change(change: BlockChange, context: PlanningContext) -> PlanningDecision: + ... + + +def lower_visible_edits_to_node_operations( + sequence: VisibleEditSequence, + context: PlanningContext, +) -> NodeOperationSequence: + ... + + +def lower_node_operations_to_patches( + operations: NodeOperationSequence, + context: PlanningContext, +) -> list[dict[str, str]]: + ... +``` + +이 초안의 의도는 다음과 같습니다. + +- planner 출력은 patch dict가 아니라 `VisibleEditSequence`입니다. +- lowering 계층은 visible edit와 DOM/node edit를 연결합니다. +- strategy 계층은 capability boundary와 fallback 선택을 담당합니다. +- 최종 apply는 기존 `xhtml_patcher.py`를 계속 사용합니다. + ### Stage 3. list / preserved anchor / container 전략 분리 목표: @@ -462,6 +544,39 @@ pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicaliza 5. verifier reason-code split test 6. result.yaml schema test +### 8.4 초기 테스트 파일 배치안 + +- `confluence-mdx/tests/test_visible_planner.py` + - planner classification snapshot + - paragraph/heading visible edit sequence snapshot +- `confluence-mdx/tests/test_operation_lowering.py` + - visible edit sequence -> node operation lowering + - node operation -> patch emission +- `confluence-mdx/tests/test_strategy_dispatcher.py` + - capability별 strategy 선택 + - fallback / skip reason 결정 +- `confluence-mdx/tests/test_list_strategy.py` + - 기존 list visible segment fixture 재사용 +- `confluence-mdx/tests/test_text_block_strategy.py` + - paragraph / heading text-bearing block fixture +- `confluence-mdx/tests/test_preserved_anchor_strategy.py` + - preserved anchor fallback / reconstruction 경계 +- `confluence-mdx/tests/test_container_strategy.py` + - parameter-bearing container reconstruction 경계 +- `confluence-mdx/tests/test_table_strategy.py` + - table capability boundary / skip reason / formatting-only verifier 연계 + +fixture 배치 초안: + +- `confluence-mdx/tests/testcases/visible-planner/paragraph-*` +- `confluence-mdx/tests/testcases/visible-planner/heading-*` +- `confluence-mdx/tests/testcases/visible-planner/list-*` +- `confluence-mdx/tests/testcases/strategies/preserved-anchor-*` +- `confluence-mdx/tests/testcases/strategies/container-*` +- `confluence-mdx/tests/testcases/strategies/table-*` + +이 배치는 planner, lowering, strategy, proof를 테스트 파일 수준에서도 분리하기 위한 최소 단위 제안입니다. + --- ## 9. 문서 구조 계획 From 46f011717800a8e1214f61b183c1bc94dd6f3048 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 24 Jul 2026 17:18:19 +0900 Subject: [PATCH 5/5] =?UTF-8?q?confluence-mdx:=20reverse-sync=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C=20=EA=B3=84=EC=95=BD=EC=9D=84=20=EC=A0=95=EC=9D=98?= =?UTF-8?q?=ED=95=A9=EB=8B=88=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary MDX 변경을 Confluence page에 안전하게 반영하기 위한 durable contract와 구현 순서를 정의합니다. - 현재 reverse-sync의 로컬 검증 기준선과 remote update correctness gap을 기록합니다. - snapshot-bound transformation, base parity, immutable manifest, compare-and-set, postcondition을 설계합니다. - P0 push safety부터 planner 분해와 capability 확대까지 구현 task를 정리합니다. - 중복 replacement plan을 OpenSpec canonical 문서로 대체합니다. ## Test plan - [x] git diff --cached --check - [x] reverse-sync Python unit test 676개 통과 - [x] reverse-sync page fixture 59개 통과 - [x] byte-equal fast path 및 forced splice 각 21개 통과 🤖 Generated with Codex Co-Authored-By: Atlas --- ...verse-sync-reconstruction-design-review.md | 4 +- ...3-13-reverse-sync-reconstruction-design.md | 4 +- ...verse-sync-reconstruction-cleanup-scope.md | 4 +- ...026-04-25-reverse-sync-replacement-plan.md | 654 ------------------ openspec/changes/README.md | 7 +- .../changes/complete-reverse-sync/design.md | 593 ++++++++++++++++ .../changes/complete-reverse-sync/proposal.md | 63 ++ .../specs/contract-reverse-sync/spec.md | 316 +++++++++ .../changes/complete-reverse-sync/tasks.md | 271 ++++++++ 9 files changed, 1255 insertions(+), 661 deletions(-) delete mode 100644 confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md create mode 100644 openspec/changes/complete-reverse-sync/design.md create mode 100644 openspec/changes/complete-reverse-sync/proposal.md create mode 100644 openspec/changes/complete-reverse-sync/specs/contract-reverse-sync/spec.md create mode 100644 openspec/changes/complete-reverse-sync/tasks.md 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 index 7b9e5587b..5cfce9aee 100644 --- 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 @@ -1,8 +1,8 @@ # Reverse Sync 전면 재구성 설계 — 검토 평가 결과 (v5) > 상태: Superseded review -> 대체 문서: `2026-04-25-reverse-sync-replacement-plan.md` -> 비고: 이 문서는 2026-03 시점 설계안에 대한 검토 기록입니다. 현재 계획 논의는 새 replacement plan을 기준으로 진행합니다. +> 대체 문서: [`openspec/changes/complete-reverse-sync/design.md`](../../../openspec/changes/complete-reverse-sync/design.md) +> 비고: 이 문서는 2026-03 시점 설계안에 대한 검토 기록입니다. 현재 계약과 계획 논의는 OpenSpec change를 기준으로 진행합니다. > 검토 대상: `2026-03-13-reverse-sync-reconstruction-design.md` > 검토일: 2026-03-13 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 16570429b..ee629c4e4 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,8 +1,8 @@ # Reverse Sync 전면 재구성 설계 > 상태: Superseded -> 대체 문서: `2026-04-25-reverse-sync-replacement-plan.md` -> 비고: 이 문서는 2026-03 시점의 phase 계획과 당시의 코드 상태를 보존하기 위한 기록입니다. 현재 작업 계획은 새 replacement plan을 기준으로 합니다. +> 대체 문서: [`openspec/changes/complete-reverse-sync/design.md`](../../../openspec/changes/complete-reverse-sync/design.md) +> 비고: 이 문서는 2026-03 시점의 phase 계획과 당시의 코드 상태를 보존하기 위한 기록입니다. 현재 계약과 작업 계획은 OpenSpec change를 기준으로 합니다. > 최초 작성일: 2026-03-13 > 갱신일: 2026-03-19 (4차 — Phase 5 진행 현황 반영 및 설계 기준선 교정) 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 index 89d1b94b8..1375f35e8 100644 --- 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 @@ -1,8 +1,8 @@ # Reverse Sync 재구성 후 삭제 대상 범위 > 상태: Superseded -> 대체 문서: `2026-04-25-reverse-sync-replacement-plan.md` -> 비고: 이 문서는 2026-03 시점 cleanup 가정과 삭제 범위를 담고 있습니다. 현재 cleanup 계획은 새 replacement plan의 단계별 실행 계획을 기준으로 합니다. +> 대체 문서: [`openspec/changes/complete-reverse-sync/tasks.md`](../../../openspec/changes/complete-reverse-sync/tasks.md) +> 비고: 이 문서는 2026-03 시점 cleanup 가정과 삭제 범위를 담고 있습니다. 현재 cleanup 계획은 OpenSpec change의 task를 기준으로 합니다. > 작성일: 2026-03-15 > 갱신일: 2026-03-16 diff --git a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md b/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md deleted file mode 100644 index 3526c41c6..000000000 --- a/confluence-mdx/docs/plans/2026-04-25-reverse-sync-replacement-plan.md +++ /dev/null @@ -1,654 +0,0 @@ -# Reverse Sync 대체 계획 (2026-04-25) - -> 상태: Draft -> 대상: `confluence-mdx/bin/reverse_sync/`, `bin/reverse_sync_cli.py`, `tests/`, `docs/` -> 목적: 기존 reconstruction phase 문서를 현재 코드 기준으로 대체하고, 앞으로의 개선 작업을 "구현 현실"에 맞는 계획으로 다시 정의합니다. - ---- - -## 1. 이 문서가 대체하는 기존 계획 문서 - -이 문서는 아래 3개 문서를 포괄적으로 대체합니다. - -- `2026-03-13-reverse-sync-reconstruction-design.md` -- `2026-03-13-reverse-sync-reconstruction-design-review.md` -- `2026-03-15-reverse-sync-reconstruction-cleanup-scope.md` - -기존 문서들은 당시로서는 타당한 문제의식과 설계 방향을 담고 있었지만, 현재 main의 구현 상태와 다음 이유로 어긋납니다. - -1. 이미 완료된 작업을 아직 계획 단계처럼 서술하는 부분이 있습니다. -2. 삭제된 모듈(`text_transfer.py`, `list_patcher.py`, `table_patcher.py`)을 전제로 하는 cleanup 항목이 남아 있습니다. -3. `mapping.yaml`과 roundtrip sidecar v3의 실제 역할 분리가 문서 구조에 충분히 반영되지 않았습니다. -4. 현재의 핵심 위험이 "재구성 자체의 부재"보다 "patch_builder 정책 집중 + verifier 결합 + capability 경계 불명확"으로 이동했는데, 예전 문서는 이 상태를 담지 못합니다. - -이 문서는 따라서 "예전 계획의 6차 개정판"이 아니라, 현재 구현을 출발점으로 다시 세운 replacement plan입니다. -이 문서를 현재 기준의 주 문서로 삼고, 3월 문서들은 historical record로만 남깁니다. - ---- - -## 2. 현재 기준선 - -현재 reverse-sync의 사실상 표준 경로는 다음과 같습니다. - -1. 원본/교정 MDX 로드 -2. `parse_mdx_blocks()` + `diff_blocks()` -3. `record_mapping(page.xhtml)` -4. `generate_sidecar_mapping()` / `mapping.yaml` -5. `build_sidecar()` / `expected.roundtrip.json` schema v3 -6. `build_patches()` -7. `patch_xhtml()` -8. patched XHTML 재기록 -9. forward converter 재실행 -10. `verify_roundtrip()` -11. pass 시에만 push 후보로 취급 - -현재 구현의 핵심 특성: - -- reverse-sync는 text patcher가 아니라 sidecar 기반 fragment-preserving system입니다. -- `patch_builder.py`가 실제 정책 엔진입니다. -- `mapping.yaml`은 lookup/lost_info 계층이고, roundtrip sidecar v3는 identity/reconstruction 계층입니다. -- list, preserved anchor, table, parameter-bearing container는 여전히 별도 capability boundary를 갖습니다. -- verifier와 forward converter의 정규화 특성 변화가 reverse-sync pass/fail에 직접 영향을 줍니다. - -이 기준선은 "무엇을 새로 만들 것인가"보다 먼저 "무엇을 더 이상 가정하지 말아야 하는가"를 정해줍니다. - ---- - -## 3. 이번 계획의 목표 - -### 3.1 최종 목표 - -reverse-sync를 "모든 MDX 변경을 무조건 Confluence에 반영하는 시스템"으로 만들겠다는 목표는 현실적이지 않습니다. 대신 다음을 목표로 삼습니다. - -1. 지원 범위를 capability 단위로 명확히 문서화합니다. -2. 지원 범위 안에서는 roundtrip proof까지 포함해 신뢰할 수 있게 만듭니다. -3. 지원 범위 밖에서는 skip/fail 이유가 코드와 문서에 일관되게 드러나게 만듭니다. -4. `patch_builder.py`의 전략 집중을 완화해 회귀 수정 비용을 줄입니다. -5. verifier/fixture/manifest 체계를 재정리해 "왜 pass/fail인지"를 추적 가능하게 만듭니다. - -### 3.2 비목표 - -이번 계획의 비목표는 다음과 같습니다. - -- Confluence XHTML의 모든 구조를 일반 emitter만으로 재생성하기 -- skip reason을 전부 제거하기 -- 테이블/복합 anchor/container를 단기간에 완전 지원하기 -- reverse-sync를 forward converter와 완전히 분리된 서브시스템으로 만들기 - -즉, 목표는 "전면 자동화"가 아니라 "지원 범위의 명확화 + 구조적 유지보수성 향상"입니다. - ---- - -## 4. 설계 원칙 - -### 4.1 fail-closed를 유지합니다 - -지원 범위가 애매한 변경은 계속 skip/fail-closed로 남겨야 합니다. coverage 없이 공격적으로 허용하는 방향은 반복 회귀를 다시 부릅니다. - -### 4.2 sidecar를 진실원으로 취급하지 않고, 계약(contract)으로 취급합니다 - -- `mapping.yaml`은 구조 lookup 계약입니다. -- roundtrip sidecar v3는 fragment identity 계약입니다. -- 둘 중 하나만으로 전체 문제를 해결하려고 하면 문서와 코드가 다시 어긋납니다. - -### 4.3 정책과 실행을 분리합니다 - -현재 `patch_builder.py`에는 다음이 함께 들어 있습니다. - -- capability 판별 -- fallback 선택 -- patch shape 생성 -- skip reason 생성 -- 일부 list/table 전용 로직 - -앞으로는 planner와 strategy 실행을 분리해야 합니다. - -### 4.4 visible segment를 planning 언어로 승격합니다 - -현재 `visible_segments.py`는 주로 list 경로의 정밀 판별 보조 모델로만 쓰이지만, 앞으로는 이 개념을 reverse-sync의 planning 언어로 확장하는 방향을 택합니다. - -- MDX 쪽 변경은 가능하면 먼저 visible segment model로 정규화합니다. -- XHTML 쪽 원본 fragment도 visible segment model 또는 그에 대응하는 visible node view로 투영합니다. -- planner는 block diff만으로 바로 patch를 만들지 않고, 우선 `visible edit sequence`를 도출합니다. -- strategy는 이 edit sequence를 그대로 DOM에 적용하지 않고, 안전한 XHTML node edit operation으로 lowering합니다. - -즉 앞으로의 중심 표현은 `plain-text diff` 자체가 아니라 `visible segment -> edit sequence -> node operation`입니다. - -### 4.5 universal emitter를 목표로 하지 않습니다 - -visible segment 중심 설계는 paragraph / heading / list / 일반 inline text 변경에는 잘 맞지만, preserved anchor / parameter-bearing container / raw HTML table / Confluence macro 구조까지 단일 모델로 무리하게 일반화하면 오히려 안전성이 떨어집니다. - -따라서 다음 원칙을 둡니다. - -- text-bearing block은 visible segment 기반 planning을 우선합니다. -- 구조 보존이 중요한 block은 sidecar/template-preserving reconstruction을 fallback 또는 authority로 유지합니다. -- 위험 구조는 `replace_fragment` 또는 `skip`으로 남깁니다. - -### 4.6 테스트는 "설계 검증"과 "회귀 방지"를 분리합니다 - -예전 문서는 둘을 한 축에 놓는 경향이 있었고, 그 결과 oracle이 흐려졌습니다. 앞으로는 테스트를 다음으로 분리합니다. - -- 계약 테스트(contract tests) -- strategy 단위 회귀 테스트 -- page fixture 기반 통합 테스트 -- batch manifest 기반 상태 리포트 - -### 4.7 문서와 코드의 시간축을 분리합니다 - -현황 문서는 현재 코드가 하는 일을 적고, 계획 문서는 앞으로 바꿀 일을 적어야 합니다. 둘을 한 문서에 섞지 않습니다. - ---- - -## 5. 핵심 문제 재정의 - -현재 reverse-sync의 가장 큰 문제는 "아직 reconstruction이 덜 구현되었다"가 아닙니다. 현재는 다음 5개가 더 중요합니다. - -### P1. capability 경계가 코드에만 있고 문서/테스트 표면에 명시되지 않습니다 - -예: - -- 어떤 table은 replace 가능하고 어떤 table은 skip됩니다. -- 어떤 preserved anchor list는 처리되고 어떤 것은 여전히 위험합니다. -- 어떤 container는 reconstruction 경로를 타지만 parameter-bearing container는 별도 취급됩니다. - -하지만 이 차이가 문서/테스트/CLI 결과에서 일관된 capability taxonomy로 드러나지 않습니다. - -### P2. `patch_builder.py`가 planner + strategy dispatcher + partial executor 역할을 동시에 수행합니다 - -이 구조에서는 새 회귀가 나올 때마다 if/elif를 추가하는 방향으로 수렴하기 쉽습니다. - -### P3. verifier와 converter 정규화가 reverse-sync 결과 해석을 어렵게 만듭니다 - -pass/fail이 실제 patch 품질 문제인지, forward converter의 canonicalization 변화인지, verifier normalization 차이인지 즉시 분리되지 않는 경우가 있습니다. - -### P4. fixture/reporting 자산은 많지만 taxonomy가 아직 정리되지 않았습니다 - -- `tests/testcases/` -- `tests/reverse-sync/pages.yaml` -- unit test -- e2e regression fixture - -실데이터 기준으로 `pages.yaml`의 역할은 이미 비교적 명확합니다. reverse-sync 입장에서는 참조용 metadata catalog이며, 테스트 구현 과정에서만 추가 필드가 얹혀 있습니다. 문제는 `pages.yaml` 자체보다, capability / expected_status / verifier outcome을 어떤 taxonomy로 읽을지 아직 정리되지 않았다는 점입니다. - -### P5. 계획 문서가 실제 main과 엇갈리며, 삭제된/완료된 축을 계속 참조합니다 - -이 상태에서는 새 작업을 시작할 때마다 먼저 문서를 해석해야 하므로, 문서가 가이드보다 부담이 됩니다. - ---- - -## 6. 목표 아키텍처 - -이번 계획의 목표 아키텍처는 "새 시스템을 다시 처음부터 만들자"가 아니라, 현재 구조를 다음처럼 재배치하는 것입니다. - -### 6.1 Planner / Strategy / Proof 3계층 - -1. Planner - - 변경 블록을 capability 단위로 분류 - - 사용 가능한 identity source 결정 - - MDX/XHTML visible model 생성 여부 결정 - - visible edit sequence 도출 - - strategy 후보 결정 - - skip/fail reason 결정 - -2. Strategy - - visible edit sequence를 XHTML node edit operation으로 lowering - - direct edit - - fragment replacement - - container reconstruction - - list strategy - - table strategy - - preserved-anchor template rewrite - -3. Proof - - patch 적용 - - forward roundtrip - - result classification - - push eligibility - -현재는 이 셋이 `patch_builder.py`와 `reverse_sync_cli.py` 사이에 뒤섞여 있습니다. 앞으로는 책임 경계를 명시해야 합니다. - -### 6.2 Visible segment planner와 XHTML node operation 계층 - -이번 replacement plan에서 새로 채택하는 핵심 방향은, `visible_segments.py`를 list 전용 보조 유틸에 머무르게 두지 않고 planner 계층의 공통 표현으로 승격하는 것입니다. - -목표 파이프라인은 다음과 같습니다. - -1. old/new MDX block -> visible segment model -2. original XHTML fragment -> visible segment model 또는 visible node view -3. 두 model 사이의 `visible edit sequence` 계산 -4. `visible edit sequence`를 XHTML node edit operation으로 lowering -5. 기존 `patch_xhtml()` 계층으로 적용 -6. roundtrip verifier로 proof - -여기서 중요한 점은 다음과 같습니다. - -- `visible edit sequence`는 곧바로 DOM mutation이 아닙니다. -- 중간 lowering 단계에서 capability boundary, sidecar identity, template preservation 필요 여부를 함께 판단해야 합니다. -- 결과 operation은 현재 patch 체계와 호환되는 `modify`, `delete`, `insert`, `replace_fragment` 계열로 귀결되어도 됩니다. - -즉 이 계획은 apply 계층을 버리는 것이 아니라, patch 생성 전의 정책 언어를 visible segment 중심으로 재구성하는 계획입니다. - -### 6.3 Capability registry 도입 - -다음과 같은 capability 단위를 문서와 코드 양쪽에 명시적으로 도입합니다. - -- `paragraph_visible_segment_edit` -- `heading_visible_segment_edit` -- `inline_whitespace_adjustment` -- `clean_block_replace` -- `container_body_reconstruction` -- `preserved_anchor_template_rewrite` -- `list_visible_segment_edit` -- `table_fragment_replace` -- `unsafe_table_edit` -- `unsupported_complex_anchor_structure` - -중요한 것은 naming 그 자체보다, 각 capability가 아래를 갖는 것입니다. - -- 지원 여부 -- strategy owner -- skip/fail reason -- 대표 fixture -- regression test location - -### 6.4 Result taxonomy 표준화 - -현재 `pass/fail/no_changes/skipped_changes`는 존재하지만, 더 세밀한 운영 의미가 필요합니다. 특히 table, whitespace, column-width 같은 formatting 수준 차이를 semantic mismatch와 분리해야 합니다. 계획상 목표 taxonomy는 다음 4계층으로 둡니다. - -1. Accept - - `exact_match` - - `normalized_match` - - `formatting_only_match` -2. Review - - `semantic_diff_needs_review` - - `reconstruction_needs_review` - - `preservation_uncertain` -3. Block - - `capability_boundary_blocked` - - `missing_identity` - - `unsafe_edit` - - `ambiguous_target` -4. Error - - `patch_generation_error` - - `roundtrip_verification_error` - - `reconversion_error` - -이 taxonomy는 CLI, result.yaml, 문서, 테스트 설명이 공통으로 써야 합니다. 구현 세부 enum은 더 쪼갤 수 있지만, 문서와 운영 보고는 위 중간 해상도를 기준으로 맞춥니다. - ---- - -## 7. 단계별 실행 계획 - -### Stage 0. 문서/분류 기준선 확정 - -목표: - -- current-state 문서와 plan 문서의 역할을 분리합니다. -- capability registry 초안을 문서에 먼저 정의합니다. -- 기존 plan 문서를 superseded 상태로 전환합니다. - -산출물: - -- 현재 구현 상태 문서 -- 본 replacement plan -- 기존 plan 문서 superseded 표기 - -완료 기준: - -- 새 작업이 더 이상 3월 phase 문서를 기준으로 시작되지 않습니다. - -### Stage 1. capability matrix와 fixture inventory 정리 - -목표: - -- 현재 처리 가능한 변경을 capability 기준으로 표에 정리합니다. -- `pages.yaml`를 실데이터 기준의 reference metadata catalog로 문서화하고, 테스트 전용 부가 필드와 구분해 설명합니다. - -작업: - -1. `failure_type`, label, severity를 capability/skip reason과 매핑합니다. -2. 각 capability에 대표 fixture를 1~3개씩 지정합니다. -3. `expected_status`와 verifier outcome이 taxonomy 어디에 매핑되는지 문서에서 재정의합니다. -4. unsupported 영역을 명시적으로 목록화합니다. - -완료 기준: - -- "이 케이스는 왜 아직 안 되나"를 capability 이름으로 설명할 수 있습니다. -- 회귀 이슈가 들어오면 어느 capability에 속하는지 바로 분류할 수 있습니다. - -### Stage 2. `patch_builder.py` 분해 1차 — planner 추출 - -목표: - -- `patch_builder.py`에서 정책 판단과 patch 생성 책임을 분리합니다. - -작업: - -1. planner 입력/출력 구조 정의 - - change - - mapping source - - sidecar source - - mdx visible model - - xhtml visible model 또는 visible node view - - visible edit sequence - - capability - - chosen strategy - - skip reason -2. `visible edit sequence`를 표준 planning 산출물로 고정 -3. strategy dispatcher 인터페이스 정의 -4. 기존 `build_patches()`는 planner + strategy executor orchestration으로 축소 - -완료 기준: - -- 새 회귀 수정 시 `build_patches()` 본체에 직접 if/elif를 추가하지 않고, planner rule 또는 strategy handler 수정으로 끝낼 수 있습니다. -- planner 출력만 보면 "어떤 visible 변화가 어떤 node operation 후보로 내려갔는지"를 설명할 수 있습니다. - -초기 모듈 분해 초안: - -- `confluence-mdx/bin/reverse_sync/planner_types.py` - - `VisibleEdit` - - `VisibleEditSequence` - - `PlanningContext` - - `PlanningDecision` -- `confluence-mdx/bin/reverse_sync/visible_planner.py` - - `plan_visible_edit_sequence(...) -> VisibleEditSequence` - - `plan_change(...) -> PlanningDecision` -- `confluence-mdx/bin/reverse_sync/node_operations.py` - - `NodeOperation` - - `NodeOperationSequence` -- `confluence-mdx/bin/reverse_sync/operation_lowering.py` - - `lower_visible_edits_to_node_operations(...) -> NodeOperationSequence` - - `lower_node_operations_to_patches(...) -> list[dict[str, str]]` -- `confluence-mdx/bin/reverse_sync/strategy_dispatcher.py` - - `choose_strategy(...) -> str` - - `execute_strategy(...) -> list[dict[str, str]]` -- 전략 하위 모듈 - - `confluence-mdx/bin/reverse_sync/strategies/list_strategy.py` - - `confluence-mdx/bin/reverse_sync/strategies/text_block_strategy.py` - - `confluence-mdx/bin/reverse_sync/strategies/preserved_anchor_strategy.py` - - `confluence-mdx/bin/reverse_sync/strategies/container_strategy.py` - - `confluence-mdx/bin/reverse_sync/strategies/table_strategy.py` - -초기 함수 시그니처 초안: - -```python -@dataclass(frozen=True) -class VisibleEdit: - kind: Literal[ - "keep", - "insert_text", - "delete_text", - "replace_text", - "split_segment", - "merge_segment", - "formatting_only", - ] - old_segment_ids: tuple[str, ...] = () - new_segment_ids: tuple[str, ...] = () - old_text: str = "" - new_text: str = "" - meta: dict[str, Any] = field(default_factory=dict) - - -def plan_visible_edit_sequence( - *, - old_mdx: str, - new_mdx: str, - xhtml_fragment: str, - capability: str, -) -> VisibleEditSequence: - ... - - -def plan_change(change: BlockChange, context: PlanningContext) -> PlanningDecision: - ... - - -def lower_visible_edits_to_node_operations( - sequence: VisibleEditSequence, - context: PlanningContext, -) -> NodeOperationSequence: - ... - - -def lower_node_operations_to_patches( - operations: NodeOperationSequence, - context: PlanningContext, -) -> list[dict[str, str]]: - ... -``` - -이 초안의 의도는 다음과 같습니다. - -- planner 출력은 patch dict가 아니라 `VisibleEditSequence`입니다. -- lowering 계층은 visible edit와 DOM/node edit를 연결합니다. -- strategy 계층은 capability boundary와 fallback 선택을 담당합니다. -- 최종 apply는 기존 `xhtml_patcher.py`를 계속 사용합니다. - -### Stage 3. list / preserved anchor / container 전략 분리 - -목표: - -- 현재 가장 복잡한 capability를 독립 전략 모듈로 분리합니다. - -우선순위: - -1. list visible-segment 전략 -2. preserved-anchor template rewrite 전략 -3. parameter-bearing container reconstruction 전략 - -세부 방향: - -- list는 현재 `visible_segments.py`를 그대로 출발점으로 삼되, 결과를 `visible edit sequence`로 명시화합니다. -- paragraph / heading은 새 visible model extractor를 도입해 plain-text transfer보다 planner 기반 lowering을 우선 검토합니다. -- preserved anchor / container는 visible planning 결과만으로 충분하지 않은 경우 template-preserving reconstruction으로 안전하게 내려갑니다. - -완료 기준: - -- 각 전략이 독립 테스트 파일과 fixture 세트를 가집니다. -- `patch_builder.py`는 분기 지점만 남고 세부 알고리즘을 직접 품지 않습니다. -- text-bearing block에 대해서는 "MDX visible model -> edit sequence -> node operation" 흐름을 설명할 수 있습니다. - -### Stage 4. table 경계 명시화 - -목표: - -- table 지원을 넓히기 전에 먼저 boundary를 명확히 하고, verifier가 formatting-level 차이를 과도하게 mismatch로 보지 않도록 정리합니다. - -작업: - -1. 현재 skip reason을 capability 문서와 1:1로 연결 -2. markdown table / raw HTML table / preserved-anchor table을 명확히 분리 -3. 어떤 table이 "지원 대상"인지 acceptance contract를 먼저 고정 -4. whitespace-only / column-width-only 차이는 `formatting_only_match`로 분류하는 verifier 규칙을 정의 - -완료 기준: - -- table 관련 버그가 들어왔을 때 "미지원"과 "회귀"를 혼동하지 않습니다. -- whitespace-only / column-width-only table diff는 더 이상 semantic mismatch로 집계되지 않습니다. -- 새로운 지원 확대는 capability 추가로 표현합니다. - -### Stage 5. verifier contract 정리 - -목표: - -- patch quality 문제와 normalization 문제를 분리하고, proof 계층의 결과 언어를 고정합니다. - -작업: - -1. `verify_roundtrip()` 결과를 Accept / Review / Block / Error taxonomy로 재분류 -2. `--lenient`, `--no-normalize`의 의미를 문서와 테스트에서 고정 -3. converter normalization 변화로 인한 false failure를 `normalized_match` 또는 `formatting_only_match`와 구분해 분리 -4. proof 계층에서 result classification과 push eligibility를 연결하는 규칙 정의 - -완료 기준: - -- fail 및 review 원인을 `semantic review` / `capability block` / `normalization-driven accept` / `system error`로 구분할 수 있습니다. - -### Stage 6. push rollout contract 정리 - -목표: - -- verify pass와 push eligibility를 같은 개념으로 취급하지 않도록 정리합니다. - -작업: - -1. push safety gate 문서화 -2. conflict / remote drift / dry-run / human confirmation 경계 정리 -3. CLI 출력과 result.yaml에 push-blocked reason 추가 - -완료 기준: - -- "verify는 통과했지만 왜 push하지 않았는가"가 결과에 명시됩니다. - ---- - -## 8. 테스트 계획 - -### 8.1 테스트 계층 - -1. Contract tests - - sidecar schema - - planner classification - - visible edit sequence schema / invariants - - result taxonomy - -2. Strategy tests - - list visible segment - - paragraph/heading visible segment lowering - - preserved anchor rewrite - - container reconstruction - - table classification - -3. Fixture integration tests - - `tests/testcases/` - - `original.mdx` / `improved.mdx` / `expected.reverse-sync.patched.xhtml` - -4. Batch manifest tests - - `tests/reverse-sync/pages.yaml` - - capability/expected_status/failure_type/result taxonomy alignment 검증 - -### 8.2 테스트 원칙 - -- 새 capability를 추가할 때는 representative fixture를 반드시 함께 추가합니다. -- skip reason을 바꿀 때는 result taxonomy 테스트도 함께 갱신합니다. -- verifier normalization 변경은 strategy 테스트가 아니라 verifier contract 테스트로 검증합니다. -- `pages.yaml`은 실데이터 기준 reference metadata catalog이며, 테스트에서는 regression reporting asset으로도 활용합니다. - -### 8.3 우선 추가할 테스트 - -1. planner classification snapshot test -2. table skip reason classification test -3. list visible segment capability fixture set -4. paragraph/heading visible edit sequence snapshot test -5. verifier reason-code split test -6. result.yaml schema test - -### 8.4 초기 테스트 파일 배치안 - -- `confluence-mdx/tests/test_visible_planner.py` - - planner classification snapshot - - paragraph/heading visible edit sequence snapshot -- `confluence-mdx/tests/test_operation_lowering.py` - - visible edit sequence -> node operation lowering - - node operation -> patch emission -- `confluence-mdx/tests/test_strategy_dispatcher.py` - - capability별 strategy 선택 - - fallback / skip reason 결정 -- `confluence-mdx/tests/test_list_strategy.py` - - 기존 list visible segment fixture 재사용 -- `confluence-mdx/tests/test_text_block_strategy.py` - - paragraph / heading text-bearing block fixture -- `confluence-mdx/tests/test_preserved_anchor_strategy.py` - - preserved anchor fallback / reconstruction 경계 -- `confluence-mdx/tests/test_container_strategy.py` - - parameter-bearing container reconstruction 경계 -- `confluence-mdx/tests/test_table_strategy.py` - - table capability boundary / skip reason / formatting-only verifier 연계 - -fixture 배치 초안: - -- `confluence-mdx/tests/testcases/visible-planner/paragraph-*` -- `confluence-mdx/tests/testcases/visible-planner/heading-*` -- `confluence-mdx/tests/testcases/visible-planner/list-*` -- `confluence-mdx/tests/testcases/strategies/preserved-anchor-*` -- `confluence-mdx/tests/testcases/strategies/container-*` -- `confluence-mdx/tests/testcases/strategies/table-*` - -이 배치는 planner, lowering, strategy, proof를 테스트 파일 수준에서도 분리하기 위한 최소 단위 제안입니다. - ---- - -## 9. 문서 구조 계획 - -최종적으로 reverse-sync 문서는 다음 구조를 목표로 합니다. - -- `docs/architecture.md` - - 현재 코드가 실제로 어떻게 동작하는지 설명 -- `docs/analysis-reverse-sync-refactoring.md` - - 현재 구현 상태와 구조적 리스크 분석 -- `docs/plans/2026-04-25-reverse-sync-replacement-plan.md` - - 앞으로의 변경 계획 -- (선택) capability matrix 문서 - - 지원/미지원 범위를 표로 관리 - -즉, 현황 / 분석 / 계획을 분리합니다. - ---- - -## 10. 기존 계획 문서에서 유지할 것과 버릴 것 - -### 유지할 것 - -- fail-closed 원칙 -- sidecar 기반 identity 보존 방향 -- reconstruction 중심 접근 -- 테스트 우선 사고 -- regression gate를 먼저 세우는 태도 - -### 버릴 것 - -- phase 번호 중심 서술 -- 이미 삭제된 모듈을 기준으로 한 cleanup 계획 -- `mapping.yaml` 단독 설명으로 전체 reverse-sync를 설명하려는 방식 -- 구현 완료/미완료 상태와 계획을 한 문서에 섞는 방식 - ---- - -## 11. 승인 기준 - -이 계획은 아래 방향 합의를 기준선으로 삼고 실행합니다. - -1. capability registry naming을 유지합니다. -2. planner / strategy / proof 3계층 분리를 코드 정리의 중심축으로 삼습니다. -3. table은 지원 확대보다 boundary 명시와 verifier normalization 보정을 우선합니다. -4. `pages.yaml`는 실데이터 기준 reference metadata catalog로 보고, 이 점을 과도한 설계 쟁점으로 확대하지 않습니다. -5. verifier taxonomy는 세부적으로 분류합니다. -6. visible segment model을 planner 계층의 중심 표현으로 승격하되, apply 계층은 patch/DOM/sidecar 체계를 유지합니다. - -즉, 이제 남은 것은 큰 방향의 재논의가 아니라, 위 기준선 아래에서 capability 매핑과 proof taxonomy를 실제 코드/CLI/테스트에 반영하는 일입니다. - ---- - -## 12. 이번 리뷰에서 합의된 방향 - -이 PR은 Draft이며, 아래 항목은 이번 리뷰에서 방향이 합의된 상태로 간주합니다. - -1. capability registry naming은 유지합니다. -2. table은 현 수준의 지원을 유지하되, verifier에서 whitespace-only / column-width-only 차이를 mismatch가 아니라 `formatting_only_match`로 판정하는 쪽을 우선합니다. -3. planner / strategy / proof 3계층 분리는 실제 코드 정리의 중심축으로 채택합니다. -4. `pages.yaml`는 reverse-sync 관점에서 reference metadata catalog로 보고, 테스트 전용 부가 필드 때문에 역할 논의를 과도하게 확대하지 않습니다. -5. verifier taxonomy는 운영 판단에 직접 쓰일 수 있도록 세부적으로 분류합니다. -6. visible segment 기반 `edit sequence -> XHTML node operation` 흐름은 text-bearing block의 목표 설계로 채택하되, sidecar/template-preserving fallback을 함께 유지합니다. - ---- - -## 13. 결론 - -예전 계획의 핵심 직감은 맞았습니다. reverse-sync는 heuristic patch를 계속 덧대는 방향만으로는 안정화되지 않습니다. 다만 현재 시점에는 이미 많은 reconstruction/sidecar 기반 구현이 main에 들어와 있으므로, 이제 필요한 것은 "새 phase를 더 추가하는 문서"가 아니라 다음입니다. - -- 현재 구조를 capability 기준으로 다시 설명하고 -- 정책 엔진을 분해할 수 있는 계획을 세우고 -- 지원 범위와 한계를 문서/테스트/CLI에서 같은 언어로 표현하는 것 - -이 문서는 그 전환을 위한 새로운 기준 문서입니다. diff --git a/openspec/changes/README.md b/openspec/changes/README.md index 481ae104a..55f6225dc 100644 --- a/openspec/changes/README.md +++ b/openspec/changes/README.md @@ -1,7 +1,12 @@ # OpenSpec Changes 이 디렉토리는 proposed requirement 또는 contract change를 보관합니다. -현재 active change는 없습니다. + +## Active Changes + +| Change | 목적 | 상태 | +| --- | --- | --- | +| [`complete-reverse-sync`](./complete-reverse-sync/proposal.md) | snapshot-bound MDX → Confluence page update 계약과 구현 계획을 정의합니다. | proposed | ## Change 구조 diff --git a/openspec/changes/complete-reverse-sync/design.md b/openspec/changes/complete-reverse-sync/design.md new file mode 100644 index 000000000..31eaa3a48 --- /dev/null +++ b/openspec/changes/complete-reverse-sync/design.md @@ -0,0 +1,593 @@ +## Context + +### 현재 구현 기준선 + +현재 reverse-sync의 표준 경로는 `confluence-mdx/bin/reverse_sync_cli.py::run_verify()`입니다. + +1. original/improved MDX를 파싱하고 block diff를 계산합니다. +2. 로컬 `var//page.xhtml`에서 mapping과 roundtrip sidecar를 생성합니다. +3. `patch_builder.py`가 변경별 patch strategy를 선택합니다. +4. `xhtml_patcher.py`가 patch를 적용하여 `reverse-sync.patched.xhtml`을 만듭니다. +5. patched XHTML을 forward converter로 다시 MDX로 변환합니다. +6. `roundtrip_verifier.py`가 improved MDX와 비교합니다. +7. `pass`이면 `_do_push()`가 Confluence REST API로 page body를 갱신합니다. + +2026-07-24에 최신 `origin/main`에서 확인한 검증 기준선은 다음과 같습니다. + +| 검증 | 결과 | 증명하는 범위 | +| --- | ---: | --- | +| reverse-sync Python unit test | 676 passed | parser, mapping, patch, reconstruction, verifier, CLI의 로컬 동작 | +| 정밀 reverse-sync golden fixture | 16 passed | expected patched XHTML과 중간 artifact | +| 실제 회귀 page fixture | 43 passed | 과거 verify 실패 page의 현재 round-trip | +| byte-equal fast path | 21/21 passed | 미변경 document 재조립 | +| byte-equal forced splice | 21/21 passed | 미변경 fragment와 separator 보존 | + +이 기준선은 기존 fragment-preserving 접근을 버릴 이유가 없음을 보여줍니다. 반면 emitter 단독 전체 재생성은 `confluence-mdx/docs/architecture.md`에 기록된 normalize-diff 기준으로 1/21만 통과합니다. MDX가 Confluence Storage XHTML의 모든 macro parameter, local ID, attachment metadata, layout, preservation unit을 표현하지 않기 때문입니다. + +### 현재 구현의 correctness gap + +| ID | 현재 작동 | 위험 | +| --- | --- | --- | +| C1 | original MDX는 기본적으로 `main:`, XHTML은 로컬 `var//page.xhtml`을 사용합니다. | 두 입력이 같은 Confluence version에서 생성되었다는 보장이 없습니다. | +| C2 | push 직전에 원격 최신 version을 읽고 그 값에 `+1`을 적용합니다. | stale 로컬 XHTML로 만든 body도 최신 version에 덮어쓸 수 있습니다. | +| C3 | version과 body를 별도 GET으로 조회합니다. | 두 응답 사이에 page가 바뀌면 backup과 version이 서로 다른 snapshot일 수 있습니다. | +| C4 | verifier는 title을 제거하고 여러 공백·빈 list item·문장 경계·table padding을 정규화합니다. | 사람이 볼 수 있는 변경 또는 실제 미반영을 `pass`로 분류할 수 있습니다. | +| C5 | block identity가 line range, content hash, normalized text fallback에 분산되어 있습니다. | 중복 문단, block reorder, 구조 변경에서 잘못된 fragment를 고를 수 있습니다. | +| C6 | push는 `var//reverse-sync.patched.xhtml`을 직접 읽습니다. | verify 후 artifact가 바뀌어도 검증 결과와 push payload의 결합을 확인하지 않습니다. | +| C7 | PUT 성공 후 원격 body를 다시 검증하지 않습니다. | Confluence canonicalization, draft reconciliation, 잘못된 payload를 발견하지 못합니다. | +| C8 | Confluence Cloud는 current page update를 draft와 reconciliation할 수 있습니다. | active draft의 편집 의도와 충돌하거나 draft를 덮을 수 있습니다. | +| C9 | title 변경과 새 attachment 참조가 body round-trip에서 별도 mutation으로 검증되지 않습니다. | title 변경이 조용히 무시되거나 존재하지 않는 attachment를 참조할 수 있습니다. | + +가장 치명적인 문제는 C2입니다. 현재 `_do_push()`는 원격 latest version을 읽은 뒤, 그 version에서 가져온 body가 아니라 과거 로컬 snapshot에서 만든 body를 PUT합니다. 이는 optimistic locking처럼 보이지만 실제로는 “검증한 base와 update base가 동일하다”는 compare-and-set 조건을 만족하지 않습니다. + +### 외부 API 제약 + +[Confluence Cloud REST API v2 Page](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/)는 Storage representation과 version을 포함한 page 조회 및 version을 포함한 page update를 제공합니다. 또한 current version update가 draft에 reconciliation될 수 있고, 두 content가 크게 다르면 제공한 current body가 draft를 덮을 수 있음을 명시합니다. + +따라서 version number만 확인해서는 충분하지 않습니다. + +- 검증에 사용한 body와 원격 current body가 같은지 확인해야 합니다. +- current와 active draft의 충돌 가능성을 별도 gate로 다뤄야 합니다. +- PUT 성공을 최종 성공으로 보지 말고 저장된 body를 다시 검증해야 합니다. + +## Goals / Non-Goals + +### Goals + +1. MDX 변경 의도만 현재 Confluence page의 검증된 snapshot에 적용합니다. +2. MDX로 표현되지 않는 기존 Storage XHTML 정보를 보존합니다. +3. source snapshot, patch artifact, push payload, persisted result를 hash로 연결합니다. +4. stale base, ambiguous mapping, unsupported capability, active draft를 fail-closed로 차단합니다. +5. push 전 local proof와 push 후 remote proof를 구분하고 모두 추적합니다. +6. 각 실패가 input drift, capability boundary, patch error, verifier mismatch, remote conflict 중 무엇인지 reason code로 드러나게 합니다. +7. 기존 676개 unit test와 59개 page fixture, 21개 byte-equal fixture를 migration safety net으로 유지합니다. + +### Non-Goals + +- 임의의 Confluence Storage XHTML을 MDX만으로 완전 재생성하지 않습니다. +- 첫 구현에서 서로 다른 원격 편집과 MDX 변경을 자동 three-way merge하지 않습니다. +- 첫 구현에서 새 attachment upload, attachment 삭제, page title 변경을 지원하지 않습니다. +- batch push에 all-or-nothing 분산 transaction을 제공하지 않습니다. +- active draft를 자동 merge하거나 강제로 덮지 않습니다. +- 구현 PR에서 모든 기존 heuristic을 한 번에 제거하지 않습니다. + +## First Principles + +### 원칙 1: MDX는 완전한 Confluence source가 아닙니다 + +MDX는 public documentation의 source of truth이지만, Confluence Storage XHTML의 byte-level source of truth는 아닙니다. forward conversion에서 의도적으로 사라지는 정보가 있으므로 다음 식은 성립하지 않습니다. + +```text +emit(parse(forward(storage_xhtml))) == storage_xhtml +``` + +따라서 reverse-sync는 full regeneration이 아니라 검증된 base XHTML에 대한 intent-preserving transformation이어야 합니다. + +### 원칙 2: 성공은 “생성”이 아니라 “불변조건 증명”입니다 + +XHTML을 생성했다는 사실은 성공 조건이 아닙니다. 최소한 다음을 함께 증명해야 합니다. + +- base snapshot이 original MDX와 대응합니다. +- 모든 MDX intent가 계획되었고 skip이 없습니다. +- unchanged source fragment가 보존됩니다. +- candidate XHTML을 forward conversion한 결과가 target MDX와 동등합니다. +- push 시점의 remote snapshot이 base snapshot과 같습니다. +- 저장된 remote 결과가 target과 동등합니다. + +### 원칙 3: version과 body는 하나의 snapshot입니다 + +`version`, `title`, `storage_xhtml`을 독립적으로 조회하고 조합하지 않습니다. 하나의 API response 또는 같은 version을 명시한 조회 결과를 `PageSnapshot`으로 고정해야 합니다. + +### 원칙 4: 모호한 자동 merge보다 명시적 중단이 안전합니다 + +base 이후 Confluence에서 변경이 발생하면 사용자는 먼저 fetch → forward conversion → MDX merge를 수행해야 합니다. reverse-sync가 원격 변경과 MDX 변경을 임의로 합치지 않습니다. + +### 원칙 5: diagnostic equivalence와 push equivalence를 분리합니다 + +`--lenient` 또는 광범위한 regex normalization은 분석에는 유용하지만 publish proof로 사용할 수 없습니다. push equivalence는 versioned canonical model과 명시적으로 승인된 formatting-only 차이만 허용해야 합니다. + +## Decisions + +### Decision: snapshot-bound three-way transformation을 채택합니다 + +입력을 세 개의 명시적 값으로 정의합니다. + +- `B` — Confluence에서 획득한 base `PageSnapshot` +- `O` — `B`를 forward conversion하여 얻었거나 base parity를 통과한 original MDX +- `I` — 사용자가 변경한 improved MDX + +출력은 `C = apply(plan(diff(O, I), B), B)`인 candidate Storage XHTML입니다. + +```text + one remote read + │ + ▼ + B: PageSnapshot + version + title + body + hash + │ + ┌────────────┴────────────┐ + ▼ ▼ + forward_convert(B) preserve(B) + │ │ + ▼ │ + base parity(O) │ + │ │ +O ───────┴──── diff ───── I │ + │ │ + ▼ │ + intent planner │ + │ │ + ▼ ▼ + patch renderer ────────> C + │ + ▼ + local proof + SyncManifest + │ + ▼ + remote preflight: R == B + │ + ▼ + PUT version B+1 + │ + ▼ + remote postcondition proof +``` + +이 모델에서 로컬 `page.xhtml`은 snapshot metadata 없이 push source가 될 수 없습니다. offline verify에는 사용할 수 있지만 결과는 `push_eligible: false`입니다. + +### Decision: `PageSnapshot`을 원격 일관성의 최소 단위로 둡니다 + +`PageSnapshot`은 최소한 다음 필드를 가집니다. + +```yaml +page_id: "544112828" +status: current +title: User Agent +version: 42 +storage_sha256: "..." +storage_xhtml: "

...

" +fetched_at: "2026-07-24T00:00:00Z" +api: confluence-v2 +active_draft: false +``` + +규칙: + +- body와 version을 가능한 한 단일 v2 GET으로 조회합니다. +- adapter는 response에서 `page_id`, `status`, `version`, body representation을 검증합니다. +- body hash는 UTF-8 bytes의 SHA-256으로 계산합니다. +- snapshot은 실행별 artifact directory에 immutable하게 저장합니다. +- `page.v1.yaml`, `page.v2.yaml`, `page.xhtml`을 조합하여 원격 snapshot처럼 취급하지 않습니다. + +### Decision: base parity를 patch 전에 검증합니다 + +`forward_convert(B.storage_xhtml)`과 `O`의 content가 push equivalence 기준으로 일치해야 합니다. + +불일치 시 다음 중 하나로 분류합니다. + +- `stale_original_mdx` — base snapshot이 main MDX보다 새롭습니다. +- `stale_page_snapshot` — original MDX가 base snapshot보다 새롭거나 다른 page에서 왔습니다. +- `forward_converter_drift` — converter 변경으로 동일 snapshot의 canonical MDX가 바뀌었습니다. +- `page_identity_mismatch` — page ID, path, `confluenceUrl`이 일치하지 않습니다. + +base parity 실패 상태에서 patch를 계속 만들 수는 있지만 결과는 diagnostic이며 push할 수 없습니다. 기본 CLI는 즉시 block합니다. + +### Decision: block identity는 provenance-first로 해결합니다 + +우선순위는 다음과 같습니다. + +1. base snapshot에서 forward conversion 시 생성한 sidecar block identity +2. exact fragment path + source fragment hash +3. MDX block hash + source line range + block family +4. 명시적으로 단일 후보임이 증명된 structural match + +normalized text prefix는 push-eligible path에서 identity source로 사용하지 않습니다. 중복 block 또는 다중 후보가 있으면 `ambiguous_target`으로 block합니다. + +추가/삭제/reorder는 stable neighbor identity를 기준으로 계획합니다. + +- insert는 `before_block_id`와 `after_block_id` 중 하나 이상을 가져야 합니다. +- delete는 exact base fragment identity를 가져야 합니다. +- reorder는 source와 target order가 명시적으로 증명되지 않으면 첫 구현에서 block합니다. + +### Decision: unchanged fragment는 byte-preserving, changed fragment는 capability-driven으로 처리합니다 + +document envelope, separator, unchanged top-level fragment는 base snapshot의 bytes를 그대로 사용합니다. + +changed fragment는 capability registry에 따라 다음 전략 중 하나를 택합니다. + +| 전략 | 조건 | 처리 | +| --- | --- | --- | +| `template_rewrite` | 원본 wrapper/macro를 보존하며 visible segment만 안전하게 바꿀 수 있음 | 원본 fragment template 위에서 typed segment를 변경 | +| `owned_replace` | emitter가 block 구조를 완전히 소유하고 preservation unit이 없음 | canonical fragment를 새로 emit | +| `container_reconstruct` | sidecar reconstruction metadata가 충분함 | outer wrapper를 보존하고 body를 재구성 | +| `insert_owned_block` | 새 block type과 link/attachment dependency가 지원됨 | canonical fragment 삽입 | +| `delete_exact_fragment` | 삭제 대상 identity가 exact하고 보호 metadata 정책을 충족함 | fragment 삭제 | +| `blocked` | identity 또는 preservation proof가 부족함 | XHTML을 만들지 않고 reason code 반환 | + +기존 `visible_segments.py`의 개념은 paragraph, heading, list 등 text-bearing block의 planning model로 확장합니다. 다만 arbitrary Confluence macro를 universal visible model로 일반화하지 않습니다. + +### Decision: capability registry를 code와 test의 공통 언어로 둡니다 + +초기 registry는 다음과 같이 정의합니다. + +| Capability | 초기 상태 | 비고 | +| --- | --- | --- | +| `paragraph_visible_edit` | supported | link/inline preservation unit은 template proof 필요 | +| `heading_visible_edit` | supported | page title에 대응하는 H1은 제외 | +| `code_block_replace` | supported | language와 CDATA 안전성 검증 필요 | +| `clean_list_reconstruct` | supported | visible/structural model 일치 필요 | +| `preserved_anchor_template_rewrite` | conditional | anchor target 변경은 별도 capability | +| `container_body_reconstruct` | conditional | parameter-bearing outer wrapper 보존 | +| `simple_markdown_table_replace` | conditional | cell 구조와 preserved anchor 없음 | +| `raw_html_table_edit` | blocked | 명시적으로 승인된 cell text-only 전략 전까지 차단 | +| `unknown_macro_mutation` | blocked | unchanged macro는 byte-preserving | +| `page_title_change` | blocked | 별도 page mutation contract 필요 | +| `new_attachment_reference` | blocked | attachment upload contract 필요 | +| `active_draft_reconciliation` | blocked | 자동 merge하지 않음 | + +registry entry는 다음을 가져야 합니다. + +- capability ID와 support level +- planner/renderer owner +- required evidence +- block reason +- representative unit test와 page fixture + +### Decision: local proof를 여러 독립 gate로 분해합니다 + +`verified` 상태는 다음 gate를 모두 통과해야 합니다. + +1. `source_identity` + - page ID, path, `confluenceUrl`이 일치합니다. +2. `base_parity` + - `forward(B)`와 `O`가 push equivalence 기준으로 동등합니다. +3. `intent_complete` + - `diff(O, I)`의 모든 change가 정확히 하나의 plan operation에 대응합니다. + - `skipped_changes`가 0입니다. +4. `artifact_integrity` + - plan과 candidate body hash가 manifest와 일치합니다. +5. `storage_well_formed` + - namespace-aware parser로 Storage XHTML fragment가 parse됩니다. +6. `preservation` + - unchanged fragments, envelope, separator가 byte-equal입니다. +7. `semantic_roundtrip` + - `forward(C)`와 `I`가 push equivalence 기준으로 동등합니다. +8. `determinism` + - 같은 B/O/I/tool version으로 같은 plan/candidate hash가 생성됩니다. +9. `idempotency` + - candidate를 새 base로 보았을 때 같은 intent가 추가 mutation을 만들지 않습니다. +10. `dependency` + - referenced page와 attachment가 존재하고 resolver error가 없습니다. + +`--lenient` 결과, manual review 결과, formatting-only diagnostic은 이 gate를 대신할 수 없습니다. + +### Decision: push equivalence는 versioned typed canonical model로 정의합니다 + +현재 `roundtrip_verifier.py`의 regex normalization을 세 종류로 분류합니다. + +1. source formatting only + - 예: Markdown table cell padding + - typed model이 content와 structure를 보존함을 증명하면 push equivalence에서 허용할 수 있습니다. +2. rendered-visible + - 예: list marker 뒤 공백, inline boundary, 빈 list item + - push equivalence에서 제거하지 않습니다. +3. unsupported/lossy converter behavior + - 예: title 제거, 임의 문장 merge + - normalization으로 숨기지 않고 capability 또는 converter defect로 block합니다. + +canonical model은 block type, nesting, inline token, visible whitespace policy, link target, attachment filename, macro preservation marker를 포함합니다. policy version을 manifest에 기록하여 verifier 변경 후 과거 artifact를 잘못 재사용하지 않도록 합니다. + +기존 `--no-normalize`는 raw diagnosis, `--lenient`는 triage 용도로 유지할 수 있지만 둘 다 push eligibility와 분리합니다. + +### Decision: `SyncManifest`를 verify와 push 사이의 계약으로 둡니다 + +manifest는 최소한 다음을 기록합니다. + +```yaml +schema_version: 1 +run_id: "..." +tool: + git_sha: "..." + verifier_policy: "reverse-sync-equivalence-v1" +page: + page_id: "544112828" +base: + version: 42 + title: User Agent + storage_sha256: "..." +original_mdx: + descriptor: "main:src/content/ko/..." + sha256: "..." +improved_mdx: + descriptor: "branch:src/content/ko/..." + sha256: "..." +plan: + sha256: "..." + operation_count: 3 +candidate: + storage_sha256: "..." +verification: + status: verified + gates: + base_parity: pass + intent_complete: pass + preservation: pass + semantic_roundtrip: pass +push: + eligible: true + blocked_reasons: [] +``` + +push는 file name이 아니라 manifest가 가리키는 candidate hash를 읽습니다. 다음 중 하나라도 다르면 `artifact_tampered`로 차단합니다. + +- manifest hash +- base snapshot hash +- original/improved MDX hash +- plan hash +- candidate body hash +- verifier policy/tool version + +artifact는 `var//reverse-sync//`에 실행별로 저장합니다. “최근 파일”을 덮어쓰는 방식은 호환 bridge로만 유지합니다. +publisher는 local proof manifest를 수정하지 않고, manifest hash를 참조하는 별도 `PushReceipt`와 post-snapshot을 기록합니다. + +### Decision: push는 preflight compare-and-set과 postcondition으로 구성합니다 + +publisher의 순서는 다음과 같습니다. + +1. verified manifest와 candidate hash를 검증합니다. +2. 원격 current `PageSnapshot R`을 단일 조회로 가져옵니다. +3. `R.page_id`, `R.status`, `R.version`, `R.title`, `R.storage_sha256`를 base `B`와 비교합니다. +4. `R`이 base와 다르지만 이미 improved MDX와 동등하면 PUT을 생략하고 `already_applied`로 기록합니다. +5. active draft가 확인되면 `active_draft`로 차단합니다. +6. base와 다른 나머지 경우는 `remote_drift`로 차단하고 PUT을 호출하지 않습니다. +7. `version = B.version + 1`, `title = B.title`, `body = C`로 update합니다. +8. API conflict는 HTTP 409만 가정하지 않고 adapter가 version conflict response를 표준 reason으로 변환합니다. +9. 성공 응답 후 원격 `PageSnapshot P`를 다시 가져옵니다. +10. `P.version == B.version + 1`이고 `forward(P.body) == I`인지 검증합니다. +11. 통과하면 `remote_verified`, 실패하면 `postcondition_failed`로 기록합니다. + +preflight와 PUT 사이의 race는 API version compare-and-set이 방어합니다. preflight에서 latest version을 읽어 새 base로 채택하지 않습니다. + +active draft 감지 방식은 Confluence v2 adapter contract test와 canary에서 확정합니다. API가 안정적으로 draft 존재 여부를 제공하지 못하면 push를 허용하는 조건과 운영 절차를 별도 승인하기 전까지 자동 push rollout을 중단합니다. + +### Decision: title과 attachment를 조용히 무시하지 않습니다 + +첫 구현에서 다음은 block reason입니다. + +- original/improved frontmatter title 또는 첫 H1이 변경됨 +- frontmatter title과 첫 H1이 일치하지 않음 +- improved MDX가 base에 존재하지 않는 attachment filename을 참조함 +- link resolver가 `#link-error` 또는 ambiguous page를 생성함 + +향후 title update와 attachment upload는 별도 capability와 별도 API transaction으로 추가합니다. body update에 암묵적으로 섞지 않습니다. + +### Decision: batch push는 page별 독립 transaction입니다 + +batch는 다음 순서로 동작합니다. + +1. 모든 page의 plan과 local proof를 먼저 생성합니다. +2. verified manifest 목록과 blocked 목록을 출력합니다. +3. 확인 후 verified page만 page별 preflight → update → postcondition을 수행합니다. +4. 한 page가 conflict 또는 error여도 이미 push된 page를 자동 rollback하지 않습니다. +5. 최종 결과는 page별 상태와 전체 partial success를 모두 반환합니다. + +`--all-or-nothing`처럼 보이는 표현을 사용하지 않습니다. 진정한 원자성이 필요하면 별도의 staging/publishing architecture가 필요합니다. + +### Decision: 상태와 reason code를 분리합니다 + +상태는 workflow lifecycle을 표현합니다. + +```text +planned + ├─ blocked + └─ verified_local + ├─ stale_remote + ├─ push_failed + └─ pushed + ├─ remote_verified + └─ postcondition_failed +``` + +대표 reason code: + +- input: `page_identity_mismatch`, `stale_original_mdx`, `base_parity_mismatch` +- planning: `missing_identity`, `ambiguous_target`, `unsupported_capability` +- proof: `skipped_change`, `preservation_mismatch`, `semantic_mismatch`, `artifact_tampered` +- dependency: `missing_attachment`, `link_resolution_error`, `title_change_unsupported` +- publish: `active_draft`, `remote_drift`, `version_conflict`, `permission_denied` +- postcondition: `persisted_body_mismatch`, `persisted_version_mismatch` +- system: `network_error`, `parse_error`, `converter_error` + +상태와 reason code를 섞지 않으면 “local proof는 통과했지만 remote가 stale”인 경우를 `fail` 한 단어로 뭉개지 않을 수 있습니다. + +### Decision: 자동 rollback은 첫 구현 범위에서 제외하고 recovery evidence를 강화합니다 + +Confluence update는 외부 side effect이며 postcondition 실패 후 완전한 원자적 rollback을 보장할 수 없습니다. 자동 restore가 또 다른 version을 만들고 concurrent edit와 충돌할 수 있기 때문입니다. + +대신 다음을 보장합니다. + +- base snapshot과 pushed candidate를 실행별로 보존합니다. +- response version, remote post-snapshot, error를 manifest에 기록합니다. +- postcondition 실패 시 추가 push를 중단합니다. +- current remote가 방금 쓴 candidate와 동일한 경우에만 수행할 수 있는 conditional restore 절차를 출력합니다. +- 자동 compensating rollback은 별도 OpenSpec change와 canary 검증 후 도입합니다. + +### Decision: 구현 책임을 planner / renderer / proof / publisher로 분리합니다 + +목표 module boundary는 다음과 같습니다. + +| 책임 | 모듈 후보 | +| --- | --- | +| immutable model/state/reason | `reverse_sync/models.py` | +| Confluence snapshot adapter | `reverse_sync/confluence_client.py`, `reverse_sync/snapshot.py` | +| base parity와 dependency gate | `reverse_sync/base_parity.py` | +| capability/identity/edit planning | `reverse_sync/planner.py`, `reverse_sync/capabilities.py` | +| visible edit/node operation | `reverse_sync/visible_model.py`, `reverse_sync/operations.py` | +| capability별 render | `reverse_sync/strategies/**` | +| patch apply | 기존 `xhtml_patcher.py` | +| local proof/equivalence | `reverse_sync/proof.py`, `roundtrip_verifier.py` | +| manifest read/write/integrity | `reverse_sync/manifest.py` | +| preflight/update/postcondition | `reverse_sync/publisher.py` | +| CLI orchestration/display | `reverse_sync_cli.py` | + +분리는 한 번에 수행하지 않습니다. 먼저 snapshot/manifest/publisher safety seam을 추가한 뒤 기존 `build_patches()`를 adapter로 감싸고, capability별 planner와 strategy를 점진적으로 추출합니다. + +## Push Eligibility Matrix + +| Local result | Remote preflight | Push 가능 | 설명 | +| --- | --- | --- | --- | +| `verified_local` | base와 version/body/title 동일, draft 없음 | 가능 | 정상 경로 | +| `no_changes` | remote가 target과 semantic equivalent | update 생략 | `already_applied` | +| `verified_local` | version 또는 body hash 다름 | 불가 | `remote_drift` | +| normalized/lenient match | 무관 | 불가 | diagnostic only | +| skipped/unsupported 존재 | 무관 | 불가 | `intent_complete` 실패 | +| title 변경 | 무관 | 불가 | 첫 구현 범위 밖 | +| 새 attachment 참조 | 무관 | 불가 | attachment transaction 없음 | +| active draft | 무관 | 불가 | draft reconciliation 자동화 없음 | +| artifact hash 불일치 | 무관 | 불가 | 검증 payload와 push payload 불일치 | + +## Alternatives Considered + +### Alternative: improved MDX 전체를 Storage XHTML로 새로 emit + +기각합니다. + +- MDX는 macro parameter, local ID, attachment version, layout, extension metadata를 모두 담지 않습니다. +- emitter 단독 normalize-diff 기준선은 1/21입니다. +- 지원하지 않는 XHTML이 있는 page에서 기존 정보를 광범위하게 손실할 수 있습니다. + +full emitter는 `owned_replace` 가능한 clean fragment와 insert에만 사용합니다. + +### Alternative: 현재 patch pipeline을 유지하고 409 conflict만 처리 + +기각합니다. + +- 현재 코드는 push 직전에 latest version을 읽으므로 stale local body도 latest version에 맞춰 보낼 수 있습니다. +- version/body를 별도 GET으로 읽어 snapshot 일관성도 없습니다. +- Confluence v2 update가 문서화한 draft reconciliation 위험을 다루지 못합니다. + +### Alternative: 원격 drift를 자동 three-way merge + +첫 구현에서는 기각합니다. + +- MDX와 Storage XHTML은 정보량이 다르므로 일반적인 text merge가 아닙니다. +- 같은 block, macro, attachment를 동시에 수정한 경우 conflict 판정이 어렵습니다. +- 잘못된 자동 merge보다 fetch/convert/MDX merge 후 재실행이 안전합니다. + +향후 provenance identity와 capability별 merge semantics가 충분해지면 별도 change로 검토할 수 있습니다. + +### Alternative: ADF를 canonical write format으로 전환 + +이번 change에서는 기각합니다. + +- 현재 forward converter와 fixture, sidecar, public workflow는 Storage XHTML에 기반합니다. +- ADF 전환은 converter, lost_info, macro, attachment, API adapter 전체의 별도 migration입니다. +- snapshot/manifest/compare-and-set 계약은 write representation과 독립적으로 먼저 필요합니다. + +### Alternative: verifier normalization을 더 늘려 pass 비율을 높임 + +기각합니다. + +- pass 비율은 correctness가 아닙니다. +- visible whitespace나 구조 미반영을 숨길 수 있습니다. +- formatting-only equivalence는 typed canonical model에서 명시적으로 정의해야 합니다. + +## Migration Plan + +### Phase 0: contract와 regression red test + +- stale local body가 최신 remote version으로 overwrite되는 현재 동작을 재현합니다. +- split GET snapshot inconsistency, artifact tampering, active draft, postcondition failure를 contract test로 추가합니다. +- push eligibility와 reason code schema를 고정합니다. + +### Phase 1: snapshot/manifest safety foundation + +- Confluence v2 single-snapshot read adapter를 추가합니다. +- `PageSnapshot`, `SyncManifest`, artifact hash 검증을 구현합니다. +- 기존 verify 결과를 manifest로 감싸되 push는 아직 비활성화합니다. + +### Phase 2: base parity와 strict proof + +- page identity, base parity, dependency gate를 추가합니다. +- verifier normalization을 typed equivalence로 분류합니다. +- skipped change와 diagnostic match가 push eligibility를 얻지 못하도록 합니다. + +### Phase 3: transaction-safe publisher + +- manifest-bound payload read, remote preflight, compare-and-set update, postcondition을 구현합니다. +- active draft를 감지하고 차단합니다. +- batch partial success와 recovery artifact를 구현합니다. +- 기존 `_do_push()` 직접 artifact read 경로를 제거합니다. + +### Phase 4: planner/strategy 분해 + +- 기존 `patch_builder.py`를 capability planner와 renderer strategy로 점진 분해합니다. +- text prefix fallback을 push-eligible path에서 제거합니다. +- visible segment model을 paragraph/heading/list planning에 확장합니다. +- unsupported table/macro 구조를 explicit block으로 전환합니다. + +### Phase 5: rollout + +1. fixture와 offline verify만 수행합니다. +2. 실제 branch를 대상으로 remote snapshot read + dry-run을 shadow mode로 수행합니다. +3. 전용 canary page에서 단일 update와 postcondition을 검증합니다. +4. 사람 확인이 있는 단일 page push를 활성화합니다. +5. 충분한 telemetry 후 batch push를 활성화합니다. + +각 단계는 이전 단계의 artifact와 reason code를 수집한 뒤 진행합니다. + +## Risks / Trade-offs + +### 더 많은 작업이 block될 수 있습니다 + +base parity, active draft, attachment gate가 추가되면 기존에 우연히 통과하던 작업이 중단됩니다. 이는 기능 후퇴가 아니라 unsafe success를 명시적 block으로 바꾸는 것입니다. + +### remote read가 늘어납니다 + +page마다 prepare snapshot, push preflight, postcondition read가 필요합니다. correctness를 위해 필요한 비용이며 batch에서는 bounded concurrency와 retry/backoff를 사용합니다. + +### typed equivalence 구현 비용이 큽니다 + +regex normalization보다 설계와 테스트 비용이 큽니다. 대신 무엇을 의미 없는 formatting 차이로 간주하는지 명시할 수 있고, 새로운 converter 변화가 silent acceptance를 만들지 않습니다. + +### active draft API 동작이 환경에 따라 다를 수 있습니다 + +Atlassian API 문서와 실제 QueryPie space의 동작을 canary로 확인해야 합니다. 안정적으로 감지할 수 없으면 자동 push rollout 범위를 축소합니다. + +### postcondition 실패 후 완전 원자성은 없습니다 + +외부 API가 transaction을 제공하지 않으므로 conditional recovery만 가능합니다. 이 한계를 숨기지 않고 상태와 artifact로 드러냅니다. + +### 기존 patch_builder 분해가 장기화될 수 있습니다 + +P0 push safety를 planner refactor와 분리하여 먼저 적용합니다. 기존 patch engine은 local proof를 통과하는 동안 adapter 뒤에서 사용할 수 있습니다. + +## Open Questions + +1. QueryPie Confluence space에서 v2 `get-draft`가 active draft를 안정적으로 식별하는지 canary로 확인해야 합니다. +2. push equivalence v1에서 허용할 formatting-only 차이를 table padding 외에 어디까지 둘지 reviewer 합의가 필요합니다. +3. page title update를 별도 capability로 언제 도입할지 결정해야 합니다. +4. 새 attachment upload와 기존 attachment version 변경을 별도 transaction으로 지원할지 결정해야 합니다. +5. postcondition 실패 시 conditional automatic restore를 도입할지 별도 safety review가 필요합니다. +6. 실행 artifact 보존 기간과 민감 정보 redaction 정책을 정해야 합니다. +7. Confluence v1 client를 즉시 제거할지, v2 adapter 안정화 기간 동안 fallback으로 유지할지 결정해야 합니다. diff --git a/openspec/changes/complete-reverse-sync/proposal.md b/openspec/changes/complete-reverse-sync/proposal.md new file mode 100644 index 000000000..0f96ada4a --- /dev/null +++ b/openspec/changes/complete-reverse-sync/proposal.md @@ -0,0 +1,63 @@ +## Why + +현재 reverse-sync는 MDX 변경을 기존 Confluence Storage XHTML에 반영하고 forward round-trip으로 검증하는 로컬 파이프라인을 갖추고 있습니다. 2026-07-24 기준 reverse-sync unit test 676개, page fixture 59개, byte-equal fixture 21개가 모두 통과합니다. + +그러나 로컬 검증 결과와 실제 Confluence page update 사이에는 안전 계약이 없습니다. 특히 현재 push는 로컬 `page.xhtml`에서 생성한 `reverse-sync.patched.xhtml`을 사용하면서, push 직전에 조회한 원격 최신 version에 `+1`을 적용합니다. 로컬 XHTML이 오래되어도 version conflict가 발생하지 않고 원격 편집을 덮어쓸 수 있습니다. + +또한 원본 MDX와 base XHTML의 동등성, 검증 artifact의 무결성, active draft, attachment 참조, push 후 원격 결과를 필수 gate로 확인하지 않습니다. 따라서 현재의 `pass`는 “로컬 round-trip이 통과했다”는 뜻이지 “이 artifact를 현재 원격 page에 안전하게 반영할 수 있다”는 뜻이 아닙니다. + +이 change는 reverse-sync를 “변환이 되는 도구”에서 “검증한 snapshot에만 안전하게 반영하고 결과까지 증명하는 workflow”로 완성하기 위한 contract와 구현 순서를 정의합니다. + +## What Changes + +- Confluence page의 `version`, `title`, Storage XHTML을 하나의 `PageSnapshot`으로 묶고 body hash로 식별합니다. +- original MDX가 base snapshot의 forward conversion 결과와 동등한지 확인하는 base parity gate를 추가합니다. +- MDX 변경 의도, patch 결과, 검증 결과, 입력/출력 hash를 immutable `SyncManifest`에 기록합니다. +- 지원하지 않거나 모호한 변경, skipped change, diagnostic normalization 결과는 push 대상에서 제외합니다. +- push 직전에 원격 snapshot이 검증에 사용한 base와 동일한지 version과 body hash로 재확인합니다. +- version compare-and-set update 후 원격 page를 다시 조회하여 target MDX와의 postcondition을 검증합니다. +- `planner / renderer / proof / publisher` 책임을 분리하고 capability별 지원 범위와 reason code를 표준화합니다. +- 첫 구현 범위에서는 page title 변경, 새 attachment upload, active draft reconciliation을 자동 처리하지 않고 명시적으로 block합니다. +- batch push는 page별 독립 transaction으로 정의하고 부분 성공을 명시적으로 보고합니다. + +## Capabilities + +### New Capabilities + +- `contract-reverse-sync` + - snapshot-bound MDX → Confluence Storage XHTML 생성 + - base parity 및 immutable manifest + - fail-closed push eligibility + - 원격 drift 감지와 compare-and-set update + - post-write semantic verification 및 recovery evidence + +### Modified Capabilities + +- 기존 reverse-sync의 `pass / fail / no_changes` 결과를 planning, verification, push eligibility, remote postcondition 상태로 분리합니다. +- 기존 sidecar와 fragment reconstruction을 source snapshot 보존 계층으로 유지하되, text heuristic과 normalization에 의존한 성공 판정을 축소합니다. +- 기존 `reverse_sync_cli.py push`는 verified manifest를 소비하는 publisher로 전환합니다. + +## Impact + +- 주요 구현 후보 + - `confluence-mdx/bin/reverse_sync_cli.py` + - `confluence-mdx/bin/reverse_sync/confluence_client.py` + - `confluence-mdx/bin/reverse_sync/patch_builder.py` + - `confluence-mdx/bin/reverse_sync/roundtrip_verifier.py` + - `confluence-mdx/bin/reverse_sync/sidecar.py` + - 신규 snapshot, manifest, planner, proof, publisher 모듈 +- 주요 테스트 후보 + - `confluence-mdx/tests/test_reverse_sync_cli.py` + - 신규 snapshot/manifest/push transaction contract test + - `confluence-mdx/tests/test_reverse_sync_*.py` + - `confluence-mdx/tests/testcases/**` + - `confluence-mdx/tests/reverse-sync/**` +- 운영 영향 + - stale fetch 또는 base parity 불일치 상태에서는 push가 차단됩니다. + - 기존 `--lenient`는 진단 용도로만 남고 push eligibility를 부여하지 않습니다. + - 기존 backup은 유지하되, version/hash가 포함된 실행별 artifact로 대체합니다. +- 관련 자료 + - `confluence-mdx/docs/architecture.md` + - `confluence-mdx/docs/analysis-reverse-sync-refactoring.md` + - GitHub issue `#960` + - [Confluence Cloud REST API v2 Page](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/) diff --git a/openspec/changes/complete-reverse-sync/specs/contract-reverse-sync/spec.md b/openspec/changes/complete-reverse-sync/specs/contract-reverse-sync/spec.md new file mode 100644 index 000000000..af9af3bf1 --- /dev/null +++ b/openspec/changes/complete-reverse-sync/specs/contract-reverse-sync/spec.md @@ -0,0 +1,316 @@ +# contract-reverse-sync + +## Purpose + +MDX 변경을 기존 Confluence Storage XHTML의 보존 정보와 안전하게 결합하고, 검증한 원격 page snapshot에만 반영하며, 저장된 결과가 target MDX와 동등함을 증명하는 durable contract를 정의합니다. + +## References + +- `openspec/changes/complete-reverse-sync/proposal.md` +- `openspec/changes/complete-reverse-sync/design.md` +- `confluence-mdx/docs/architecture.md` +- `confluence-mdx/bin/reverse_sync_cli.py` +- `confluence-mdx/bin/reverse_sync/**` +- [Confluence Cloud REST API v2 Page](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-page/) + +## Requirements + +### Requirement: Consistent PageSnapshot + +reverse-sync는 Confluence page의 `page_id`, `status`, `title`, `version`, Storage XHTML을 하나의 논리적으로 일관된 `PageSnapshot`으로 획득하고 Storage XHTML의 SHA-256을 기록해야 합니다(SHALL). + +version과 body를 서로 다른 시점의 독립 조회에서 가져와 하나의 snapshot으로 조합해서는 안 됩니다(SHALL NOT). + +#### Scenario: 원격 current page snapshot 획득 + +- GIVEN 사용자가 reverse-sync prepare 또는 online verify를 실행합니다. +- WHEN 도구가 Confluence current page를 조회합니다. +- THEN 하나의 API response 또는 동일 version을 명시한 조회에서 page ID, status, title, version, Storage XHTML을 획득해야 합니다(SHALL). +- AND body hash와 fetch 시각, API adapter version을 snapshot에 기록해야 합니다(SHALL). + +#### Scenario: offline XHTML 입력 + +- GIVEN 사용자가 snapshot metadata가 없는 로컬 `page.xhtml`로 verify합니다. +- WHEN local round-trip이 통과합니다. +- THEN 도구는 진단 결과를 제공할 수 있습니다(MAY). +- AND 결과를 push eligible로 표시해서는 안 됩니다(SHALL NOT). + +#### Scenario: snapshot field 불일치 + +- GIVEN API response의 page ID, status, representation, version 중 필수 필드가 없거나 요청과 다릅니다. +- WHEN snapshot adapter가 response를 파싱합니다. +- THEN 도구는 `invalid_page_snapshot`으로 block해야 합니다(SHALL). +- AND 불완전한 snapshot으로 patch 또는 push를 계속해서는 안 됩니다(SHALL NOT). + +### Requirement: Base Parity + +reverse-sync는 patch를 push eligible로 판정하기 전에 base snapshot을 forward conversion한 canonical MDX와 original MDX의 page identity 및 content 동등성을 확인해야 합니다(SHALL). + +#### Scenario: base와 original MDX가 대응함 + +- GIVEN `PageSnapshot B`와 original MDX `O`가 있습니다. +- WHEN `B.storage_xhtml`을 현재 forward converter와 동일한 dependency catalog로 변환합니다. +- THEN 변환 결과와 `O`의 page ID, `confluenceUrl`, content가 push equivalence policy에서 일치해야 합니다(SHALL). +- AND 사용한 converter/tool version과 입력 hash를 manifest에 기록해야 합니다(SHALL). + +#### Scenario: stale original MDX + +- GIVEN 원격 base snapshot에는 original MDX에 없는 content가 있습니다. +- WHEN base parity를 검증합니다. +- THEN 결과는 `stale_original_mdx` 또는 `base_parity_mismatch`로 block되어야 합니다(SHALL). +- AND reverse-sync는 원격 content를 target MDX로 조용히 덮어써서는 안 됩니다(SHALL NOT). + +#### Scenario: converter drift + +- GIVEN 동일한 Storage XHTML이 converter 변경으로 과거 original MDX와 다르게 변환됩니다. +- WHEN base parity를 검증합니다. +- THEN 결과는 일반 patch mismatch와 구분되는 `forward_converter_drift` evidence를 포함해야 합니다(SHALL). +- AND reviewer가 equivalence policy 또는 original MDX를 갱신하기 전까지 push eligible이 되어서는 안 됩니다(SHALL NOT). + +### Requirement: Intent-Complete Patch Plan + +reverse-sync는 original MDX `O`와 improved MDX `I`의 모든 변경을 provenance가 확인된 base XHTML fragment에 대응시키는 deterministic patch plan을 생성해야 합니다(SHALL). + +모호한 identity, unsupported capability, skipped change가 존재하는 plan을 verified로 판정해서는 안 됩니다(SHALL NOT). + +#### Scenario: 지원되는 block 변경 + +- GIVEN original/improved MDX 사이에 지원되는 paragraph 변경이 있습니다. +- AND original block은 base snapshot의 exact fragment identity를 가집니다. +- WHEN planner가 변경을 분석합니다. +- THEN 변경은 정확히 하나의 capability와 target fragment, operation에 대응해야 합니다(SHALL). +- AND plan은 old/new block hash와 target fragment hash를 포함해야 합니다(SHALL). + +#### Scenario: 중복 block의 target이 모호함 + +- GIVEN 동일한 content를 가진 block이 여러 개이고 provenance로 target을 하나로 확정할 수 없습니다. +- WHEN planner가 target을 결정합니다. +- THEN `ambiguous_target`으로 block해야 합니다(SHALL). +- AND normalized text prefix만으로 임의의 target을 선택해서는 안 됩니다(SHALL NOT). + +#### Scenario: unsupported 구조 변경 + +- GIVEN improved MDX가 preserved macro 또는 raw HTML table의 지원되지 않는 구조를 변경합니다. +- WHEN planner가 capability를 분류합니다. +- THEN `unsupported_capability`과 capability ID를 반환해야 합니다(SHALL). +- AND patch를 일부만 적용하거나 skipped change를 success로 숨겨서는 안 됩니다(SHALL NOT). + +#### Scenario: insert와 delete + +- GIVEN improved MDX가 block을 추가하거나 삭제합니다. +- WHEN planner가 operation을 생성합니다. +- THEN insert는 stable neighbor identity를, delete는 exact base fragment identity를 가져야 합니다(SHALL). +- AND 필요한 identity가 없으면 `missing_identity`로 block해야 합니다(SHALL). + +### Requirement: Preserve Unmodeled XHTML + +reverse-sync는 MDX 변경 의도에 포함되지 않은 document envelope, separator, fragment, macro parameter, local ID, attachment metadata를 byte-preserving 방식으로 유지해야 합니다(SHALL). + +#### Scenario: unchanged fragment + +- GIVEN base snapshot의 한 fragment에 대응하는 MDX block이 변경되지 않았습니다. +- WHEN candidate XHTML을 생성합니다. +- THEN 해당 fragment bytes와 인접 separator는 base snapshot과 같아야 합니다(SHALL). + +#### Scenario: template-preserving edit + +- GIVEN changed paragraph가 Confluence link 또는 attachment preservation unit을 포함합니다. +- AND capability가 visible text 변경만 지원합니다. +- WHEN renderer가 candidate fragment를 생성합니다. +- THEN 원본 wrapper와 preservation unit의 target/metadata를 유지하고 승인된 visible segment만 변경해야 합니다(SHALL). + +#### Scenario: preservation proof 부족 + +- GIVEN renderer가 unmodeled markup을 안전하게 보존했는지 증명할 수 없습니다. +- WHEN candidate fragment를 검증합니다. +- THEN `preservation_uncertain` 또는 `preservation_mismatch`로 block해야 합니다(SHALL). + +### Requirement: Strict Local Proof + +reverse-sync는 candidate XHTML을 push eligible로 표시하기 전에 source identity, base parity, intent completeness, artifact integrity, XHTML well-formedness, unchanged fragment preservation, semantic round-trip, determinism, idempotency, dependency gate를 모두 통과해야 합니다(SHALL). + +#### Scenario: local proof 통과 + +- GIVEN plan의 모든 operation이 candidate XHTML에 적용되었습니다. +- WHEN local proof를 실행합니다. +- THEN candidate XHTML의 forward conversion 결과는 improved MDX와 versioned push equivalence policy에서 동등해야 합니다(SHALL). +- AND 모든 필수 gate가 `pass`이고 blocked reason이 없을 때만 상태를 `verified_local`로 설정해야 합니다(SHALL). + +#### Scenario: skipped change + +- GIVEN patch engine이 하나 이상의 MDX 변경을 skip했습니다. +- WHEN local proof 결과를 조립합니다. +- THEN `intent_complete` gate는 실패해야 합니다(SHALL). +- AND round-trip normalization이 우연히 일치하더라도 push eligible이 되어서는 안 됩니다(SHALL NOT). + +#### Scenario: lenient match + +- GIVEN strict push equivalence는 실패하지만 `--lenient` 비교는 통과합니다. +- WHEN 결과를 출력합니다. +- THEN diagnostic match와 strict failure를 함께 보고할 수 있습니다(MAY). +- AND 상태를 `verified_local`로 설정해서는 안 됩니다(SHALL NOT). + +#### Scenario: title normalization + +- GIVEN original/improved MDX의 title 또는 첫 H1이 다릅니다. +- WHEN body reverse-sync proof를 실행합니다. +- THEN title을 비교에서 제거하여 success로 만들지 않고 `title_change_unsupported`로 block해야 합니다(SHALL). + +### Requirement: Immutable SyncManifest + +reverse-sync는 base snapshot, original/improved MDX, patch plan, candidate XHTML, verifier policy, tool version의 hash를 포함하는 실행별 `SyncManifest`를 생성해야 합니다(SHALL). + +push는 verified manifest가 가리키는 candidate만 사용할 수 있습니다(SHALL). +publisher는 verified manifest를 수정하지 않고 manifest hash를 참조하는 별도 push receipt와 post-snapshot을 기록해야 합니다(SHALL). + +#### Scenario: verify 후 push + +- GIVEN local proof를 통과한 manifest가 있습니다. +- WHEN publisher가 push를 준비합니다. +- THEN manifest와 모든 referenced artifact의 hash를 다시 계산해야 합니다(SHALL). +- AND hash가 모두 일치할 때만 remote preflight로 진행해야 합니다(SHALL). + +#### Scenario: candidate artifact 변경 + +- GIVEN verify 후 candidate XHTML이 수정되었습니다. +- WHEN publisher가 manifest integrity를 검증합니다. +- THEN `artifact_tampered`로 block해야 합니다(SHALL). +- AND 수정된 body를 PUT해서는 안 됩니다(SHALL NOT). + +#### Scenario: verifier policy 변경 + +- GIVEN manifest가 기록한 verifier policy 또는 tool version이 현재 publisher가 허용한 version과 다릅니다. +- WHEN publisher가 artifact를 로드합니다. +- THEN 재검증을 요구하거나 `stale_verification`으로 block해야 합니다(SHALL). + +### Requirement: Remote Compare-And-Set + +publisher는 PUT 직전에 원격 current snapshot을 다시 획득하고, 검증에 사용한 base snapshot과 page ID, status, version, title, Storage body hash가 동일한지 확인해야 합니다(SHALL). + +publisher는 preflight에서 얻은 latest version을 검증 base로 암묵적으로 승격해서는 안 됩니다(SHALL NOT). + +#### Scenario: remote가 base와 동일함 + +- GIVEN verified manifest의 base version과 body hash가 원격 current snapshot과 같습니다. +- AND active draft가 없습니다. +- WHEN publisher가 update를 요청합니다. +- THEN base version에 정확히 1을 더한 version과 manifest의 candidate body를 사용해야 합니다(SHALL). + +#### Scenario: remote body drift + +- GIVEN remote version 또는 body hash가 verified manifest의 base와 다릅니다. +- AND remote content가 improved MDX와 동등하지 않습니다. +- WHEN publisher가 preflight합니다. +- THEN `remote_drift`로 block해야 합니다(SHALL). +- AND PUT을 호출해서는 안 됩니다(SHALL NOT). + +#### Scenario: preflight와 PUT 사이의 race + +- GIVEN preflight 후 다른 사용자가 page를 갱신했습니다. +- WHEN publisher가 base version + 1로 update합니다. +- THEN API conflict를 `version_conflict`로 분류해야 합니다(SHALL). +- AND 최신 version을 다시 읽어 같은 candidate를 자동 재시도해서는 안 됩니다(SHALL NOT). + +#### Scenario: active draft + +- GIVEN page에 current version과 다른 active draft가 있습니다. +- WHEN publisher가 preflight합니다. +- THEN `active_draft`로 block해야 합니다(SHALL). +- AND 명시적으로 승인된 draft reconciliation capability가 없으면 current page update를 수행해서는 안 됩니다(SHALL NOT). + +### Requirement: Remote Postcondition + +publisher는 update 응답을 성공의 최종 증거로 취급하지 않고 persisted remote snapshot을 다시 조회하여 version과 semantic target을 검증해야 합니다(SHALL). + +#### Scenario: persisted result 검증 + +- GIVEN Confluence update API가 성공을 반환했습니다. +- WHEN publisher가 current page를 다시 조회합니다. +- THEN persisted version은 base version + 1이어야 합니다(SHALL). +- AND persisted Storage XHTML의 forward conversion 결과는 improved MDX와 push equivalence policy에서 동등해야 합니다(SHALL). +- AND 통과한 경우에만 상태를 `remote_verified`로 설정해야 합니다(SHALL). + +#### Scenario: persisted body mismatch + +- GIVEN update API는 성공했지만 persisted body가 target MDX와 동등하지 않습니다. +- WHEN postcondition을 검증합니다. +- THEN 상태를 `postcondition_failed`로 기록해야 합니다(SHALL). +- AND base snapshot, attempted candidate, persisted snapshot, response version을 recovery evidence로 보존해야 합니다(SHALL). +- AND 후속 batch push를 기본적으로 중단해야 합니다(SHALL). + +#### Scenario: 이미 적용된 target + +- GIVEN remote snapshot이 base와 다르지만 이미 improved MDX와 semantic equivalent입니다. +- WHEN 사용자가 동일한 intent를 다시 실행합니다. +- THEN PUT을 생략하고 `already_applied`로 보고할 수 있습니다(MAY). +- AND remote content가 target과 동등하다는 postcondition evidence를 기록해야 합니다(SHALL). + +### Requirement: Explicit Dependency Boundaries + +첫 reverse-sync completion 범위는 body content update로 제한하며 title 변경, 새 attachment lifecycle, unresolved link를 암묵적으로 처리해서는 안 됩니다(SHALL NOT). + +#### Scenario: title 변경 + +- GIVEN improved MDX가 frontmatter title 또는 page title H1을 변경합니다. +- WHEN planner가 변경을 분석합니다. +- THEN `title_change_unsupported`로 block해야 합니다(SHALL). + +#### Scenario: 새 attachment + +- GIVEN improved MDX가 base page에 존재하지 않는 attachment filename을 참조합니다. +- WHEN dependency gate를 실행합니다. +- THEN `missing_attachment`로 block해야 합니다(SHALL). +- AND broken `ri:attachment` 참조를 포함한 body를 push해서는 안 됩니다(SHALL NOT). + +#### Scenario: unresolved internal link + +- GIVEN link resolver가 target page를 하나로 결정하지 못합니다. +- WHEN candidate XHTML을 생성합니다. +- THEN `link_resolution_error`로 block해야 합니다(SHALL). + +### Requirement: Page-Scoped Batch Semantics + +batch reverse-sync는 각 page를 독립 transaction으로 처리하고 전체 batch가 원자적인 것처럼 보고해서는 안 됩니다(SHALL NOT). + +#### Scenario: batch local proof + +- GIVEN branch에 여러 MDX page 변경이 있습니다. +- WHEN batch verify를 실행합니다. +- THEN 모든 page의 local proof를 push 전에 완료하고 verified/blocked manifest를 구분해야 합니다(SHALL). + +#### Scenario: batch partial success + +- GIVEN 첫 page는 `remote_verified`이고 두 번째 page는 `remote_drift`입니다. +- WHEN batch push가 종료됩니다. +- THEN 첫 page의 성공을 rollback된 것처럼 표시해서는 안 됩니다(SHALL NOT). +- AND 전체 상태를 partial success로 표시하고 page별 상태를 반환해야 합니다(SHALL). + +#### Scenario: postcondition failure + +- GIVEN batch 중 한 page가 `postcondition_failed`가 되었습니다. +- WHEN 다음 page를 처리하려고 합니다. +- THEN 기본 동작은 남은 push를 중단해야 합니다(SHALL). +- AND 사용자가 별도 실행으로 재개할 수 있는 manifest 목록을 제공해야 합니다(SHALL). + +### Requirement: Recoverable and Auditable Operation + +reverse-sync는 실행별 base snapshot, plan, candidate, verification, push response, post-snapshot을 보존하고 credential 또는 API token을 artifact에 기록해서는 안 됩니다(SHALL NOT). + +#### Scenario: push 실패 조사 + +- GIVEN push가 conflict, network error, postcondition failure 중 하나로 종료되었습니다. +- WHEN maintainer가 run artifact를 확인합니다. +- THEN page ID, base/persisted version, relevant hash, reason code, 재현 가능한 local verification 정보를 확인할 수 있어야 합니다(SHALL). + +#### Scenario: recovery 안내 + +- GIVEN postcondition failure가 발생했습니다. +- WHEN 도구가 결과를 출력합니다. +- THEN base version과 backup artifact 위치를 포함한 수동 recovery 안내를 제공해야 합니다(SHALL). +- AND current remote가 attempted candidate와 동일한지 확인하지 않은 자동 restore를 수행해서는 안 됩니다(SHALL NOT). + +#### Scenario: credential redaction + +- GIVEN API request/response와 error가 artifact에 기록됩니다. +- WHEN manifest와 log를 직렬화합니다. +- THEN email, API token, Authorization header를 포함해서는 안 됩니다(SHALL NOT). diff --git a/openspec/changes/complete-reverse-sync/tasks.md b/openspec/changes/complete-reverse-sync/tasks.md new file mode 100644 index 000000000..d70c6cdb8 --- /dev/null +++ b/openspec/changes/complete-reverse-sync/tasks.md @@ -0,0 +1,271 @@ +## 1. Contract + +- [x] 현재 `main`의 reverse-sync CLI, patch engine, sidecar, verifier, Confluence client를 조사합니다. +- [x] local verify와 remote page update 사이의 correctness gap을 식별합니다. +- [x] `contract-reverse-sync` change-local spec을 작성합니다. +- [x] snapshot-bound transformation, base parity, manifest, compare-and-set, postcondition을 design decision으로 기록합니다. +- [ ] reviewer와 다음 contract boundary를 확정합니다. + - push equivalence v1의 formatting-only 허용 범위 + - active draft 감지 방식과 rollout 조건 + - title/attachment capability의 별도 change 필요 여부 + - artifact 보존 기간과 redaction 정책 + +## 2. Implementation + +### 2.1 P0 — 현재 unsafe push를 red test로 고정 + +- [ ] `confluence-mdx/tests/test_reverse_sync_push_transaction.py`를 추가합니다. +- [ ] stale local `page.xhtml` + latest remote version 조합이 PUT으로 이어지는 현재 문제를 재현합니다. +- [ ] version과 body를 별도 GET으로 읽을 때 서로 다른 snapshot이 될 수 있는 문제를 재현합니다. +- [ ] preflight 이후 concurrent update가 발생하면 자동 retry하지 않는 계약을 테스트합니다. +- [ ] verify 후 candidate body가 바뀌면 push를 차단하는 테스트를 추가합니다. +- [ ] PUT 성공 후 remote body가 target과 다르면 `postcondition_failed`가 되는 테스트를 추가합니다. +- [ ] active draft, title 변경, missing attachment, lenient-only pass가 push blocked인지 테스트합니다. + +완료 gate: + +- 새 contract test가 현재 구현에서 의도한 이유로 실패합니다. +- 기존 unit/page/byte-equal fixture 결과는 그대로 유지됩니다. + +### 2.2 P0 — immutable model과 artifact layout + +- [ ] `confluence-mdx/bin/reverse_sync/models.py`를 추가합니다. + - `PageSnapshot` + - `SyncManifest` + - `VerificationGate` + - `SyncStatus` + - `ReasonCode` + - `PushReceipt` +- [ ] `confluence-mdx/bin/reverse_sync/manifest.py`를 추가합니다. + - canonical serialization + - SHA-256 계산 + - referenced artifact integrity 검사 + - schema version 및 verifier policy 검사 +- [ ] artifact를 `var//reverse-sync//`에 실행별로 저장합니다. +- [ ] 기존 `reverse-sync.*` 경로는 최신 run을 가리키는 read-only compatibility output으로 제한합니다. +- [ ] email, token, Authorization header redaction test를 추가합니다. + +완료 gate: + +- 같은 입력과 tool version에서 manifest/candidate hash가 deterministic합니다. +- candidate 또는 manifest를 수정하면 publisher 이전에 `artifact_tampered`가 발생합니다. + +### 2.3 P0 — consistent Confluence snapshot adapter + +- [ ] `confluence-mdx/bin/reverse_sync/confluence_client.py`에 v2 page snapshot 조회를 추가합니다. +- [ ] `GET /wiki/api/v2/pages/{id}`에서 Storage body와 version을 한 snapshot으로 획득합니다. +- [ ] `page_id`, `status=current`, body representation, title, version 필수 필드를 검증합니다. +- [ ] active draft 조회/감지 adapter를 분리하고 실제 API 응답 fixture를 추가합니다. +- [ ] HTTP 400/409 등 provider response를 `version_conflict`, `permission_denied`, `network_error`로 변환합니다. +- [ ] 기존 `get_page_version()` + `get_page_body()` 조합을 push 경로에서 제거합니다. + +완료 gate: + +- publisher가 서로 다른 GET의 version/body를 조합할 수 없습니다. +- v2 response contract와 error mapping unit test가 통과합니다. +- QueryPie canary page에서 current/draft 응답 shape를 read-only로 확인합니다. + +### 2.4 P0 — base parity와 dependency gate + +- [ ] `confluence-mdx/bin/reverse_sync/base_parity.py`를 추가합니다. +- [ ] remote snapshot의 forward conversion 결과와 original MDX를 비교합니다. +- [ ] page ID, `confluenceUrl`, repository MDX path를 함께 검증합니다. +- [ ] `stale_original_mdx`, `forward_converter_drift`, `page_identity_mismatch`를 구분합니다. +- [ ] original/improved title과 첫 H1 invariant를 검증하고 title change를 block합니다. +- [ ] attachment catalog에서 improved MDX의 attachment reference를 검증합니다. +- [ ] internal link resolver error와 ambiguous target을 dependency failure로 변환합니다. +- [ ] snapshot metadata가 없는 offline verify는 `push_eligible: false`로 표시합니다. + +완료 gate: + +- base parity를 통과하지 않은 입력으로 patch/push가 진행되지 않습니다. +- title, attachment, link dependency가 조용히 무시되지 않습니다. + +### 2.5 P0 — strict proof와 push eligibility + +- [ ] `confluence-mdx/bin/reverse_sync/proof.py`를 추가하여 필수 gate를 orchestration합니다. +- [ ] `roundtrip_verifier.py` normalization을 source formatting, rendered-visible, unsupported/lossy로 분류합니다. +- [ ] push equivalence v1 typed canonical model을 구현합니다. +- [ ] `skipped_changes > 0`이면 `intent_complete`를 실패시킵니다. +- [ ] `--lenient`와 `--no-normalize` 결과를 diagnostic field로 이동합니다. +- [ ] unchanged fragment, separator, document envelope byte-equal을 proof에 포함합니다. +- [ ] well-formed Storage XHTML, determinism, idempotency 검사를 추가합니다. +- [ ] 기존 `pass`를 `verified_local`과 분리합니다. + +완료 gate: + +- 모든 required gate를 통과한 manifest만 `push_eligible: true`입니다. +- visible whitespace 또는 title 차이가 normalization으로 숨지 않습니다. +- 기존 fixture 중 policy 변경으로 block되는 case는 reason code와 reviewer 결정을 함께 기록합니다. + +### 2.6 P0 — transaction-safe publisher + +- [ ] `confluence-mdx/bin/reverse_sync/publisher.py`를 추가합니다. +- [ ] verified manifest와 candidate artifact hash를 다시 검증합니다. +- [ ] remote preflight snapshot의 page ID/status/version/title/body hash를 base와 비교합니다. +- [ ] active draft가 있으면 PUT 전에 block합니다. +- [ ] base version + 1과 base title, manifest candidate body로 update합니다. +- [ ] conflict 시 latest version으로 자동 retry하지 않습니다. +- [ ] update 후 persisted snapshot을 다시 조회합니다. +- [ ] persisted version과 target MDX semantic postcondition을 검증합니다. +- [ ] base, candidate, response, post-snapshot을 recovery evidence로 저장합니다. +- [ ] postcondition failure 시 batch 후속 push를 기본 중단합니다. +- [ ] 현재 `_do_push()`가 `reverse-sync.patched.xhtml`을 직접 읽는 경로를 제거합니다. + +완료 gate: + +- stale remote에서 PUT 호출이 0회입니다. +- preflight/PUT race는 `version_conflict`로 종료됩니다. +- API 성공만으로 `remote_verified`가 되지 않습니다. + +### 2.7 P1 — CLI와 batch state 전환 + +- [ ] `reverse_sync_cli.py`를 prepare/verify/push lifecycle orchestration으로 축소합니다. +- [ ] `verify` 출력에 run ID, base version/hash, local gates, push eligibility, reason code를 표시합니다. +- [ ] `push`가 explicit run/manifest를 받도록 합니다. +- [ ] interactive confirmation에 page ID, base version, target version, change count, candidate hash를 표시합니다. +- [ ] batch는 모든 local proof 후 page별 transaction을 실행합니다. +- [ ] batch partial success, conflict, postcondition failure exit code와 JSON schema를 정의합니다. +- [ ] resume는 `remote_verified`/blocked 상태를 재해석하지 않고 명시적 manifest 목록으로 수행합니다. +- [ ] `--yes`가 safety gate를 우회하지 못하도록 테스트합니다. + +완료 gate: + +- human confirmation과 `--yes`는 사용자 확인만 제어하고 base/proof/preflight gate를 건너뛰지 않습니다. +- batch가 all-or-nothing처럼 출력되지 않습니다. + +### 2.8 P1 — planner / renderer 책임 분리 + +- [ ] `patch_builder.py`의 capability 판별을 `capabilities.py`와 planner로 추출합니다. +- [ ] planning output을 typed `PatchPlan`/operation으로 바꾸고 raw patch dict를 boundary 안에 가둡니다. +- [ ] block identity를 provenance-first 순서로 바꿉니다. +- [ ] normalized text prefix fallback을 push-eligible path에서 제거합니다. +- [ ] visible segment model을 paragraph, heading, list에 확장합니다. +- [ ] strategy를 text block, list, preserved anchor, container, table로 분리합니다. +- [ ] unsupported table/macro 구조를 explicit block reason으로 전환합니다. +- [ ] 기존 `xhtml_patcher.py`는 validated operation 적용에 집중하도록 축소합니다. + +완료 gate: + +- 새 capability 추가가 `build_patches()` 본체의 heuristic branch 추가를 요구하지 않습니다. +- 모든 operation은 capability, target identity, required proof, reason code를 가집니다. + +### 2.9 P2 — capability 확대 + +- [ ] page title update를 body update와 분리된 capability/change로 설계합니다. +- [ ] attachment upload/version update/delete transaction을 별도 capability/change로 설계합니다. +- [ ] preserved anchor target 변경을 지원할지 결정합니다. +- [ ] raw HTML table cell text-only mutation의 typed proof를 설계합니다. +- [ ] active draft reconciliation 또는 draft-aware publish를 별도 capability/change로 설계합니다. +- [ ] remote drift three-way merge는 provenance와 conflict semantics가 준비된 뒤 별도 change로 검토합니다. + +## 3. Verification + +### 3.1 Focused contract test + +- [ ] 다음 명령으로 snapshot, manifest, proof, publisher contract test를 실행합니다. + +```bash +cd confluence-mdx/tests +../venv/bin/python3 -m pytest -q \ + test_reverse_sync_snapshot.py \ + test_reverse_sync_manifest.py \ + test_reverse_sync_proof.py \ + test_reverse_sync_push_transaction.py +``` + +### 3.2 Existing reverse-sync unit regression + +- [ ] 기존 reverse-sync unit suite를 실행합니다. + +```bash +cd confluence-mdx/tests +../venv/bin/python3 -m pytest -q \ + test_reverse_sync*.py \ + test_lost_info_patcher.py +``` + +기준선: 2026-07-24 `origin/main`에서 676 passed입니다. + +### 3.3 Page fixture regression + +- [ ] 정밀 golden fixture와 실제 회귀 fixture를 실행합니다. + +```bash +cd confluence-mdx/tests +make test-reverse-sync +``` + +기준선: + +- `tests/testcases/**`: 16 passed +- `tests/reverse-sync/**`: 43 passed + +### 3.4 Byte preservation + +- [ ] fast path와 forced splice byte-equal을 실행합니다. + +```bash +cd confluence-mdx/tests +make test-byte-verify +``` + +기준선: + +- fast path: 21/21 passed +- forced splice: 21/21 passed + +### 3.5 Broader converter regression + +- [ ] verifier/equivalence 또는 emitter를 변경한 PR은 다음을 추가로 실행합니다. + +```bash +cd confluence-mdx/tests +make test-convert +make test-reverse-sync +make test-byte-verify +``` + +- [ ] 영향도에 따라 전체 Python test와 render test를 실행합니다. + +```bash +cd confluence-mdx/tests +../venv/bin/python3 -m pytest -q +make test-render +``` + +### 3.6 Remote canary + +- [ ] 별도로 승인된 Confluence canary page에서 current/draft snapshot response를 read-only로 수집합니다. +- [ ] canary page의 version/body hash를 고정한 dry-run manifest를 생성합니다. +- [ ] 사람 확인 후 단일 trivial text edit를 push합니다. +- [ ] post-snapshot version, body hash, forward round-trip, Confluence UI 표시를 확인합니다. +- [ ] 같은 manifest 재실행이 PUT 없이 `already_applied` 또는 stale manifest로 종료되는지 확인합니다. +- [ ] canary 검증 전에는 production batch push를 활성화하지 않습니다. + +## 4. Spec / 구현 drift 확인 + +- [ ] `PageSnapshot` 구현이 version/body를 하나의 논리적 response에서 가져오는지 source scan합니다. +- [ ] publisher가 latest remote version을 새 base로 채택하는 fallback이 없는지 검색합니다. +- [ ] `skipped_changes`, `--lenient`, title strip이 push eligibility를 부여하지 않는지 확인합니다. +- [ ] push가 filename이 아니라 manifest candidate hash를 소비하는지 확인합니다. +- [ ] 모든 PUT 호출 경로가 preflight와 postcondition을 거치는지 확인합니다. +- [ ] batch output이 page별 상태와 partial success를 노출하는지 확인합니다. +- [ ] credential이 manifest/log/fixture에 포함되지 않았는지 검색합니다. + +```bash +rg -n "update_page|requests\\.put|method.?=.?(PUT|put)" confluence-mdx/bin +rg -n "get_page_version|get_page_body|latest.*version|version.*\\+ *1" confluence-mdx/bin/reverse_sync* +rg -n "skipped_changes|lenient|strip_first_heading|push_eligible" confluence-mdx/bin/reverse_sync* +rg -n "api_token|Authorization|confluence\\.conf" confluence-mdx/var confluence-mdx/tests \ + --glob 'reverse-sync/**' --glob '!*.py' +``` + +## 5. OpenSpec Cleanup + +- [ ] P0/P1 구현과 canary가 완료되면 change-local `contract-reverse-sync`를 accepted spec으로 승격합니다. +- [ ] `openspec/specs/README.md` inventory에 `contract-reverse-sync`를 추가합니다. +- [ ] `docs/architecture.md`의 current-state 설명을 최종 module/state 이름과 동기화합니다. +- [ ] `.agents/skills/reverse-sync/SKILL.md`의 CLI와 artifact 경로를 새 lifecycle에 맞게 갱신합니다. +- [ ] 중복된 historical plan은 짧은 bridge link만 남기거나 archive합니다. +- [ ] 완료된 change를 `openspec/archive/-complete-reverse-sync/`로 이동합니다.