跳至主要内容

ADR-010: feature slice 收納於 {basePkg}.feature.* 命名空間

ADR 編號: 010 狀態: 已接受 (Accepted) 決策日期: 2026-07-20 決策者: AppFuse Team + AI Assistant 範圍: docs-methodology(feature slice 的 package 落點;/feature/scaffold-module 與 partition 推導的路徑基準)


摘要

ADR-006 D-C 把 feature slice 落為 {basePkg} 下的頂層 package,與消費端自己的業務域、骨架平起平坐。實務暴露兩個成本:root namespace 被殖民(下游裝 N 個 feature,root 就多 N 個不是自己寫的 package,「誰的碼」要查 catalog 才知道);佈局選擇被隱性綁架(框架的 package-by-feature 長在 root,視覺上成為「本專案的佈局慣例」,消費端對自己業務碼的佈局自由名存實亡)。

「佈局的兩種所有權」(17-us-development.md,2026-07-20 自 ict-server reflow)已用文字劃界:{feature}/ 由框架決定、{domain}/ 由專案決定,框架 provision 過的檔放在哪不構成本地碼該放在哪的論據。本 ADR 把這條線做成結構

全部 feature slice 遷入 {basePkg}.feature.*feature 子樹之下=框架決定,之外=專案決定。

骨架與業務域不搬。root package 不變,Spring 掃描、config fragment、框架 jar 零觸碰。時機拍在現在:已 provision 的下游只有一個(ict-server,剛完成重 baseline、diff 最乾淨)、尚未進正式環境;almanac/tts 尚未 provision,重構先行則它們日後直接落在新佈局、零遷移。


背景 (Context)

問題陳述:圍籬只存在於文字

ADR-006 遷移後的 app-server(與 provision 後的下游)root package 長這樣:

{basePkg}/
├── acting/ almanac/ audit/ auth/ cache/ calendar/ delegation/
│ file/ impersonation/ mail/ notification/ notificationtenantoverride/
│ platforminfo/ referencedata/ scheduling/ serviceaccount/ signedlink/ tenant/
│ ← 18 個 feature(框架決定、sync 覆蓋)
├── customer/ order/ sales/ dashboard/
│ ← 業務域(專案決定;參考實作為 @reference-surface)
└── config/ entity/ exception/ handler/ initializer/ listener/ mapper/
← 骨架(框架種下、專案延伸)

三種所有權完全不同的 package 平鋪在同一層。後果:

  1. root 殖民:下游的 root 被 18 個框架 package 佔據,自己的業務域淹沒其中。哪些檔會被 /feature sync 覆蓋、哪些是自己的,結構上不可見——只能查 features.json
  2. 佈局綁架:頂層 feature package 群使 package-by-feature 看起來像「專案的慣例」。「佈局的兩種所有權」節已澄清那只是框架自己的佈局決定,但文字紀律敵不過視覺慣性——下游想以 layer-first 排自己的業務碼,會顯得與「全專案」格格不入。
  3. 混合 package 噪音:本地碼塞進 mail/(看起來像鄰居)的門檻遠低於塞進一個明顯是框架領地的子樹;每個混合 package 都在 /feature provisionstatus 產生一筆要人裁決的報告噪音(見「佈局的兩種所有權」的「唯一的成本」)。

為什麼是現在(機會窗口)

  • fleet 中已 provision 的 server 下游只有 ict-server 一個,且剛完成重 baseline(localSeams 只剩真客製、全 feature syncedSHA 對齊、I3 PASSED、build 綠)——遷移 diff 處於最乾淨狀態。
  • ict 尚未進正式環境;上線後遷移須背部署風險,現在只需一輪完整測試。
  • almanac-server、tts-server 尚未 provision(仍 layer-first)。順序若反過來(先 provision 再重構),它們要各付一次遷移;重構先行則 provision 時直接落在新佈局。

決策 (Decision)

D-A:feature slice 遷入 {basePkg}.feature.{featurePkg}

全部 feature(含 tenantnotification-tenant-override 等微 feature)自 root 遷入 feature 子樹:

{basePkg}/
├── feature/ ← 框架決定的一切 feature slice(/feature sync 的作用面)
│ ├── auth/ cache/ mail/ file/ notification/ …
├── {domain}/ ← 業務域:專案決定(參考實作為 @reference-surface)
└── config/ entity/ … ← 骨架:不搬(見 D-C)
  • package 名用單數 feature:與既有骨架的單數類別 package(configentity)一致;{basePkg}.feature.mail 讀作「feature: mail」。
  • feature id 中的連字號照現行慣例壓平為 package 名(notification-tenant-overridenotificationtenantoverride)。
  • 無命名衝突:不存在名為 feature 的 feature id 或業務域。

D-B:骨架與業務域不搬

處置理由
骨架config/entity/base/exception/…)留 root業務 entity 繼承 entity/base 的 audit 基類、骨架是專案會延伸的框架(config/TimeConfig 等)——搬動的侵入性大得多;且 root 留幾個 Spring 慣例 package 不是本 ADR 要解的痛(殖民感來自 18 個 feature,不來自 config/
業務域{domain}/留 root它們就是專案的碼(參考實作的為 @reference-surface、宿命是 retarget/prune);root 屬於專案,業務域住 root 正是 D-A 要成全的事

D-C:root package 不變,接線零觸碰

@ComponentScan@EntityScan@EnableJpaRepositories 一律指根 package(ADR-006「不要把 package 名寫進字串」的既有紀律),掃描自然涵蓋 feature 子樹——Spring 接線零修改。config fragment(config/feature-{id}.yml)是 resources、與 package 無關。框架 jar(io.leandev.appfuse.*)零觸碰。

D-D:為何在 {basePkg} 之下、而非框架 namespace

曾考慮 io.leandev.appfuse.feature.*(框架 namespace)——否決:provision 的碼是應用擁有的(隨 scaffold retarget 到下游 basePkg;seams 由專案修改、@ConfigurationProperties 與掃描根都錨定 app package)。放框架 namespace 會製造「看似 jar、實為源碼」的身分謊言,且 retarget 機制(P3)以 basePkg 為替換錨點、不容第二個根。{basePkg}.feature.* 誠實表達「這是你的碼、但佈局由框架管理」。

D-E:partition 推導隨之簡化(單一實作)

「業務=殘餘」(ADR-006/FU-52)的推導從「頂層 package 不在 features 也不在 skeleton」變成:

feature/ 子樹=feature slice;skeleton 宣告=骨架;root 其餘頂層 package=業務

殘餘空間更小、誤判面更小。實作仍收斂於 feature-inventory.py 單點(/scaffold-module 呼叫它、不重推導);variant-assembly.py 的組裝樹、排除集、per-variant I3 同步改認新根。variants/ overlay 樹鏡像 src 路徑、一併搬遷。

D-F:下游 catalog 的 seam 路徑連動改寫

下游 features.jsonseamslocalSeams 內嵌完整檔案路徑(如 src/main/java/{basePkgPath}/mail/MailConfig.java)——遷移時一併機械改寫為 …/{basePkgPath}/feature/mail/…,並以 I3 校驗收口。此為 ict 遷移步驟的一部分,不留半新半舊。


考量的方案 (Alternatives)

方案說明為何不採
維持現狀(ADR-006 D-C 原樣)feature 續留頂層三個成本(殖民、佈局綁架、混合 package 噪音)是慢性的,且隨 fleet 增長單調惡化;機會窗口(單一 provisioned 下游、未上線)一旦關閉,遷移成本跳階
features 複數{basePkg}.features.*純命名之別;單數與既有 configentity 等類別 package 風格一致,採單數
框架 namespaceio.leandev.appfuse.feature.*見 D-D:身分謊言 + retarget 錨點衝突
骨架一併搬(如 {basePkg}.framework.* 收骨架+feature)更徹底的圍籬侵入性跳階(業務 entity 繼承鏈、@ConfigurationProperties 前綴慣例、下游既有 import 面全動),而骨架不是痛源;不划算

後果 (Consequences)

正面

  • 所有權圍籬結構化feature/ 之下=框架管理(sync 覆蓋)、之外=專案的——「佈局的兩種所有權」從文字紀律升級為目錄事實。
  • 消費端佈局自由兌現:root 屬於專案;業務碼採 package-by-feature 或 layer-first 純屬專案選擇,不再有視覺壓力。
  • 混合 package 噪音萎縮:本地碼誤入框架領地的機率隨結構圍籬下降。
  • partition 推導更硬:殘餘空間縮小(D-E)。

負面 / 成本

  • 一次性機械 churn:框架 app-server ~18 個 package 的 git mv + 全量 import 改寫(含業務域對 feature 的接線、測試)、variants/ 樹搬遷、兩支工具與相關 rules/skills 更新;ict-server 同型遷移 + catalog 路徑改寫 + 完整測試。全部編譯器可見、無靜默失配面(字串編碼路徑除外——遷移時依 P3 紀律 grep 收口)。
  • 歷史文檔的路徑示例過時:既有 ADR/FU 記錄中的舊路徑({basePkg}/mail/…)成為歷史敘事,不回改(ADR 是決策當時的記錄)。
  • package 路徑加深一層:純外觀成本。

中性

  • 未 provision 的下游(almanac/tts 與所有僅前端/docs 的專案)零影響——provision 時直接落新佈局。
  • 框架 jar、config fragments、@ComponentScan 接線不變(D-C)。

不採用是安全的

維持現狀不產生新破壞——成本是慢性的殖民與噪音,與 fleet 規模同步累積。但機會窗口不可回復:一旦 almanac/tts provision 或 ict 上線,同一決策的執行成本跳階。


落地順序

順序即窗口紀律:在 almanac/tts provision 之前、ict 上線之前完成。

#步驟產出 / 驗收
0接受本 ADR2026-07-20 拍板feature 單數、骨架不搬
1框架端遷移2026-07-20 完成(225e5dc9,250 檔)32 目錄 git mv(main 18+test 12+variants 2)、246 檔 FQN/路徑改寫(長名優先+\b 邊界)、catalog seam 路徑(D-F);app-server 全量測試綠、I3、variantCheck、完整 I4(ownership 217 tests)全綠。工具(feature-inventory.pyFEATURE_NS+skeleton 吞併守衛、variant-assembly.pyunit_of 排除比對)與 FeatureSliceArchitectureTest(I1/I2 pattern)於本步一併 namespace-aware 化
2方法論規範面更新2026-07-20 完成(fa8cc5d0/feature(P3 migrate 目標)、/scaffold-module(businessDepended 掃描、裁剪路徑、stamp 表)、m-reference-code(佈局 A partition)、m-reference-prune(域識別)、17-us-development(目錄樹+所有權表);殘留掃描零命中
3ict-server 遷移2026-07-20 完成(ict f736c2a2,已 push)11 目錄 git mv、169 檔改寫、catalog seams/localSeams 路徑改寫+全 10 feature syncedSHA 推進 fa8cc5d0(框架 src delta 經驗證純為 namespace 遷移);I3 PASSED、ict 全量 build 綠。字串引用僅根 package、不受影響
4傳播2026-07-20 完成/methodology publish/practices publish 全 fleet 對齊 fa8cc5d0(6 workspace/20 模組;server type 下行規範與工具、其餘 type 純 stamp;local 面斷言通過)
5(之後)almanac/tts provision 直接落新佈局無額外工作,僅註記

與其他 ADR 的關係

ADR關係
ADR-006修訂其 D-C 的落點:package-by-feature 的切片原則不變,feature slice 的居所由 root 頂層改為 feature/ 子樹;「不要把 package 名寫進字串」紀律原樣沿用且是本遷移的安全前提
ADR-007adopt/provision 的目標佈局隨本 ADR 更新;drift 三分類、逐 feature 增量、每步 build 綠的紀律不變
ADR-009overlay 樹鏡像 src 路徑、隨遷移搬入 feature/;I4 是本遷移的驗收 harness 之一
ADR-004無交集(本 ADR 不動隔離語意),僅 tenant feature package 隨遷

拍板紀錄(2026-07-20)

接受 D-A ~ D-F。 命名定案=單數 feature;骨架與業務域不搬(D-B 原樣)。維護者於審閱時要求對 D-D 展開說明(namespace=擁有權、目錄圍籬=治理權的兩軸區分;retarget 單錨點、split package/package-private 洩漏、掃描第二根三個機制性理由),說明已納入本文 D-D 節的精神、無修訂。拍板當時的 fleet 事實:唯一 provisioned 下游 ict-server 處於乾淨 baseline(localSeams 僅真客製、I3 PASSED)、未上線;almanac/tts 未 provision。