跳至主要内容

ADR-012: Feature 採版本化發布與下游主動升級

ADR 編號: 012 狀態: 已接受 (Accepted) 決策日期: 2026-08-18 決策者: AppFuse Team + AI Assistant 修訂: ADR-006 D-D、ADR-007 D-A′ 的同步方向與狀態模型 範圍: app-*-server Feature 的發布、下游升級、Changelog 與 catalog 基線


摘要

Feature 不再以「來源端廣播 sync、直接改寫 fleet」作為主要更新方式,改採與框架套件相同的 責任分離:

  1. 參考實作以 /release-app 發布不可變的 app-v{CalVer} 快照與 Feature Changelog;
  2. 下游以 /feature provision <id> --from app-v{CalVer} 從不可變快照初次採用 Feature;
  3. 下游在自己的版本、工作區與驗證脈絡中執行 /feature upgrade
  4. canonical core 依目標快照自動收斂;
  5. seam 以上次已協調的上游內容為 base,對下游現況與目標快照做 three-way merge;
  6. 來源端 /feature status 保留為唯讀 fleet dashboard,不再把「發布」等同於「修改所有下游」。

Changelog 負責說明變更意圖、影響與 migration;不可變 tag 中的 source delta 才是實際 patch; catalog 的 per-Feature/per-seam 基線則負責區分尚未升級、已協調的客製與新的上游變更。三者缺一, 工具仍只能反覆顯示無法判讀的 two-way diff。

背景

ADR-006ADR-007 建立了 Feature core/seam、provision、 per-Feature syncedSHA 與 source-to-target sync。這套模型適合 canonical core,卻沒有完整表達 seam 的生命週期:

  • syncedSHA 只代表 core 對齊進度;seam 仍停在 provision 當時;
  • seam 與來源現版不同時,two-way diff 無法區分「純落後」與「有意客製」;
  • 全歷史比對可以找到「下游是否曾等於某個來源版本」,但無法記錄哪一版變更已被採用、拒絕或 人工合併;
  • core 基線若先推進,未處理的 seam、test support 或 reference domain 變更可能從升級區間中消失;
  • 來源端廣播把 upstream release cadence 與 downstream delivery cadence 綁在一起,也讓下游在 缺少自己完整測試與產品脈絡時被動收到 commit。

這與 methodology/practices 的同步面不同。兩者的 canonical 內容原則上不允許在下游分歧, drift 多半可直接判為應收斂或應 reflow;Feature seam 則預期由下游行使宣告權,因此升級是 一次 migration,而不是 publish 的例外分支。

現有參考實作已具備所需的版本基礎:所有 app-* 共用 CalVer,/release-app 原子建立 app-v{YYYY.N.P} tag。該 tag 可同時定位 tenant/tenantless source、catalog、Feature 原始碼與其 實際使用的 appfuse-server 版本。

決策

D-A:發布、初次採用與升級分離

Feature 生命週期分為兩個不同權限邊界:

階段執行位置動作是否修改下游
author參考實作 source修改 Feature 與同次 Feature Changelog
publishworkspace source/release-app 驗證後建立不可變 app-v{CalVer}
observesource 或 downstream/feature status 顯示 release、drift 與 pending migration
provision下游 server/feature provision <id> --from app-v{CalVer}是,只新增當前 Feature
upgrade下游 server/feature upgrade [id] --to app-v{CalVer}是,只改當前下游

來源端可產生 fleet 升級清單、通知或建議命令,但不得以無 target 的 publish/sync 廣播直接修改 下游。下游的寫入必須在下游工作區發起,使用下游自己的測試、交付節奏與審查流程。

provisionupgrade 保持不同命令與不同意圖:前者建立下游原本不存在的 Feature,後者只 演進 catalog 已登錄的 Feature。兩者不得以「自動判斷 present/absent」互相代替,也不得由 upgrade 偷渡新 Feature。兩者共用同一套 Feature transaction engine、write-set guard、驗證、 finalize 與 rollback 能力,但各自擁有 planner 與命令契約。

D-B:app-v{CalVer} 是 Feature release coordinate

正式下游 provision 與升級只接受不可變 app tag,例如 app-v2026.1.7。目標 tag 同時提供:

  • 對應 server type 的 .claude/features.json
  • Feature core、seam、test support 與 reference domain source snapshot;
  • 該參考實作實際依賴的 appfuse-server 版本;
  • 同一 release 的 FEATURE_CHANGELOG.md

一般 provision/升級不得直接追蹤 source HEAD。本機開發與 pilot 可明示 --ref {git SHA},但報告與 catalog 必須標記為 unpublished,不得冒充 app release。

/feature provision <id> --from app-v{CalVer} 從該 snapshot materialize Feature,而不是複製目前 source worktree。成功 finalize 時直接建立完整新 schema:coreRef 指向 --from,每個 seam 與 test support 建立 sourcePath + reconciledRef + baseBlob,每個 reference domain 建立 reviewedRef;不得先寫或補寫 legacy syncedSHA。依賴 Feature 必須已 provision;缺少 requires 時停止並要求明確 provision,不能暗中展開 adoption 範圍。

tenant 與 tenantless 仍是兩個獨立 type。相同 app tag 只提供共同時間座標,不表示兩個 type 的 同名 Feature 內容相同或可跨 type 套用。

D-C:不同 surface 使用不同升級語意

SurfaceOwnership升級行為狀態粒度
coresource canonical由目標快照覆蓋/補回並 retargetper Feature
defaultable seam下游可採 canonical 預設可證明 L 命中歷史 canonical 時自動快轉;真實客製才裁決per file
application-owned seam下游必須擁有並確認的應用政策upstream contract 改變時一律要求明確 resolutionper file
localSeams純下游永不取上游內容,只做契約與 build 驗證無上游基線
test support下游測試環境持有顯示 migration;可選 three-way mergeper file
reference domain下游接管的預設資源實作不自動覆蓋;依 Changelog opt-in 遷移per domain

core 可自動覆蓋的前提仍是 ADR-006 的中性不變量。若非 seam 的檔長期需要下游客製,出口仍只有 上收 capability/SPI、降級 reference domain,或在首次 adoption 時明確成為 localSeams;不得 把 three-way merge 擴張成所有 core 都能任意 fork。

每個 source catalog 宣告的 seam 都必須在 seamAdoption 逐 path 分類,key 集合與 seams 完全相同:

  • defaultable:框架提供可直接採用的 canonical 組裝。沒有 surface state 時,工具只在下游內容 精確命中目標 release 祖先中的某個 source blob 時推導可信 baseline 並自動快轉;無歷史命中仍 要人工 reconciliation,不能把未知客製當 drift。
  • application-owned:框架只提供組裝形狀,URL 授權、角色映射等政策必須由應用擁有。當 B 與 U 不同時,即使 three-way merge 無衝突、L 等於 B,也不能自動採 U;必須明確選擇採用、保留或合併。
  • localSeams:source 不分類;它是下游自行增加的選配擴充,沒有 upstream merge base。

application-owned 表示「必須確認」,不表示內容必須產生 diff。Feature 也不必勉強擁有全部類型; 純 mechanics Feature 可以只有 core,只有 canonical 預設的 Feature 也可以全部為 defaultable。 作者化時應把 application-owned 檔維持為薄的 composition/policy root,將可共用 mechanics 留在 core, 避免一個大 seam 每次都承擔不必要的人工合併。

config/feature-{id}.yml 也必須是同名 Feature 的 seam,不能只存在於 resources 與 import 清單而 未進 catalog。分類看的是「下游是否需要在 source file 層擁有決策」,不是「部署時是否會用外部 設定覆蓋值」:production-safe defaults 即使每個環境都會覆蓋,仍可是 defaultable;承載應用 安全政策、角色/授權或專案 URL 的 fragment 則為 application-owned。source inventory 必須檢查 每個 feature-{id}.yml 恰由同名 Feature 唯一擁有,避免升級時把漏登錄誤判成 downstream-only 檔。 downstream 在逐 Feature rolling migration 尚未取得新 catalog 宣告前不提前套用這道 source gate。

catalog invariant 分成 source 與 downstream 兩個強度:source 必須永遠完整分類;downstream 在 upgrade --all 的逐 Feature 遷移期間,尚未輪到的 legacy Feature 可暫時缺少整個 seamAdoption map,但 map 一旦存在就必須與 seams 完整對齊。這個 rolling-migration 例外只避免 第一筆交易被其他尚未升級的 Feature 阻斷,不會替缺少分類的 Feature 推定 adoption policy。

D-D:catalog 分開記錄 core 與下游持有 surface 的基線

下游 catalog 不再用單一 syncedSHA 表達整個 Feature。目標 schema 以使用者可讀 release ref 定位版本,並以 blob identity 驗證實際 merge base:

{
"features": {
"mail": {
"coreRef": "app-v2026.1.7",
"seams": [
"src/main/java/com/acme/app/feature/mail/MailConfig.java"
],
"seamAdoption": {
"src/main/java/com/acme/app/feature/mail/MailConfig.java": "defaultable"
},
"seamState": {
"src/main/java/com/acme/app/feature/mail/MailConfig.java": {
"sourcePath": "src/main/java/io/leandev/app/feature/mail/MailConfig.java",
"reconciledRef": "app-v2026.1.6",
"baseBlob": "<git blob id>",
"presence": "present"
}
},
"testSupportState": {
"src/test/java/com/acme/app/feature/mail/MailConfigTest.java": {
"sourcePath": "src/test/java/io/leandev/app/feature/mail/MailConfigTest.java",
"reconciledRef": "app-v2026.1.5",
"baseBlob": "<git blob id>"
}
},
"referenceDomainState": {
"mail-setting": {
"reviewedRef": "app-v2026.1.4"
}
}
}
}
}

欄位語意:

  • coreRef:core 上次完整通過升級交易的 source ref;取代 syncedSHA 的日常語彙。
  • reconciledRef:該下游持有檔已處理到哪個上游 ref。處理可為採用、人工合併,或明確保留 下游內容;它不宣稱檔案與上游相同。
  • baseBlobreconciledRef 上該 source path 的 blob,防止 ref/path 誤配並提供確切 merge base。
  • presence: absent:下游明確決定不建立某個新 seam;仍記錄已協調 ref,避免每次重報。
  • reviewedRef:整個 reference domain 已檢視到哪一版,不宣稱已同步或已採用。
  • seamAdoption:source-owned、隨 release/provision/upgrade 下行的每檔 adoption policy;不是下游 runtime state,也不得由下游把 application-owned 偷改成 defaultable 以繞過 review。

每個狀態各自推進。不得因 core 成功就替未處理的 seam、test support 或 reference domain 推進 基線;也不得用「跑過 upgrade」作為批次 ack。

D-E:每個 server source type 擁有 Feature Changelog

新增兩份互不混用的 Changelog:

  • app-tenant-server/FEATURE_CHANGELOG.md
  • app-tenantless-server/FEATURE_CHANGELOG.md

作者在 Feature 實作的同一 commit 寫入 ## [Unreleased];只要該 release 的 Feature surface 有變更,對應 source type 就必須有 entry。/release-app gate 通過後,才把非空區段原子轉為 app CalVer 標題並建立新的空 [Unreleased];本版無 Feature 變更的 source type 不製造空版本區段。 每個 entry 採以下結構:

## [Unreleased]

### mail

- `[seam][required]` Mail policy 接合新增 outbound safety 選項
- Paths: `src/main/java/io/leandev/app/feature/mail/MailConfig.java`
- Migration: 保留下游 sender 選擇,加入新的 safety policy bean
- Verify: `MailConfigTest` 與 application context 啟動成功
- Requires: `appfuse-server >= 4.0.0-alpha.14`

契約如下:

欄位規則
entry prefix必填 `[core
Paths必填;rename 同時列 old → new
Migrationseam、catalog、rename、delete 與 required entry 必填;純自動 core 可填 automatic
Verifyrequired entry 必填
Requires只有超出目標 source build 檔可直接推導的相容性限制時填寫

/release-app 以「現行 app tag → HEAD」的 source delta 驗證:變更到 Feature package、per-Feature catalog、test support 或 attached reference domain 時,對應 source type 的 [Unreleased] 至少須有 涵蓋該 Feature 且 surface 相符的 entry。top-level catalog doc、skeleton、structural domain 與 practices 不誤算為 Feature migration。Changelog 與程式碼同次提交;切版只把既有 [Unreleased] 封成 ## [{newVersion}] - YYYY-MM-DD 並建立不可變 tag,不事後補寫。

機制首次導入時,導入 commit 可一次性以 [Unreleased] 補齊現行 app tag 後已累積的完整 Feature delta;release gate 啟用後的新變更才嚴格要求實作與 entry 同次 commit。這是歷史 bootstrap,不能 沿用成日常「切版前再補 changelog」流程。

Feature Changelog 不是 patch source。工具以 tag 中的真實檔案產生 core replacement 與 three-way merge;Changelog 只提供人與工具無法從 diff 推導的意圖、impact、rename 與驗證方法。

現行 appfuse-server/CHANGELOG.md 維持只描述 framework JAR 公開 API;參考實作 Feature 變更 不得塞入該檔。若一個工作同時改 capability API 與對偶 Feature,兩份 Changelog 都要更新。

D-F:provision 與 upgrade 共用 per-Feature transaction engine

交易引擎負責共同的 preflight、immutable snapshot materialization、retarget、精確 write-set、 apply、validate、finalize 與 rollback;provisionupgrade 只在 planner 與 ownership resolution 不同。所有寫入仍以一個 Feature 為最小交易與 commit 單位。

對每個既有 Feature,升級依序執行:

  1. Preflight:確認下游 catalog、server type、目標 tag、乾淨 write-set,並讀取目前各 surface 基線。
  2. Compatibility:比較目標 source tag 的 framework dependency 與下游現況;不足時停止並 建議先執行 /upgrade-appfuse-server
  3. Changelog range:依目前各 surface ref 到目標 ref 分別列出相關 entry;不可只用 coreRef 裁切全部 surface。
  4. Core:從目標 tag materialize core,retarget 至下游 base package,覆蓋/補回 canonical 檔;extralocalSeams 不碰。
  5. Seam:每檔建立 B = reconciledRef 的 sourceL = 下游現況U = 目標 source;B/U 先 retarget,再 three-way merge。defaultable 可在歷史 source 命中時自動建立 B 並快轉; application-owned 的 B→U 變更即使可乾淨合併也必須明確裁決。新增、刪除與 rename 依 Changelog 與 adoption policy 處理。
  6. 下游持有 surface:依 impact 處理 test support 與 reference domain。required entry 未完成 時不得宣稱 Feature 已升級完成。
  7. 驗證:執行 Feature Changelog 指定驗證、feature-inventory.py --check./gradlew build
  8. Stamp:全部 required migration 與驗證成功後,才更新本次實際處理的 coreRef、各 reconciledRefreviewedRef;未處理者保持原值。
  9. Commit:一個 Feature 一個下游 commit,使升級可獨立審查與回退。

初次 provision 走同一生命週期,但 planner 必須先確認 Feature 不在下游 catalog、所有 requires 已存在,再把目標 snapshot 的 core、seam、test support 與 attached reference domain 一次列入 write-set。因為沒有既有下游版本,所有 source-owned 檔皆為 install,不做 three-way merge; finalize 直接建立 D-D 的完整 surface state。若下游已存在同路徑內容,視為 adoption conflict, 必須在 apply 前停止,不得靜默覆蓋。

使用者在 seam 衝突中選擇「保留下游版本」仍可把 reconciledRef 推進到目標 ref,因為那代表 已對該上游變更完成一次明確 resolution;若選擇「稍後處理」,則不推進,下一次 status/upgrade 仍須顯示該項。

工具不得以 whole-repo hard reset 回退。升級前記錄精確 write-set 與原狀;失敗時只還原本次 交易寫入的路徑,且不得覆蓋交易期間出現的並行使用者變更。

D-G:/upgrade-appfuse-server 不吸收 Feature migration

/upgrade-appfuse-server 的責任仍是:取得 framework CHANGELOG、升級 JAR dependency、預檢 公開 API 並 build。Feature 的版本座標、source snapshot 與 ownership 都在 app reference release, 與 JAR SemVer 不同,故不把兩者硬塞進同一支 skill。

未來可增加 workspace 編排器 /upgrade-app

/upgrade-app
1. 升級 appfuse-server / appfuse-web dependency
2. 依目標 app release 升級已安裝 Feature
3. 執行跨模組驗證

編排器只排序既有交易,不改變各自的版本 SoT 與 Changelog 邊界。

D-H:既有 syncedSHA 與 seam 的一次性遷移

既有下游採漸進式 bootstrap,不製造虛假的 release 基線:

  1. features[{id}].syncedSHA 先保留為合法的 sha:{SHA} coreRef;若它恰好對應 immutable app tag,才轉為 app-v{CalVer}。不得猜最近 tag 後直接宣稱已對齊。
  2. seam 若完整命中某個來源歷史 blob,以該 commit/tag 建立 reconciledRef + baseBlob
  3. customized seam 若無任何歷史命中,必須做一次人工 baseline reconciliation:選定可說明的 source base、呈現完整 diff、處理到某個目標 release,成功後才寫 reconciledRef
  4. 現有 localSeams 維持純下游語意,不建立上游 merge base。
  5. 在所有已安裝 Feature 完成 schema migration 前,status 同時讀 legacy 與新欄位;寫入新狀態 後才移除該 Feature 的 legacy syncedSHA

這個相容讀取只服務既有 catalog。新 provision 從第一天即寫新 schema,不經過 syncedSHA

D-I:upgrade --all 是可續跑的逐 Feature 編排,不是大型交易

/feature upgrade --all --to app-v{CalVer} 只選取下游 catalog 已 provision 的 Feature;目標 release 新增但下游未安裝者不在集合內,也不自動 provision。編排器依目標 snapshot 的 requires 做拓樸 排序,依賴先於使用者;若已安裝 Feature 在目標版新增尚未 provision 的必要依賴、依賴不存在或 形成 cycle,整批在寫入前失敗。

執行分為兩層:

  1. Batch plan:先為排序後的全部 Feature 產生 plan 並彙總 compatibility、Changelog、衝突與 驗證要求;所有 plan 成功前,不啟動任何 Feature 寫入。
  2. Per-Feature transaction:逐一執行 D-F 的 apply → validate → finalize → commit;每個 Feature 仍有自己的 transaction manifest、rollback 邊界與 commit。

批次 queue 只記錄目標 release、拓樸順序、completed commit 與第一個未完成 Feature。任一 Feature 失敗即停止;若已 apply,先只 rollback 當前 Feature,再留下可診斷的 stopped state。resume 必須 重新驗證相同 immutable target、已完成 commit/catalog state、剩餘 Feature plan 與乾淨 write-set, 再從第一個未完成項繼續。

不建立跨 Feature 的大型原子交易。 後續 Feature 失敗時,先前已 finalize 並 commit 的 Feature 維持完成;batch abort 也只終止 queue,不回退那些 commit。需要整批業務回復時,使用正常的反向 commit/release 流程,而不是讓工具對多個已驗證 commit 做隱性 hard reset。

後果

正面

  • 發布不再對下游造成非預期寫入,source 與 downstream release cadence 解耦。
  • core 仍維持零分歧治理,不因引入 merge 而退化成任意 fork。
  • seam 從「永遠顯示 diff」提升為有 base、有 migration intent、可驗證的升級交易。
  • seam 的 adoption intent 成為 catalog 事實,可自動處理 canonical default,又不替應用決定業務政策。
  • 下游明確保留客製後不會反覆收到相同 diff;未處理項也不會因 core stamp 前進而消失。
  • app tag、server type 與 framework dependency 都來自同一 immutable snapshot,可重現與稽核。
  • provision 與 upgrade 的意圖明確,但共享同一交易安全性;批次升級可先全覽並在失敗後續跑。

成本

  • catalog schema 增加 per-surface 狀態,release 與 upgrade skill 都需升級。
  • Feature 作者必須在變更當下維護結構化 Changelog。
  • Feature 作者必須逐 seam 宣告 defaultableapplication-owned,並保持 policy root 薄而明確。
  • three-way merge 只能降低機械成本,無法取代對 Spring wiring、資料 migration 與安全政策的人工 判斷。
  • 首次替既有 customized seam 建立可信 baseline 需要一次性人工成本。
  • upgrade --all 不是 all-or-nothing;部分完成是刻意且可見的狀態,操作端必須理解 resume/反向 commit 的邊界。

不採用的方案

方案不採用原因
維持來源端 broadcast sync把 publish 與下游 mutation 綁在一起;seam 仍沒有可推進的 merge base
只改善 two-way diff可讀性變好但無法記錄已協調狀態,下次仍重複同一問題
只靠 Changelog 套 migrationChangelog 不是可驗證 patch,也不知道下游實際客製內容
全部 surface 都 three-way mergecore 因此失去 canonical 不變量;reference domain 也被誤裝成受治理模板
把 Feature migration 塞進 /upgrade-appfuse-serverJAR SemVer 與 app CalVer、API ownership 與 reference source ownership 不同
只存 per-Feature seam ref部分檔已處理時會替其他 seam 製造假陰性;狀態粒度必須對齊可獨立裁決單元
provision 與 upgrade 合併成自動判斷命令present/absent 會改變授權與衝突語意;命令名稱應讓使用者明確表達 adoption 或 evolution
upgrade --all 建立跨 Feature 原子交易長時間交易放大 rollback 與並行修改風險,也抹掉可獨立驗證、提交與回退的 Feature 邊界

與其他文件的關係

文件關係
ADR-006保留 Feature partition、core 中性、seam ownership 與 provision;本 ADR 取代 D-D 的 broadcast sync 與「seam 只顯示 diff」終態
ADR-007保留既有下游 provision migrate、首次 drift triage 與 per-Feature transaction;本 ADR 取代 D-A′ 的主要寫入方向與後續 sync 狀態模型
ADR-011server type 仍是 source 與 upgrade 路由邊界;共同 app tag 不允許跨 type 套用
server ADR-014core 可自動覆蓋的中性前提與 capability/Feature 對偶
server ADR-026固定 adapter 留 core、預期由消費端重塑者退出 core 的 ownership 判準
m-app-release.md/release-app提供 app CalVer、immutable app-v* tag、[Unreleased] coverage gate 與版本區段 finalize
m-changelog-format.md/upgrade-appfuse-server僅管理 framework package API Changelog;與 Feature Changelog 分工、不混檔

實作順序

  1. 新增兩個 server source type 的 FEATURE_CHANGELOG.md/release-app 驗證契約。(已完成)
  2. 擴充 catalog reader,使 status 可同時讀 legacy syncedSHA 與新 surface state。(已完成)
  3. 實作 in-target /feature upgrade 的 core replacement、seam three-way merge 與交易回復。(已完成)
  4. 實作 release-based /feature provision <id> --from ...,與 upgrade 共用 transaction engine,直接 建立完整 surface state,不新增 syncedSHA(已完成)
  5. 實作 /feature upgrade --all --to ... 的 installed-only 拓樸 plan、逐 Feature queue、stop 與 resume;證明沒有跨 Feature rollback。(已完成)
  6. 以 provision、無客製 seam、customized seam 與中途失敗批次做 pilot,驗證初始 state、 「採用/keep-local/defer」、停止與續跑。
  7. 對既有下游逐 Feature bootstrap baseline。
  8. 將 source-side /feature sync 降級為相容提示,最終移除 broadcast write;保留唯讀 fleet status。