From 56bc52756b2dd252657efc56e988af44c0db627d Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 2 Nov 2026 11:10:50 +0800 Subject: [PATCH 1/3] =?UTF-8?q?ci(guards):=20=E6=96=B0=E5=A2=9E=20presente?= =?UTF-8?q?r=20=E9=9B=B6=20UI=20=E4=B8=8E=20design=20=E9=9B=B6=E7=AC=AC?= =?UTF-8?q?=E4=B8=89=E6=96=B9=E4=BE=9D=E8=B5=96=E4=B8=A4=E9=81=93=E9=97=A8?= =?UTF-8?q?=E7=A6=81=EF=BC=88M89=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - presenter-no-ui:照 core-no-ui 的「依赖表 + cargo tree」双查,名单一致 (slint/egui/eframe/iced/winit/gtk/javafx/tauri/fltk/druid/relm)。 依赖表检查按表头切出依赖表再查键名——presenter 的 description 合法地写着 「Slint 版与 iced 版共用」,core-no-ui 那种整文件 grep 在这里会误报。 - design-zero-dep:cargo tree -p unlucky-design --edges normal 只允许出现自身一行, 把 README 明写的「零第三方依赖」卖点从人肉守变成 CI 守。 - 两条都 fail-closed:cargo tree 自身失败(crate 改名/解析异常)同样红; 失败信息给出命中清单与放哪里的建议。 - 未改任何已有 job 的语义(core-no-ui / file-size / licenses 原样)。 --- .github/workflows/guards.yml | 85 ++++++++++++++++++++++++++++++++++-- 1 file changed, 82 insertions(+), 3 deletions(-) diff --git a/.github/workflows/guards.yml b/.github/workflows/guards.yml index 2a172bb..e2109ed 100644 --- a/.github/workflows/guards.yml +++ b/.github/workflows/guards.yml @@ -1,9 +1,11 @@ name: Guards # 架构与合规守门(与 fmt/clippy 同级,不允许 continue-on-error): -# 1. core-no-ui —— crates/core 零 UI 依赖(D3 铁律),依赖表 + cargo tree 双查 -# 2. file-size —— 全仓代码/文档单文件 ≤400 行(用户硬约束) -# 3. licenses —— cargo-deny:许可证白名单 + GPL 依赖拦截 + 供应链来源校验 +# 1. core-no-ui —— crates/core 零 UI 依赖(D3 铁律),依赖表 + cargo tree 双查 +# 2. presenter-no-ui —— crates/presenter 零 UI 依赖(D26 / M85):Slint 版与 iced 版共用的宿主层 +# 3. design-zero-dep —— crates/design 零第三方依赖(M61 / M87):依赖树只允许出现自身 +# 4. file-size —— 全仓代码/文档单文件 ≤400 行(用户硬约束) +# 5. licenses —— cargo-deny:许可证白名单 + GPL 依赖拦截 + 供应链来源校验 on: push: @@ -43,6 +45,83 @@ jobs: fi echo "OK: unlucky-core 依赖树零 UI" + # presenter 是 Slint 版(crates/app)与 iced 版(crates/app-iced)共用的唯一业务层(D26)。 + # 一旦 slint/iced/winit 被拉进来,分层就废了,而编译与单测仍会全绿 —— 所以必须由 CI 守。 + # 与 core-no-ui 同样是「依赖表 + cargo tree」双查,名单保持一致;脚本刻意复制而非抽公共脚本, + # 理由是 CI 守门脚本自身挂掉的排查成本远高于这几行重复。 + presenter-no-ui: + name: presenter 零 UI 守门 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: 依赖表直查(crates/presenter/Cargo.toml 的依赖表不得出现 UI 框架) + run: | + set -uo pipefail + forbidden='slint|egui|eframe|iced|winit|gtk|javafx|tauri|fltk|druid|relm' + # 与 core-no-ui 的整文件 grep 不同:本 crate 的 description 合法地写着 + # 「Slint 版与 iced 版共用」,整文件 grep 会把它当越线。所以先按表头切出依赖表 + # (含 target.*.dependencies 与 dev/build 表),再去掉注释行,只查键名。 + awk ' + /^[[:space:]]*\[/ { + in_deps = ($0 ~ /^[[:space:]]*\[(target\..*\.)?(dependencies|dev-dependencies|build-dependencies)\][[:space:]]*$/) + next + } + in_deps && !/^[[:space:]]*#/ && !/^[[:space:]]*$/ { printf "%d:%s\n", NR, $0 } + ' crates/presenter/Cargo.toml > /tmp/presenter-deps.txt + cat /tmp/presenter-deps.txt + if grep -iE "$forbidden" /tmp/presenter-deps.txt; then + echo "::error file=crates/presenter/Cargo.toml::presenter 不得依赖 UI 框架(D26)。窗口创建 / 渲染 / 图片解码属于 app 与 app-iced 侧;需要跨前端共用的类型请放进 presenter 的 dto。" + exit 1 + fi + echo "OK: crates/presenter/Cargo.toml 依赖表无 UI 依赖" + - name: 依赖树泄漏检查(cargo tree -p unlucky-presenter,排除自身行) + run: | + set -uo pipefail + forbidden='slint|egui|eframe|iced|winit|gtk|javafx|tauri|fltk|druid|relm' + # 注意:cargo tree 命中 UI 框架时**退出码仍是 0**,所以只能抓输出再判断。 + if ! tree=$(cargo tree -p unlucky-presenter --edges normal --prefix none); then + echo "::error::cargo tree -p unlucky-presenter 执行失败 —— crate 是否仍叫 unlucky-presenter?" + exit 1 + fi + echo "$tree" + hits=$(printf '%s\n' "$tree" | grep -vE '^unlucky-presenter v' | grep -iE "$forbidden") + if [ -n "$hits" ]; then + echo "::error::presenter 的依赖树出现 UI 框架(传递依赖也不允许),本次命中:" + printf '%s\n' "$hits" | sed 's/^/ - /' + echo "::error::presenter 是 Slint 版与 iced 版共用的宿主层:把窗口 / 渲染 / 图片解码留在 app 侧,presenter 只持有与框架无关的 DTO、状态 reducer 与 host 装配。" + exit 1 + fi + echo "OK: unlucky-presenter 依赖树零 UI" + + # unlucky-design 是「纯数据 + 纯函数、零第三方依赖」的设计令牌层,这是 README 明写的卖点 + # (连 serde 都不加),但目前只靠人肉守。零依赖的判据比 UI 名单更硬:依赖树只允许有自己一行。 + design-zero-dep: + name: design 零第三方依赖守门 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: 依赖树只允许出现自身(cargo tree -p unlucky-design --edges normal) + run: | + set -uo pipefail + if ! tree=$(cargo tree -p unlucky-design --edges normal --prefix none); then + echo "::error::cargo tree -p unlucky-design 执行失败 —— crate 是否仍叫 unlucky-design?" + exit 1 + fi + echo "$tree" + # 空行不算,剩下的每一行都必须是自己:正常时整个输出只有一行 + # `unlucky-design v0.1.0 (/path/to/crates/design)`。 + lines=$(printf '%s\n' "$tree" | grep -cE '[^[:space:]]' || true) + others=$(printf '%s\n' "$tree" | grep -vE '^[[:space:]]*$' | grep -vE '^unlucky-design v' || true) + if [ -n "$others" ] || [ "$lines" -ne 1 ]; then + echo "::error file=crates/design/Cargo.toml::unlucky-design 必须零第三方依赖,本应只有自身一行,实际 $lines 行;越线的包:" + printf '%s\n' "$others" | sed 's/^/ - /' + echo "::error::design 是纯数据 + 纯函数的设计令牌:色彩 / 排版 / 形状 / 高程 / 动效请不要引入 serde、rand、once_cell 等任何 crate。序列化与运行时能力放到消费方(app / app-iced / presenter)。" + exit 1 + fi + echo "OK: unlucky-design 依赖树只有自身,零第三方依赖" + file-size: name: 文件行数 ≤400 守门 runs-on: ubuntu-latest From 48061c3f31e80d0e73456e7122a05d349f3cdd24 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 2 Nov 2026 11:15:16 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(guards):=20presenter=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E8=A1=A8=E6=A3=80=E6=9F=A5=E8=A6=86=E7=9B=96=20[depen?= =?UTF-8?q?dencies.slint]=20=E5=AD=90=E8=A1=A8=E5=86=99=E6=B3=95=EF=BC=88M?= =?UTF-8?q?89=20=E5=A4=8D=E5=AE=A1=20p2-1=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 原判据要求表头是 `[dependencies]`,`[dependencies.slint]` / `[target.'cfg(windows)'.dependencies.slint]` 这类合法子表既不匹配、表头又被 next 吃掉,整段内容完全不查。配合「子表 + optional + 非默认 feature」可让依赖表与 cargo tree 两步同时绿。 - 表头正则加 `(\.[^]]*)?`,并把带子表后缀的表头本身纳入检查(表头携带包名)。 - 实测:基线绿;`[dependencies.slint]` 红;子表+optional+feature 红;target.* 变体红; `[dependencies.serde_json]` 不误报(仍绿)。 --- .github/workflows/guards.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/guards.yml b/.github/workflows/guards.yml index e2109ed..1f08c31 100644 --- a/.github/workflows/guards.yml +++ b/.github/workflows/guards.yml @@ -62,9 +62,13 @@ jobs: # 与 core-no-ui 的整文件 grep 不同:本 crate 的 description 合法地写着 # 「Slint 版与 iced 版共用」,整文件 grep 会把它当越线。所以先按表头切出依赖表 # (含 target.*.dependencies 与 dev/build 表),再去掉注释行,只查键名。 + # 子表写法 `[dependencies.slint]` / `[target.'cfg(windows)'.dependencies.slint]` + # 是合法的等长写法,且表头本身带着包名,所以表头也要一并落进检查范围—— + # 否则「子表 + optional + 非默认 feature」这种组合能同时骗过依赖表与 cargo tree。 awk ' /^[[:space:]]*\[/ { - in_deps = ($0 ~ /^[[:space:]]*\[(target\..*\.)?(dependencies|dev-dependencies|build-dependencies)\][[:space:]]*$/) + in_deps = ($0 ~ /^[[:space:]]*\[(target\..*\.)?(dependencies|dev-dependencies|build-dependencies)(\.[^]]*)?\][[:space:]]*$/) + if (in_deps && $0 ~ /\.[^]]*\]/) printf "%d:%s\n", NR, $0 next } in_deps && !/^[[:space:]]*#/ && !/^[[:space:]]*$/ { printf "%d:%s\n", NR, $0 } From e9034a40c8f08f933be32607af3a2cf44d73ff90 Mon Sep 17 00:00:00 2001 From: AskaEth Date: Mon, 2 Nov 2026 11:16:00 +0800 Subject: [PATCH 3/3] =?UTF-8?q?fix(guards):=20design=20=E9=9B=B6=E4=BE=9D?= =?UTF-8?q?=E8=B5=96=E9=97=A8=E7=A6=81=E8=A6=86=E7=9B=96=20dev/build=20?= =?UTF-8?q?=E4=BE=9D=E8=B5=96=E8=A1=A8=EF=BC=88M89=20=E5=A4=8D=E5=AE=A1=20?= =?UTF-8?q?p2-2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cargo tree --edges normal 看不到 dev-dependencies / build-dependencies,而 design 对外承诺的是「零第三方依赖」,不分 normal/dev/build。新增一步「依赖表必须为空」: 按表头切出 dependencies / dev-dependencies / build-dependencies / target.*.dependencies (含 [dependencies.serde] 子表写法,表头携带包名),有任何一个键即失败;原「依赖树只允许 出现自身」那步保留,两道互补。 实测:基线绿;[dev-dependencies] serde 红(同一时刻 tree step 仍绿,正是原来的盲区); [build-dependencies] digest 红。老 job(core-no-ui / file-size / licenses)解析后仍与 main 相等。 --- .github/workflows/guards.yml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/.github/workflows/guards.yml b/.github/workflows/guards.yml index 1f08c31..eebb723 100644 --- a/.github/workflows/guards.yml +++ b/.github/workflows/guards.yml @@ -3,7 +3,7 @@ name: Guards # 架构与合规守门(与 fmt/clippy 同级,不允许 continue-on-error): # 1. core-no-ui —— crates/core 零 UI 依赖(D3 铁律),依赖表 + cargo tree 双查 # 2. presenter-no-ui —— crates/presenter 零 UI 依赖(D26 / M85):Slint 版与 iced 版共用的宿主层 -# 3. design-zero-dep —— crates/design 零第三方依赖(M61 / M87):依赖树只允许出现自身 +# 3. design-zero-dep —— crates/design 零第三方依赖(M61 / M87):依赖表(含 dev/build)必须为空 + 依赖树只允许出现自身 # 4. file-size —— 全仓代码/文档单文件 ≤400 行(用户硬约束) # 5. licenses —— cargo-deny:许可证白名单 + GPL 依赖拦截 + 供应链来源校验 @@ -100,12 +100,35 @@ jobs: # unlucky-design 是「纯数据 + 纯函数、零第三方依赖」的设计令牌层,这是 README 明写的卖点 # (连 serde 都不加),但目前只靠人肉守。零依赖的判据比 UI 名单更硬:依赖树只允许有自己一行。 + # 注意 `cargo tree --edges normal` 看不到 dev/build 依赖,所以另设一步按表头切依赖表逐个查空。 design-zero-dep: name: design 零第三方依赖守门 runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable + - name: 依赖表必须为空(dependencies / dev / build 表一律不许有键,含子表写法) + run: | + set -uo pipefail + # design 承诺的是「零第三方依赖」,不分 normal / dev / build —— 而 cargo tree + # --edges normal 对 dev/build 是瞎的。所以先按表头切出全部依赖表(含 + # `[dependencies.serde]` 这类子表写法,表头本身携带包名),再看是否为空。 + awk ' + /^[[:space:]]*\[/ { + in_deps = ($0 ~ /^[[:space:]]*\[(target\..*\.)?(dependencies|dev-dependencies|build-dependencies)(\.[^]]*)?\][[:space:]]*$/) + if (in_deps && $0 ~ /\.[^]]*\]/) printf "%d:%s\n", NR, $0 + next + } + in_deps && !/^[[:space:]]*#/ && !/^[[:space:]]*$/ { printf "%d:%s\n", NR, $0 } + ' crates/design/Cargo.toml > /tmp/design-deps.txt + if [ -s /tmp/design-deps.txt ]; then + cat /tmp/design-deps.txt + echo "::error file=crates/design/Cargo.toml::unlucky-design 的依赖表必须为空(normal / dev / build 都算),本次命中:" + sed 's/^/ - /' /tmp/design-deps.txt + echo "::error::design 是纯数据 + 纯函数的设计令牌,README 对外承诺零第三方依赖(连 serde 都不加):色彩 / 排版 / 形状 / 高程 / 动效请不要引入任何 crate,测试与构建辅助依赖同样不许加。序列化与运行时能力放到消费方(app / app-iced / presenter)。" + exit 1 + fi + echo "OK: crates/design/Cargo.toml 依赖表全空(含 dev / build)" - name: 依赖树只允许出现自身(cargo tree -p unlucky-design --edges normal) run: | set -uo pipefail