From a31e2fa4bf0304f700b59e7c27342a53db2e1b9a Mon Sep 17 00:00:00 2001 From: Qiu Yuzhou Date: Mon, 5 Oct 2026 20:27:11 +0800 Subject: [PATCH] =?UTF-8?q?docs(adr):=20=E8=AE=B0=E5=BD=95=E6=BA=90?= =?UTF-8?q?=E7=A0=81=E7=9B=AE=E5=BD=95=E5=8D=B3=E5=88=86=E5=B1=82=E4=B8=8E?= =?UTF-8?q?=E4=B8=BB=E9=A2=98=E5=8F=8A=E5=AE=88=E5=8D=AB=E7=BA=A6=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App/ 按分层、Domain/ 按主题组织,两个根层只放子目录、不放平铺文件;边界由 Tests/SkeletonTests.swift 的守卫正面圈定断言,而不是禁令加豁免登记。 ADR-0029 只记录组织原则本身:分层与主题划分的依据(Domain 主题名取自 GLOSSARY.md 的章节划分)、四条被否决的备选(平铺加豁免登记、feature-first、 Domain 照搬分层、守卫锁定目录集合),以及两条在代码里不可见的 XcodeGen 约束和跨进程契约按文件引用的代价。 ADR 不维护目录与文件清单——清单由 ShadowsocksX-NG2/README.md 的工程结构 一节维护,避免记录随事实漂移。 project.yml 的两处分层注释与 README 的工程结构一节各加一处引用:前者按该 文件既有写法引用编号,后者用完整链接。 --- ShadowsocksX-NG2/README.md | 2 +- ShadowsocksX-NG2/project.yml | 6 +++-- ...-source-layout-declares-layer-and-theme.md | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+), 3 deletions(-) create mode 100644 docs/adr/0029-source-layout-declares-layer-and-theme.md diff --git a/ShadowsocksX-NG2/README.md b/ShadowsocksX-NG2/README.md index d29d48cd..71151c2f 100644 --- a/ShadowsocksX-NG2/README.md +++ b/ShadowsocksX-NG2/README.md @@ -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` 的章节划分: diff --git a/ShadowsocksX-NG2/project.yml b/ShadowsocksX-NG2/project.yml index b894574e..e3d833f4 100644 --- a/ShadowsocksX-NG2/project.yml +++ b/ShadowsocksX-NG2/project.yml @@ -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 相对路径 diff --git a/docs/adr/0029-source-layout-declares-layer-and-theme.md b/docs/adr/0029-source-layout-declares-layer-and-theme.md new file mode 100644 index 00000000..1b7dd6ef --- /dev/null +++ b/docs/adr/0029-source-layout-declares-layer-and-theme.md @@ -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` 失败信息里列举的目录名会过期,断言本身不会失败)。