From d85d9f4b9a2b39e19e95e919fb7aa852bdb3c1a6 Mon Sep 17 00:00:00 2001 From: JK Date: Fri, 24 Jul 2026 23:24:25 +0900 Subject: [PATCH 1/2] =?UTF-8?q?confluence-mdx:=20P2=20capability=20?= =?UTF-8?q?=EA=B2=BD=EA=B3=84=EB=A5=BC=20=EC=84=A4=EA=B3=84=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 ## Summary 확장 capability의 안전 경계와 별도 change 진입 조건을 결정합니다. - page title mutation을 versioned page postcondition으로 분리합니다. - attachment create/update/delete의 비원자적 transaction과 recovery evidence를 정의합니다. - preserved anchor target 변경은 generic template rewrite에서 제외합니다. - raw HTML table cell text mutation의 typed identity/proof를 정의합니다. - active draft와 remote drift는 자동 merge 없이 fail-closed를 유지합니다. 🤖 Generated with Codex Co-Authored-By: Atlas --- .../changes/complete-reverse-sync/design.md | 152 +++++++++++++++++- .../changes/complete-reverse-sync/tasks.md | 24 ++- 2 files changed, 165 insertions(+), 11 deletions(-) diff --git a/openspec/changes/complete-reverse-sync/design.md b/openspec/changes/complete-reverse-sync/design.md index 16ad1c89c..93d0cc72f 100644 --- a/openspec/changes/complete-reverse-sync/design.md +++ b/openspec/changes/complete-reverse-sync/design.md @@ -46,6 +46,8 @@ [Confluence Cloud REST API v2 Attachment](https://developer.atlassian.com/cloud/confluence/rest/v2/api-group-attachment/)는 page attachment 목록과 cursor pagination을 제공합니다. reverse-sync는 모든 page를 조회한 current attachment catalog를 새 attachment reference의 dependency snapshot으로 사용합니다. +[Confluence Cloud REST API v1 Content - attachments](https://developer.atlassian.com/cloud/confluence/rest/v1/api-group-content---attachments/)는 multipart upload와 attachment ID 기반 binary update를 제공합니다. v2는 attachment delete를 제공하지만 write API가 하나의 versioned transaction으로 통합되어 있지 않으므로 page body update와 attachment mutation을 원자적으로 묶을 수 없습니다. + 따라서 version number만 확인해서는 충분하지 않습니다. - 검증에 사용한 body와 원격 current body가 같은지 확인해야 합니다. @@ -510,6 +512,148 @@ macro로 렌더링하고, target page ID/status/title을 push 직전에 재검 향후 title update와 attachment upload/update/delete는 별도 capability와 별도 API transaction으로 추가합니다. body update에 암묵적으로 섞지 않습니다. +### Decision: P2 capability는 현재 body contract와 분리합니다 + +P2는 현재 publisher의 허용 범위를 넓히지 않습니다. 각 항목은 별도 OpenSpec +change, typed intent, manifest schema, canary를 가져야 하며 그 전까지 기존 +block reason을 유지합니다. + +#### Page title mutation + +`page_title_change`는 body block diff가 아니라 page resource mutation입니다. +후속 change는 최소한 다음 계약을 가져야 합니다. + +- `PageTitleMutation(base_title, target_title)`을 body `PatchPlan`과 분리해 + manifest에 기록합니다. +- original frontmatter title, original H1, `PageSnapshot.title`이 같은 + base identity를 가리키고 improved frontmatter title과 improved H1도 + `target_title`로 일치해야 합니다. +- preflight에서 page ID, status, version, base title, body hash, active draft를 + 현재 body publisher와 동일하게 확인합니다. +- title과 body를 함께 바꾸는 경우 `PUT /pages/{id}`의 versioned payload에 + target title과 candidate body를 함께 넣고, 하나의 target version으로 + postcondition을 검증합니다. +- title-only mutation도 base body를 candidate body로 고정한 versioned page + update로 처리합니다. version precondition이 없는 `PUT /pages/{id}/title`을 + reverse-sync publisher의 CAS 경계로 사용하지 않습니다. +- postcondition은 target version, target title, body hash/semantic roundtrip을 + 모두 확인합니다. + +이 capability는 body canary 이후 별도 change로 도입합니다. 현재 +`title_change_unsupported`는 유지합니다. + +#### Attachment lifecycle + +attachment mutation은 page body PUT과 원자적으로 묶을 수 없으므로 +`new_attachment_lifecycle` 하나로 뭉개지 않고 다음 typed intent로 나눕니다. + +```text +AttachmentCreate(filename, blob_sha256, media_type, expected_absent) +AttachmentVersionUpdate(attachment_id, filename, base_version, blob_sha256) +AttachmentDelete(attachment_id, filename, base_version, expected_unreferenced) +``` + +각 intent는 source file hash, target page ID, catalog hash, attachment ID와 +version precondition을 manifest에 기록해야 합니다. filename upsert는 같은 +filename의 identity가 유일하게 증명된 create에만 제한하고, 기존 attachment +update는 attachment ID 기반 endpoint를 사용합니다. + +transaction 순서는 다음과 같습니다. + +1. current page와 전체 attachment catalog를 preflight합니다. +2. create/update를 실행하고 post-catalog에서 ID, filename, version, binary + identity를 확인합니다. +3. 해당 attachment를 참조하는 page body를 versioned PUT하고 page + postcondition을 확인합니다. +4. delete는 candidate body에서 reference가 제거되고 page postcondition이 + 통과한 뒤에만 실행합니다. + +page update 실패 전에 attachment create/update가 성공하면 orphan 또는 불필요한 +attachment version이 남을 수 있고, delete는 복원 불가능할 수 있습니다. 따라서 +자동 rollback을 성공으로 간주하지 않으며 실행별 receipt와 manual recovery +절차를 남깁니다. create, update, delete는 각각 별도 canary를 통과하기 전까지 +`missing_attachment` 또는 `new_attachment_lifecycle`로 block합니다. + +#### Preserved anchor target + +현재 `preserved_anchor_template_rewrite`는 Confluence-owned link/image/macro의 +target metadata를 보존하면서 visible text와 whitespace만 바꾸는 capability로 +고정합니다. preserved unit의 `href`, `ac:anchor`, `ri:page` title/ID, +`ri:attachment` filename이 바뀌는 mutation은 지원하지 않습니다. + +- base/candidate의 preservation-unit target fingerprint가 다르면 + `unsupported_capability`로 block해야 합니다. +- Markdown source가 명시적으로 소유하는 link target 변경은 preserved anchor + mutation이 아니라 기존 link dependency/owned text capability로 처리합니다. +- preserved target mutation을 지원하려면 target 종류별 typed intent와 + dependency preflight/postcondition을 정의하는 별도 change가 필요합니다. + +따라서 generic template rewrite에 target 변경 heuristic을 추가하지 않습니다. + +#### Raw HTML table cell text mutation + +향후 `raw_html_table_edit`를 허용하는 최소 단위는 +`RawTableCellTextMutation`입니다. + +```text +table_root_xpath +table_fragment_sha256 +row_path / cell_path / text_node_path +cell_subtree_sha256 +old_visible_text / new_visible_text +table_structure_fingerprint +preservation_unit_fingerprint +``` + +`table_structure_fingerprint`는 section/row/cell 수, `rowspan`/`colspan`, +cell tag와 attribute를 포함합니다. 다음 조건을 모두 증명한 경우에만 +executable operation을 만들 수 있습니다. + +- 변경 대상은 하나의 cell 안 plain text leaf로 유일하게 식별됩니다. +- table/cell attribute, row/cell 구조, nested block, link, macro, attachment, + inline markup과 preservation unit fingerprint가 base와 같습니다. +- renderer는 table을 다시 emit하지 않고 지정된 text node만 바꿉니다. +- 나머지 table fragment는 byte-equal이고 candidate는 well-formed, + deterministic, idempotent이며 forward roundtrip이 target MDX와 동등합니다. + +row/cell 추가·삭제·reorder, span 변경, nested table, preserved unit 변경은 이 +capability에 포함하지 않습니다. 최소 두 개의 실제 page golden fixture와 +ambiguous text-node negative fixture가 별도 change의 진입 조건입니다. + +#### Active draft + +active draft가 있는 current page update는 공식 API가 content reconciliation을 +수행하고 diverged draft를 덮을 수 있으므로 계속 `active_draft`로 block합니다. +첫 draft-aware change는 자동 merge나 강제 overwrite가 아니라 다음 read-only +reconciliation evidence만 생성합니다. + +- current `PageSnapshot`과 `get-draft` 응답의 page ID, status, version, title, + body hash +- `forward(current)`, `forward(draft)`, improved MDX의 typed diff +- current/draft/improved가 같은 capability target을 변경하는지에 대한 conflict + classification + +사람이 Confluence UI에서 draft를 publish/discard/reconcile한 뒤 새 current +snapshot으로 manifest를 다시 만들어야 합니다. 공식 API와 canary에서 +versioned draft update 및 postcondition 계약을 별도로 증명하기 전에는 +draft write mode를 추가하지 않습니다. + +#### Remote drift merge + +remote drift 자동 three-way merge는 현재 범위에서 도입하지 않기로 결정합니다. +후속 검토의 진입 조건은 다음과 같습니다. + +- base/current/improved 각각의 provenance identity가 같은 stable target을 + 가리킵니다. +- capability마다 commute 가능한 독립 edit와 conflict edit가 정의됩니다. +- preservation unit, macro, attachment, title, draft mutation은 conflict + semantics가 별도로 존재합니다. +- merged candidate가 preservation, semantic roundtrip, determinism, + idempotency gate를 모두 다시 통과합니다. + +이 조건 전에는 `remote_drift`에서 PUT을 0회로 유지하고 fetch → forward +conversion → repository MDX merge → 새 manifest 생성을 요구합니다. + ### Decision: batch push는 page별 독립 transaction입니다 batch는 다음 순서로 동작합니다. @@ -741,8 +885,6 @@ P0 push safety를 planner refactor와 분리하여 먼저 적용합니다. 기 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으로 유지할지 결정해야 합니다. +3. postcondition 실패 시 conditional automatic restore를 도입할지 별도 safety review가 필요합니다. +4. 실행 artifact 보존 기간과 민감 정보 redaction 정책을 정해야 합니다. +5. Confluence v1 client를 attachment write 전용 adapter로 격리할지 결정해야 합니다. diff --git a/openspec/changes/complete-reverse-sync/tasks.md b/openspec/changes/complete-reverse-sync/tasks.md index ba05fbcd5..bfb4410a4 100644 --- a/openspec/changes/complete-reverse-sync/tasks.md +++ b/openspec/changes/complete-reverse-sync/tasks.md @@ -236,12 +236,24 @@ ### 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로 검토합니다. +- [x] page title update를 body update와 분리된 capability/change로 설계합니다. + - version precondition이 없는 title 전용 endpoint는 사용하지 않고, 후속 + capability가 title/body/version을 하나의 page postcondition으로 증명합니다. +- [x] attachment upload/version update/delete transaction을 별도 capability/change로 설계합니다. + - create/update/delete intent와 catalog precondition을 분리하고, page PUT과 + 원자적이지 않은 side effect 및 manual recovery evidence를 명시합니다. +- [x] preserved anchor target 변경을 지원할지 결정합니다. + - generic template rewrite에서는 지원하지 않으며 target fingerprint가 바뀌면 + `unsupported_capability`로 block합니다. +- [x] raw HTML table cell text-only mutation의 typed proof를 설계합니다. + - table/cell/text-node identity, 구조 및 preservation fingerprint, byte + preservation과 semantic proof를 진입 조건으로 정의합니다. +- [x] active draft reconciliation 또는 draft-aware publish를 별도 capability/change로 설계합니다. + - 첫 단계는 read-only reconciliation evidence이며, 자동 merge/overwrite는 + 공식 API와 canary에서 versioned draft write를 증명하기 전까지 제외합니다. +- [x] remote drift three-way merge는 provenance와 conflict semantics가 준비된 뒤 별도 change로 검토합니다. + - capability별 commute/conflict semantics가 준비될 때까지 `remote_drift`에서 + PUT 0회와 manual MDX merge workflow를 유지합니다. ## 3. Verification From 4665053beff0f8a2a40f24ce14e8712f1f11668f Mon Sep 17 00:00:00 2001 From: JK Date: Mon, 27 Jul 2026 15:24:38 +0900 Subject: [PATCH 2/2] =?UTF-8?q?confluence-mdx:=20attachment=20update=20CAS?= =?UTF-8?q?=20=EA=B2=BD=EA=B3=84=EB=A5=BC=20=EB=B3=B4=EA=B0=95=ED=95=A9?= =?UTF-8?q?=EB=8B=88=EB=8B=A4=20(#1049)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary 기존 attachment binary update가 page CAS 밖에서 노출되는 계약 오류를 수정합니다. - AttachmentVersionUpdate를 create/delete transaction 순서에서 분리합니다. - non-destructive staging/body-switch proof 전까지 version update를 block합니다. - old binary 보존과 page postcondition 순서를 OpenSpec Scenario로 고정합니다. ## Test plan - [x] openspec validate complete-reverse-sync --strict - [x] 관련 attachment contract targeted search - [x] git diff --check 🤖 Generated with Codex --- .../changes/complete-reverse-sync/design.md | 30 ++++++++++++++----- .../specs/contract-reverse-sync/spec.md | 8 +++++ .../changes/complete-reverse-sync/tasks.md | 3 ++ 3 files changed, 33 insertions(+), 8 deletions(-) diff --git a/openspec/changes/complete-reverse-sync/design.md b/openspec/changes/complete-reverse-sync/design.md index 93d0cc72f..d1e6ea7f2 100644 --- a/openspec/changes/complete-reverse-sync/design.md +++ b/openspec/changes/complete-reverse-sync/design.md @@ -558,20 +558,34 @@ version precondition을 manifest에 기록해야 합니다. filename upsert는 filename의 identity가 유일하게 증명된 create에만 제한하고, 기존 attachment update는 attachment ID 기반 endpoint를 사용합니다. -transaction 순서는 다음과 같습니다. +`AttachmentCreate`와 `AttachmentDelete`의 transaction 순서는 다음과 같습니다. 1. current page와 전체 attachment catalog를 preflight합니다. -2. create/update를 실행하고 post-catalog에서 ID, filename, version, binary - identity를 확인합니다. -3. 해당 attachment를 참조하는 page body를 versioned PUT하고 page +2. create는 기존 reference가 가리키지 않는 새 attachment identity로 실행하고 + post-catalog에서 ID, filename, version, binary identity를 확인합니다. +3. 새 attachment를 참조하는 page body를 versioned PUT하고 page postcondition을 확인합니다. 4. delete는 candidate body에서 reference가 제거되고 page postcondition이 통과한 뒤에만 실행합니다. -page update 실패 전에 attachment create/update가 성공하면 orphan 또는 불필요한 -attachment version이 남을 수 있고, delete는 복원 불가능할 수 있습니다. 따라서 -자동 rollback을 성공으로 간주하지 않으며 실행별 receipt와 manual recovery -절차를 남깁니다. create, update, delete는 각각 별도 canary를 통과하기 전까지 +`AttachmentVersionUpdate`는 이 순서에 포함하지 않습니다. 기존 attachment의 +binary를 page CAS 전에 갱신하면 base body가 같은 attachment ID와 filename을 +계속 참조하는 동안 새 binary가 즉시 노출됩니다. 이후 page PUT이 +`remote_drift`, version conflict, 또는 `postcondition_failed`로 끝나도 이 +side effect를 되돌릴 수 없으므로 기존 attachment를 먼저 update해서는 안 +됩니다. + +후속 `AttachmentVersionUpdate` change는 기존 attachment를 건드리지 않는 +staging/body-switch 전략을 먼저 증명해야 합니다. 예를 들어 충돌하지 않는 +filename과 새 ID로 binary를 staging하고, staged reference로 page body CAS와 +postcondition을 통과한 뒤에만 기존 attachment를 retire할 수 있습니다. API나 +filename 계약 때문에 old binary를 page CAS 성공 시점까지 보존할 수 없다면 이 +capability는 계속 `new_attachment_lifecycle`로 block합니다. + +page update 실패 전에 attachment create가 성공하면 orphan attachment가 남을 수 +있고, delete는 복원 불가능할 수 있습니다. 따라서 자동 rollback을 성공으로 +간주하지 않으며 실행별 receipt와 manual recovery 절차를 남깁니다. create, +version update, delete는 각각 별도 canary를 통과하기 전까지 `missing_attachment` 또는 `new_attachment_lifecycle`로 block합니다. #### Preserved anchor target 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 index 4033e5333..9a058c91f 100644 --- a/openspec/changes/complete-reverse-sync/specs/contract-reverse-sync/spec.md +++ b/openspec/changes/complete-reverse-sync/specs/contract-reverse-sync/spec.md @@ -283,6 +283,14 @@ attachment upload/update/delete lifecycle, unresolved link를 암묵적으로 - AND candidate는 해당 filename을 가진 `ri:attachment` reference를 생성해야 합니다(SHALL). - AND attachment upload 또는 version 변경을 암묵적으로 실행해서는 안 됩니다(SHALL NOT). +#### Scenario: 기존 attachment binary version update + +- GIVEN improved input이 base body가 참조하는 기존 attachment ID와 filename의 binary version을 변경하려고 합니다. +- WHEN attachment mutation과 page body update를 계획합니다. +- THEN old binary를 page CAS 성공 시점까지 보존하는 staging/body-switch proof가 없으면 `new_attachment_lifecycle`로 block해야 합니다(SHALL). +- AND page body CAS 전에 기존 attachment binary를 update해서는 안 됩니다(SHALL NOT). +- AND staging/body-switch를 지원하는 후속 capability는 새 attachment identity, page reference 전환, page postcondition, 기존 attachment retire 순서를 검증해야 합니다(SHALL). + #### Scenario: unresolved internal link - GIVEN link resolver가 target page를 하나로 결정하지 못합니다. diff --git a/openspec/changes/complete-reverse-sync/tasks.md b/openspec/changes/complete-reverse-sync/tasks.md index bfb4410a4..2bc2275c5 100644 --- a/openspec/changes/complete-reverse-sync/tasks.md +++ b/openspec/changes/complete-reverse-sync/tasks.md @@ -242,6 +242,9 @@ - [x] attachment upload/version update/delete transaction을 별도 capability/change로 설계합니다. - create/update/delete intent와 catalog precondition을 분리하고, page PUT과 원자적이지 않은 side effect 및 manual recovery evidence를 명시합니다. + - 기존 attachment version update는 old binary를 page CAS 성공 시점까지 + 보존하는 staging/body-switch 전략을 별도 change에서 증명하기 전까지 + `new_attachment_lifecycle`로 block합니다. - [x] preserved anchor target 변경을 지원할지 결정합니다. - generic template rewrite에서는 지원하지 않으며 target fingerprint가 바뀌면 `unsupported_capability`로 block합니다.