refactor(domain): 按主题重组 Domain/ 源码目录

Domain/ 根目录原有 62 个平铺源文件,无法从目录读出主题划分。

按「目录即主题」重组为 11 个目录(Rules/Custom 为嵌套子目录):Catalog /
Subscription / Rules / Runtime / Activation / Settings / SystemProxy /
Plugin / Credentials / LegacyImport / Diagnostics。纯移动:类型名、模块名
与 import 均未改动,Domain/ 根下不再有散落文件。目录口径取自
GLOSSARY.md 的章节划分(配置与订阅、代理规则、代理控制与网络、设置、
凭据与诊断),而非新造分类。

配套改动:

- project.yml:wrapper 与 helper 两个 target 按文件引用的 15 条显式路径
  随目录改名,并补注释固定这一约定——这 15 条指向不存在的文件时,
  xcodegen generate 以 "missing source directory" 直接失败(实测,不静默)。
- SkeletonTests 新增 DomainLayoutTests:断言 Domain/ 根下无平铺 .swift、
  树下只有 .swift。后者防的是与 App/ 同源的坑——Domain/ 整目录是 app 与
  Core 的 source path,任何非源码文件都会被 XcodeGen 收进 Resources 阶段
  并封进签名后的 bundle。
- README 工程结构说明同步;docs/design 与 docs/research 属历史文档不改。

验证:xcodegen generate 后 app 164 / Core 163 源文件数与整理前一致;Agent
恰好 9 个、Helper 恰好 6 个 Domain 文件(无缺失无多出);app 与 Core 各覆盖
全部 62 个。clean build 0 error;task test 1024 用例全通过(整理前 1022,
+2 为新增守卫);task format 未改写任何被移动文件(证明纯移动);task lint
与整理前同为 21 条内容型 warning;packaging-gate.sh ALL CHECKS PASSED。

布局守卫已做变异验证:临时投放松散 .swift 与非 .swift 文件时两个用例均
失败并点名违规文件,清理后复验为绿。
This commit is contained in:
Qiu Yuzhou
2026-10-05 20:13:35 +08:00
parent fb428f12bc
commit 856d6b4272
65 changed files with 97 additions and 18 deletions
+12 -1
View File
@@ -20,7 +20,18 @@ ShadowsocksX-NG 的现代化重写版本。新工程的全部源码与构建配
分层边界由 `Tests/SkeletonTests.swift` 的正向圈定守卫断言:只扫描 `App/UI/`,视图层不得引用运行时控制器、工作流实现协作者或原始目录/凭据存储类型;新增运行时适配器放进 `Application/` 即自动落在扫描范围外,无需登记豁免。注意 `App/` 下任何非源码文件都会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle,因此文档不放 `App/`(`project.yml` 已排除 `**/*.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 无关;其中 `RuntimePaths`、`SslocalRuntimeDocument`、`RuntimeLog` 同时编入 wrapper 目标,保证跨进程契约单一来源。
- `Domain/` — 领域核心(配置目录树、凭据引用与持久化,spec #21 D3/D5),与 UI 无关。目录即主题,根下不留平铺文件,子目录口径取自 `GLOSSARY.md` 的章节划分:
- `Catalog/` — 配置目录树、结构不变量错误、目录文件存储、节点与凭据身份,以及 `ss://` 编解码 `SsUri`。
- `Subscription/` — 订阅文档解析、抓取、订阅记录与刷新失败。
- `Rules/` — 代理模式、规则身份与匹配、内置规则快照、规则集合/浏览/分析、离线规则测试;`Rules/Custom/` 为用户自定义规则的编辑草稿、校验与存储。
- `Runtime/` — 代理运行服务:运行时文档与监听设置、ACL 与部署收据、路径、原子写入、文件存储、日志与事件、端点/端口/socket 探针。
- `Activation/` — 激活候选校验、激活状态机与 `RuntimeConfiguration`。
- `Settings/` — 用户设置的持久化:代理设置、监听设置、静默启动。
- `SystemProxy/` — 系统代理 typed 配置、property list 映射、计划器与特权 helper 的 XPC 契约/请求引擎。
- `Plugin/` — 托管插件、用户插件映射与插件安全校验。
- `Credentials/`、`LegacyImport/`、`Diagnostics/` — 凭据存储与写入日志、旧版导入、诊断报告。
跨进程契约按**文件**编入其它 target:`Runtime/` 的 9 个文件同时编入 wrapper(`RuntimePaths`、`SslocalRuntimeDocument`、`RuntimeLog` 等),`SystemProxy/` 全部 6 个文件编入特权 helper。`project.yml` 里这 15 条是显式路径,在 `Domain/` 内移动文件必须同步,漏改会让 `xcodegen generate` 以缺文件直接失败。布局守卫(`Tests/SkeletonTests.swift` 的 `DomainLayoutTests`)断言根下无平铺 `.swift`、树下只有 `.swift`(非源码文件会被 XcodeGen 收进 Resources 阶段并封进签名后的 bundle)。
- `Tests/` — 单元测试 target,随 `ShadowsocksX-NG2` scheme 运行。
- `Vendor/<name>/manifest.json` — 外部二进制的固定供应链清单(tag + 资产 URL + 归档 SHA-256 + bundle 内位置 + 签名 identifier);二进制本体与 `.fetched.sha256` 戳是构建缓存,不入库。
- `Vendor/rules/geolocation-cn/` — 内置中国域名规则快照(issue #63):`snapshot.json`(规范化规则 + 元数据 + 损失报告)、`manifest.json`(上游版本与快照 SHA-256)、`NOTICE`(许可证与归属)。普通构建只读本地快照,绝不抓取或转换。
@@ -104,3 +104,62 @@ final class CatalogWorkflowArchitectureTests: XCTestCase {
}
}
}
/// Domain 布局守卫:`Domain/` 是「目录即主题」——根下只放主题子目录,不放平铺
/// 文件,主题口径见 `project.yml` 的 Domain 注释与 GLOSSARY.md 的章节划分。
///
/// 无此守卫时,新增领域文件会默认堆回根下(整理前 62 个文件即如此),主题
/// 划分靠人工记忆维持;守卫把该约定固定为可执行的断言。
final class DomainLayoutTests: XCTestCase {
private static let domainDirectory = URL(fileURLWithPath: #filePath)
.deletingLastPathComponent().deletingLastPathComponent()
.appendingPathComponent("Domain")
func testDomainRootHasNoLooseSwiftFiles() throws {
let rootEntries = try FileManager.default.contentsOfDirectory(
at: Self.domainDirectory, includingPropertiesForKeys: nil)
let looseSwiftFiles =
rootEntries
.filter { $0.pathExtension == "swift" }
.map(\.lastPathComponent)
.sorted()
XCTAssertTrue(
looseSwiftFiles.isEmpty,
"""
Domain/ 根下不得平铺 Swift 文件,应按主题归入子目录(Catalog / Subscription /
Rules / Rules/Custom / Runtime / Activation / Settings / SystemProxy / Plugin /
Credentials / LegacyImport / Diagnostics):
\(looseSwiftFiles.joined(separator: "\n"))
""")
}
/// `Domain/` 整目录是 app 与 Core target 的 source path:任何非 Swift 文件都会被
/// XcodeGen 收进 Resources 阶段、封进签名后的 app bundle(同 App/ 的已知行为),
/// 因此树下不允许出现这类文件——确需随包发布应改走 Vendor/ 并显式声明 buildPhase。
func testDomainTreeContainsOnlySwiftFiles() throws {
let domainDirectory = Self.domainDirectory
let enumerator = try XCTUnwrap(
FileManager.default.enumerator(at: domainDirectory, includingPropertiesForKeys: nil),
"无法枚举 Domain 目录:\(domainDirectory.path)")
var unexpectedFiles: [String] = []
for case let url as URL in enumerator {
let isRegularFile =
try url.resourceValues(forKeys: [.isRegularFileKey])
.isRegularFile == true
guard isRegularFile else { continue }
if url.pathExtension != "swift" {
unexpectedFiles.append(
url.path.replacingOccurrences(of: domainDirectory.path + "/", with: ""))
}
}
XCTAssertTrue(
unexpectedFiles.isEmpty,
"""
Domain/ 树下只允许 .swift 文件,否则会被收进 app bundle 的 Resources 阶段:
\(unexpectedFiles.sorted().joined(separator: "\n"))
""")
}
}
+26 -17
View File
@@ -33,7 +33,14 @@ targets:
# 文档不属于 target 源码:App/ 下任何非源码文件都会被 XcodeGen 收进
# Resources 阶段并封进签名后的 app bundle(实测),因此显式排除。
- "**/*.md"
# 领域核心:配置目录树、凭据引用与持久化(spec #21 D3/D5)
# 领域核心(spec #21 D3/D5)。目录即主题,根下不留平铺文件,子目录按
# GLOSSARY.md 的章节口径命名:Catalog(配置目录树与身份)/
# Subscription(订阅)/ Rules(代理规则,自定义规则在 Rules/Custom)/
# Runtime(代理运行服务:运行时文档、路径、日志、端点与探针)/
# Activation(激活候选与状态机)/ Settings(用户设置持久化)/
# SystemProxy(系统代理 typed 配置与特权 helper 契约)/
# Plugin(托管插件与用户映射)/ Credentials / LegacyImport / Diagnostics。
# 该约定由 Tests/SkeletonTests.swift 的 Domain 布局守卫断言。
- path: Domain
# LaunchAgent 注册清单:SMAppService.agent(plistName:) 从 bundle 内
# Contents/Library/LaunchAgents/ 读取;ProgramArguments 用 bundle 相对路径
@@ -170,15 +177,17 @@ targets:
- path: Agent
# wrapper 与 GUI 共享的跨进程契约:路径常量、运行时文档(含读取侧校验)
# 与日志事件基线(spec #21 D5);监听建立判定复用端点探测(issue #38)。
- path: Domain/RuntimePaths.swift
- path: Domain/AtomicFileWriter.swift
- path: Domain/ProxyACL.swift
- path: Domain/ProxyRule.swift
- path: Domain/SslocalRuntimeDocument.swift
- path: Domain/SslocalListenSettings.swift
- path: Domain/RuntimeLog.swift
- path: Domain/EndpointHealthProbe.swift
- path: Domain/RuntimeSocketOwnershipProbe.swift
# 注意:以下 9 条是显式文件路径,Domain/ 内移动文件必须同步此处,否则
# xcodegen 以 "missing source directory" 使生成失败(实测,不会静默)。
- path: Domain/Runtime/RuntimePaths.swift
- path: Domain/Runtime/AtomicFileWriter.swift
- path: Domain/Runtime/ProxyACL.swift
- path: Domain/Rules/ProxyRule.swift
- path: Domain/Runtime/SslocalRuntimeDocument.swift
- path: Domain/Runtime/SslocalListenSettings.swift
- path: Domain/Runtime/RuntimeLog.swift
- path: Domain/Runtime/EndpointHealthProbe.swift
- path: Domain/Runtime/RuntimeSocketOwnershipProbe.swift
settings:
base:
PRODUCT_NAME: ShadowsocksX-NG2Agent
@@ -191,13 +200,13 @@ targets:
- path: SystemProxyHelper
# helper 与 GUI 共享的 typed 契约与纯逻辑:closed typed 配置、property
# list 映射、计划器、XPC 协议与请求引擎(issue #71)。helper 不含产品
# 策略:不在这些文件之外做任何域判断。
- path: Domain/SystemProxyConfiguration.swift
- path: Domain/SystemProxyPlan.swift
- path: Domain/SystemProxyPropertyList.swift
- path: Domain/SystemProxyHelperContract.swift
- path: Domain/SystemProxyHelperEngine.swift
- path: Domain/SystemProxyHelperListenerDelegate.swift
# 策略:不在这些文件之外做任何域判断。以下 6 条同为显式路径,移动需同步。
- path: Domain/SystemProxy/SystemProxyConfiguration.swift
- path: Domain/SystemProxy/SystemProxyPlan.swift
- path: Domain/SystemProxy/SystemProxyPropertyList.swift
- path: Domain/SystemProxy/SystemProxyHelperContract.swift
- path: Domain/SystemProxy/SystemProxyHelperEngine.swift
- path: Domain/SystemProxy/SystemProxyHelperListenerDelegate.swift
settings:
base:
PRODUCT_NAME: ShadowsocksX-NG2SystemProxyHelper