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..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,5 +1,9 @@ # Reverse Sync 전면 재구성 설계 — 검토 평가 결과 (v5) +> 상태: Superseded review +> 대체 문서: [`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 > 검토 기준 버전: 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..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,5 +1,9 @@ # Reverse Sync 전면 재구성 설계 +> 상태: Superseded +> 대체 문서: [`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 진행 현황 반영 및 설계 기준선 교정) > 기준 브랜치: `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..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,5 +1,9 @@ # Reverse Sync 재구성 후 삭제 대상 범위 +> 상태: Superseded +> 대체 문서: [`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 > 기준 브랜치: `main` 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/`로 이동합니다.