docs(adr): 记录源码目录即分层与主题及守卫约定

App/ 按分层、Domain/ 按主题组织,两个根层只放子目录、不放平铺文件;边界由
Tests/SkeletonTests.swift 的守卫正面圈定断言,而不是禁令加豁免登记。

ADR-0029 只记录组织原则本身:分层与主题划分的依据(Domain 主题名取自
GLOSSARY.md 的章节划分)、四条被否决的备选(平铺加豁免登记、feature-first、
Domain 照搬分层、守卫锁定目录集合),以及两条在代码里不可见的 XcodeGen
约束和跨进程契约按文件引用的代价。

ADR 不维护目录与文件清单——清单由 ShadowsocksX-NG2/README.md 的工程结构
一节维护,避免记录随事实漂移。

project.yml 的两处分层注释与 README 的工程结构一节各加一处引用:前者按该
文件既有写法引用编号,后者用完整链接。
This commit is contained in:
Qiu Yuzhou
2026-10-05 20:27:11 +08:00
parent 67d0afcda1
commit a31e2fa4bf
3 changed files with 32 additions and 3 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ ShadowsocksX-NG 的现代化重写版本。新工程的全部源码与构建配
- `PlatformEffects/` — 平台效应 seam(协议 + InMemory 替身 + AppKit adapter),系统框架访问集中于此。
- `Resources/` — `Info.plist`、`entitlements`(生成物)与 `Assets.xcassets`、`*.xcstrings`(入库)。
分层边界由 `Tests/SkeletonTests.swift` 的正向圈定守卫断言:只扫描 `App/UI/`,视图层不得引用运行时控制器、工作流实现协作者或原始目录/凭据存储类型;新增运行时适配器放进 `Application/` 即自动落在扫描范围外,无需登记豁免。注意 `App/` 下任何非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle,因此文档不放 `App/`(`project.yml` 已排除 `**/*.md`)。
分层边界由 `Tests/SkeletonTests.swift` 的正向圈定守卫断言:只扫描 `App/UI/`,视图层不得引用运行时控制器、工作流实现协作者或原始目录/凭据存储类型;新增运行时适配器放进 `Application/` 即自动落在扫描范围外,无需登记豁免。注意 `App/` 下任何非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle,因此文档不放 `App/`(`project.yml` 已排除 `**/*.md`)。分层与主题的组织方式、守卫为何取正面圈定、以及 XcodeGen 的上述约束见 [ADR-0029](../docs/adr/0029-source-layout-declares-layer-and-theme.md)。
- `Agent/` — 代理运行时 wrapper(独立可执行文件,装入 `Contents/MacOS/`,由 LaunchAgent 常驻):读 `sslocal-active.json` 契约,以绝对路径启动和监管 sslocal(SIGTERM 链式停止、SIGUSR1 热重载/结构重启、崩溃时非零退出交 KeepAlive 重放,spec #21 D2/D5/D7)。PAC HTTP endpoint 已随 issue #67 移除。
- `LaunchAgent/` — `SMAppService.agent(plistName:)` 的注册清单,装入 `Contents/Library/LaunchAgents/`;`ProgramArguments` 用 bundle 相对路径,由 launchd 按注册 app 的 bundle 位置解析。
- `Domain/` — 领域核心(配置目录树、凭据引用与持久化,spec #21 D3/D5),与 UI 无关。目录即主题,根下不留平铺文件,子目录口径取自 `GLOSSARY.md` 的章节划分:
+4 -2
View File
@@ -27,7 +27,8 @@ targets:
# 目录即分层:Composition(组合根)/ Application(工作流与运行时适配)/
# Presentation(跨功能表现层)/ UI(视图,按子视图分目录)/
# PlatformEffects(平台效应 seam)/ Resources(plist、entitlements、资源)。
# 分层边界由 Tests/SkeletonTests.swift 的 UI 圈定守卫断言。
# 分层边界由 Tests/SkeletonTests.swift 的 UI 圈定守卫断言;
# 取舍与约束见 ADR 0029。
- path: App
excludes:
# 文档不属于 target 源码:App/ 下任何非源码文件都会被 XcodeGen 收进
@@ -40,7 +41,8 @@ targets:
# Activation(激活候选与状态机)/ Settings(用户设置持久化)/
# SystemProxy(系统代理 typed 配置与特权 helper 契约)/
# Plugin(托管插件与用户映射)/ Credentials / LegacyImport / Diagnostics。
# 该约定由 Tests/SkeletonTests.swift 的 Domain 布局守卫断言。
# 该约定由 Tests/SkeletonTests.swift 的 Domain 布局守卫断言,
# 取舍与约束见 ADR 0029。
- path: Domain
# LaunchAgent 注册清单:SMAppService.agent(plistName:) 从 bundle 内
# Contents/Library/LaunchAgents/ 读取;ProgramArguments 用 bundle 相对路径
@@ -0,0 +1,27 @@
# 源码目录即分层与主题,边界由守卫断言
Status: Accepted (2026-10-05)
`App/` 按分层组织(Composition / Application / Presentation / UI / PlatformEffects / Resources),`Domain/` 按主题组织,主题名取自 `GLOSSARY.md` 的章节划分(配置与订阅、代理规则、代理控制与网络、设置、凭据与诊断),避免同一概念在文档与目录里有两个名字。两个目录的根层只放子目录、不放平铺文件——目录本身就是架构声明。
边界由 `Tests/SkeletonTests.swift` 的守卫断言,且一律**正面圈定**而非禁令加豁免登记:视图层的守卫只扫 `App/UI/`(不得引用运行时控制器、工作流实现协作者、原始目录/凭据存储类型),`Domain/` 的守卫只断言根下无平铺 `.swift`、树下只有 `.swift`。把文件放进 `Application/`、`PlatformEffects/` 或某个主题目录,即自动落在扫描范围之外,无需登记豁免;新增主题目录也不需要改断言。
各目录的划分与各自职责由 `ShadowsocksX-NG2/README.md` 的工程结构一节维护——本 ADR 只固定组织原则,不维护清单。
## Considered options
- **平铺 + 禁令表 + 豁免登记**:守卫默认扫描整个目录,再逐个登记要放行的例外。每加一个运行时适配器都要改守卫,豁免清单本身成了需要维护的知识,而「这个文件属于哪一层/哪个主题」仍然无处声明。
- **feature-first(按功能纵向切分)**:把某个功能的领域类型、工作流与视图放进同一个目录。与既有分层冲突——工作流依赖的运行时控制器与目录存储会变成视图的同层邻居,边界退化成不可断言的约定。
- **`Domain/` 也照搬分层**:领域内部不存在 `App/` 那样的层间依赖方向(没有「领域视图」),照搬只会得到一个叫 Services 的杂物箱。
- **让守卫锁定主题目录集合**:即正向列举「允许的目录」。未采纳——断言应当固定「不留平铺文件」这条不变量,而不是把主题划分冻结成一份需要同步维护的清单;新增主题目录是正常演进。
## Consequences
XcodeGen 带来两条**在代码里不可见**的约束:
- 源路径的 `excludes` glob **不跨 `/`**:裸文件名只匹配被扫描目录的根层。Core 测试镜像因此必须用 `**/MainApp.swift` 这类递归形式;文件下移后旧写法会**静默**失效,把 `@main` 组合根编进测试镜像。
- `App/`、`Domain/` 整目录是 source path,其中任何非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 app bundle。因此文档不放 `App/`(`project.yml` 排除 `**/*.md`),`Domain/` 则由守卫禁止出现非 `.swift` 文件。
跨进程契约按**文件**引用而非按目录:wrapper 与 system proxy helper 两个 target 各自显式列出所需的 `Domain/` 文件,因为它们只该拿到跨进程契约,而不是整个领域层。代价是目录内移动文件必须同步 `project.yml`;漏改时 xcodegen 以 `missing source directory` 直接失败,不静默。
在目录内移动文件因此是低代价操作:模块名、类型名与 import 均不变,`task format` 与 lint 基线不变,版本历史可继续追溯。这条约定分散记录在 `project.yml` 的分层注释、`ShadowsocksX-NG2/README.md` 的工程结构一节与本 ADR;新增主题目录时只需同步前两处(`DomainLayoutTests` 失败信息里列举的目录名会过期,断言本身不会失败)。