ADR-012: Feature 採版本化發布與下游主動升級
ADR 編號: 012 狀態: 已接受 (Accepted) 決策日期: 2026-08-18 決策者: AppFuse Team + AI Assistant 修訂: ADR-006 D-D、ADR-007 D-A′ 的同步方向與狀態模型 範圍:
app-*-serverFeature 的發布、下游升級、Changelog 與 catalog 基線
摘要
Feature 不再以「來源端廣播 sync、直接改寫 fleet」作為主要更新方式,改採與框架套件相同的
責任分離:
- 參考實作以
/release-app發布不可變的app-v{CalVer}快照與 Feature Changelog; - 下游以
/feature provision <id> --from app-v{CalVer}從不可變快照初次採用 Feature; - 下游在自己的版本、工作區與驗證脈絡中執行
/feature upgrade; - canonical core 依目標快照自動收斂;
- seam 以上次已協調的上游內容為 base,對下游現況與目標快照做 three-way merge;
- 來源端
/feature status保留為唯讀 fleet dashboard,不再把「發布」等同於「修改所有下游」。
Changelog 負責說明變更意圖、影響與 migration;不可變 tag 中的 source delta 才是實際 patch; catalog 的 per-Feature/per-seam 基線則負責區分尚未升級、已協調的客製與新的上游變更。三者缺一, 工具仍只能反覆顯示無法判讀的 two-way diff。
背景
ADR-006 與
ADR-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 | 否 |
| publish | workspace source | /release-app 驗證後建立不可變 app-v{CalVer} | 否 |
| observe | source 或 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 廣播直接修改 下游。下游的寫入必須在下游工作區發起,使用下游自己的測試、交付節奏與審查流程。
provision 與 upgrade 保持不同命令與不同意圖:前者建立下游原本不存在的 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 使用不同升級語意
| Surface | Ownership | 升級行為 | 狀態粒度 |
|---|---|---|---|
| core | source canonical | 由目標快照覆蓋/補回並 retarget | per Feature |
| defaultable seam | 下游可採 canonical 預設 | 可證明 L 命中歷史 canonical 時自動快轉;真實客製才裁決 | per file |
| application-owned seam | 下游必須擁有並確認的應用政策 | upstream contract 改變時一律要求明確 resolution | per file |
localSeams | 純下游 | 永不取上游內容,只做契約與 build 驗證 | 無上游基線 |
| test support | 下游測試環境持有 | 顯示 migration;可選 three-way merge | per 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。處理可為採用、人工合併,或明確保留 下游內容;它不宣稱檔案與上游相同。baseBlob:reconciledRef上該 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.mdapp-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 |
Migration | seam、catalog、rename、delete 與 required entry 必填;純自動 core 可填 automatic |
Verify | required 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;provision 與 upgrade 只在 planner 與 ownership resolution
不同。所有寫入仍以一個 Feature 為最小交易與 commit 單位。
對每個既有 Feature,升級依序執行:
- Preflight:確認下游 catalog、server type、目標 tag、乾淨 write-set,並讀取目前各 surface 基線。
- Compatibility:比較目標 source tag 的 framework dependency 與下游現況;不足時停止並
建議先執行
/upgrade-appfuse-server。 - Changelog range:依目前各 surface ref 到目標 ref 分別列出相關 entry;不可只用
coreRef裁切全部 surface。 - Core:從目標 tag materialize core,retarget 至下游 base package,覆蓋/補回 canonical
檔;
extra與localSeams不碰。 - Seam:每檔建立
B = reconciledRef 的 source、L = 下游現況、U = 目標 source;B/U 先 retarget,再 three-way merge。defaultable可在歷史 source 命中時自動建立 B 並快轉;application-owned的 B→U 變更即使可乾淨合併也必須明確裁決。新增、刪除與 rename 依 Changelog 與 adoption policy 處理。 - 下游持有 surface:依 impact 處理 test support 與 reference domain。required entry 未完成 時不得宣稱 Feature 已升級完成。
- 驗證:執行 Feature Changelog 指定驗證、
feature-inventory.py --check與./gradlew build。 - Stamp:全部 required migration 與驗證成功後,才更新本次實際處理的
coreRef、各reconciledRef/reviewedRef;未處理者保持原值。 - 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 基線:
features[{id}].syncedSHA先保留為合法的sha:{SHA}coreRef;若它恰好對應 immutable app tag,才轉為app-v{CalVer}。不得猜最近 tag 後直接宣稱已對齊。- seam 若完整命中某個來源歷史 blob,以該 commit/tag 建立
reconciledRef + baseBlob。 - customized seam 若無任何歷史命中,必須做一次人工 baseline reconciliation:選定可說明的
source base、呈現完整 diff、處理到某個目標 release,成功後才寫
reconciledRef。 - 現有
localSeams維持純下游語意,不建立上游 merge base。 - 在所有已安裝 Feature 完成 schema migration 前,
status同時讀 legacy 與新欄位;寫入新狀態 後才移除該 Feature 的 legacysyncedSHA。
這個相容讀取只服務既有 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,整批在寫入前失敗。
執行分為兩層:
- Batch plan:先為排序後的全部 Feature 產生 plan 並彙總 compatibility、Changelog、衝突與 驗證要求;所有 plan 成功前,不啟動任何 Feature 寫入。
- 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 宣告
defaultable或application-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 套 migration | Changelog 不是可驗證 patch,也不知道下游實際客製內容 |
| 全部 surface 都 three-way merge | core 因此失去 canonical 不變量;reference domain 也被誤裝成受治理模板 |
把 Feature migration 塞進 /upgrade-appfuse-server | JAR 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-011 | server type 仍是 source 與 upgrade 路由邊界;共同 app tag 不允許跨 type 套用 |
| server ADR-014 | core 可自動覆蓋的中性前提與 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 分工、不混檔 |
實作順序
新增兩個 server source type 的(已完成)FEATURE_CHANGELOG.md與/release-app驗證契約。擴充 catalog reader,使(已完成)status可同時讀 legacysyncedSHA與新 surface state。實作 in-target(已完成)/feature upgrade的 core replacement、seam three-way merge 與交易回復。實作 release-based(已完成)/feature provision <id> --from ...,與 upgrade 共用 transaction engine,直接 建立完整 surface state,不新增syncedSHA。實作(已完成)/feature upgrade --all --to ...的 installed-only 拓樸 plan、逐 Feature queue、stop 與 resume;證明沒有跨 Feature rollback。- 以 provision、無客製 seam、customized seam 與中途失敗批次做 pilot,驗證初始 state、 「採用/keep-local/defer」、停止與續跑。
- 對既有下游逐 Feature bootstrap baseline。
- 將 source-side
/feature sync降級為相容提示,最終移除 broadcast write;保留唯讀 fleet status。