不要为了统一而统一:一次 docs、docs-alfred 和 dotfiles 的工程治理复盘
Jun 7, 2026 - ⧖ 4 minTLDR
这两天看起来做了很多不相干的事:优化 docs 前端、梳理 docs-alfred 架构、评估 Go 第三方库、给 dotfiles 接 treefmt、比较 pre-commit / prek / devenv / git-hooks.nix、看第三方 Nix flakes。
回头看,它们其实是同一类问题:个人项目长到一定规模之后,复杂度不再来自“缺少某个工具”,而是来自边界不清、配置归属不清、source of truth 不清、迁移 ROI 不清。
这次最大的收获不是“要迁到 devenv”,也不是“docs-cli 要拆”。恰恰相反,很多结论都是克制的:docs-cli 不急着拆 binary,dotfiles 不因为 Nix-native 就强行把所有项目迁到 devenv,pre-commit / prek 仍然是很多普通项目里更务实的 hook trigger,treefmt 适合做 formatter 聚合但不该拿来管 linter。
1. 这不是一次功能开发
这几天我手上同时有几条线在推进:
- 优化 docs 项目架构。
- 优化 docs-alfred 项目。
- 处理 docs-cli 的架构问题。
- 用第三方 Go packages 简化当前代码。
- 优化 dotfiles 项目架构。
- 评估 pre-commit、prek、treefmt、devenv、git-hooks.nix 这套工具链。
- 看第三方 Nix flakes 是否还能继续简化 dotfiles。
这些标题放在一起很散。它们不像一个明确的 feature,也不像一次单纯的 bugfix。更准确地说,我这两天是在给几个已经长大的个人项目重新找边界。
docs 是展示层和数据站点,承载 data/gh、blog、docs 前端和部署链路。docs-alfred 是工具层,承载 docs-cli、linear2nl、rss2nl、wiki pipeline、GitHub/Linear API、数据校验和渲染。dotfiles 则已经不是狭义 dotfiles,而是 NixOS / nix-darwin / Home Manager、K8s GitOps、Terraform / Terramate、自托管服务和个人工具链混合在一起的基础设施仓库。
所以这篇文章也不适合伪装成一个非常聚焦的技术教程。它更像一次工程治理流水账 review:我没有先想出一个理论,再去找例子;而是在几个真实项目里反复碰到同一个问题,最后才发现它们其实在问同一件事:
什么东西应该成为 source of truth?什么东西应该留在项目里?什么东西应该被全局工具接管?什么东西根本不值得统一?
2. docs:展示层先从具体体验下手
docs 这条线相对轻一些,但它是一个很好的入口,因为它的反馈最直观。
最近一个明显变化是:自定义 DocsDrawer 替换 Naive UI Drawer 后,打开和关闭速度明显变快。这个提升不是玄学。Naive UI Drawer 内部有 transition、teleport、包装层和状态管理;之前还需要 drawerKey 这类强制 remount workaround 来绕过状态残留。自定义 drawer 改成更直接的 v-if + Teleport to="body" 后,整个交互链路短了很多。
这件事给我的提醒是:展示层优化不应该先从“换框架”开始,而应该从体验最明显、依赖最重、状态最容易泄漏的地方开始。Naive UI 不是不能用,但不能把所有 UI 复杂度都交给组件库。
后续继续扫 docs 前端时,又出现了一批同类问题:
- 除了
NDataTable这类确实有价值的重组件,一些卡片、折叠、抽屉类 UI 可以逐步回到原生 HTML/CSS。 TopicsList.vue的模块级缓存和状态可能在多个实例间共享,应该收回到 composable 或更明确的状态边界里。NaiveThemeProvider在多个组件里重复嵌套,主题 provider 应该上移。- theme 初始化脚本和
useMutationObserver存在重复机制。 - blog 单篇页面读取不应该每次扫全量文章。
这些问题看起来都是前端细节,但本质仍然是归属问题:状态归谁管,主题归谁管,组件库承担到哪一层,缓存应该在模块级还是实例级。
docs 这条线给出的结论很朴素:展示层不是不能用组件库,而是要清楚哪些组件值得保留,哪些只是把简单问题包成重依赖。
3. docs-alfred:问题不是 binary 太大
docs-alfred 是这次最典型的工具层治理。
一开始的问题很容易被说成:“docs-cli 感觉挺乱的,要不要拆?” 但仔细看完后,我的判断反而是:docs-cli 有架构债,但没到必须推倒重来的程度;当前不建议拆成多个独立 binary。
原因是,混乱主要不是 binary 太大,而是语义边界漂了。一个 CLI 乱,不一定是因为它太大;也可能是因为它没有把 action、domain、source、output 这些概念分清。
几个具体例子很明显。
pkg/gh 和 service/gh 都叫 gh,但前者处理的是本地 data/gh 的 walker、check、find、append;后者处理的是远端 gh.yml、catalog、search、render。两个都叫 gh,但一个是本地结构化数据维护,一个是远端 catalog 查询,长期并存会持续制造心智负担。
cmd/pkg/service 的分层语义也不稳定。部分 service 更像聚合全家桶,CLI 层又承担了太多 workflow 决策,比如 domain/scope 推导、跨域 check 编排、输出格式选择。scope 这个词尤其麻烦,它同时混入 domain、RuleScope、路径前缀过滤等不同概念。最后输出协议也不统一:有的走 stdout,有的走 stderr,有的用 slog,有的用 checkutil report。
这时如果直接拆 binary,大概率只是把混乱复制到多个入口里。更稳的路径是:保留 docs-cli 作为 docs repo maintenance CLI 的总入口,但把边界往里收。
| 表面问题 | 更深的问题 | 更稳的处理 |
|---|---|---|
| docs-cli 太乱 | 命令语义和领域边界不清 | 保留 binary,先拆 usecase 和输出协议 |
| scope 太多 | domain/path/filter 混在一个词里 | 消灭公开 scope,内部推导 |
| pkg/service 命名冲突 | 本地 source 和远端 catalog 混淆 | 拆清本地 data source 与远端 catalog |
| 输出不统一 | 工具结果无法稳定组合 | 统一 check/search/render result contract |
所以 docs-alfred 这条线的架构结论是:cmd 层只负责命令定义、flag 解析和输出格式;领域逻辑进入 usecase 层;本地 data/gh source 和远端 gh.yml catalog/index 分开;公开 CLI 不再暴露混乱的 scope 概念;检查、查重、搜索、渲染这些命令要有稳定结果协议。
第三方包不是越多越好
docs-alfred 里还有另一条线:能不能用第三方 Go packages 简化当前代码。
这件事也很容易走偏。看起来像是在找“还有哪些库可以 import”,但真正的结论是:这个项目现在不是缺库,而是已有依赖没有统一复用。真正值得新增或强化的,大多是能把脏边界收成稳定接口的小库。
已经明确有价值的方向包括:
goquery/xurls/html.UnescapeString:处理 transcript HTML 和 URL 提取。go-astisub:解析 VTT/SRT 字幕。go-readability:提取网页正文,降低 AI 输入噪声。resty:统一 HTTP client、retry 和 body 处理。purell/publicsuffix:URL 归一化和 registrable domain。genqlient:Linear GraphQL typed client。go-github:GitHub API typed client。renameio:atomic write。xdg:XDG cache path。
但也有一些方向不值得硬上。比如不建议用 go-git 替代本机 git CLI,因为当前只需要 status、diff、stat 这类短调用。也不建议用通用 validator 替换本地 YAML AST 校验,因为这里需要行号、中文诊断、多文档和字段位置。至于 lo 这类工具,继续扩散到复杂业务循环里,未必能降低复杂度。
这部分的核心不是“库推荐清单”,而是一个判断:
真正能删掉复杂度的依赖,通常不是最炫的框架,而是那些把一个脏边界变成稳定接口的小库。
4. dotfiles:不能用“统一”掩盖成本
dotfiles 是这次讨论密度最高的一条线。
第一层判断是:这个仓库不是没有架构,核心 Nix 架构也不差。真正的问题是,它已经从普通 dotfiles 长成了混合基础设施仓库,但边界和约束还没有完全跟上。
它现在包含 NixOS、nix-darwin、Home Manager、K8s GitOps、Terraform / Terramate、自定义 packages、容器部署、editor、shell、formatter、linter、devops 工具链。到这个阶段,继续问“有没有一个第三方 flake 可以帮我大幅简化”就不太现实了。剩下的复杂度,大多来自真实服务、真实 host、真实 workflow,不是换个框架就能消掉。
更该做的是这几类事:补顶层架构说明,明确 host/home/modules/infra/manifests/pkgs 等目录职责;收紧 scanPaths 自动导入,只在明确 leaf module directory 使用;抽 host/role builder,减少 VPS/homelab 输出装配重复;给 inventory 增加 schema/断言;补 K8s、Terraform、Terramate、Flux 的固定校验入口。
这些都不是“换工具”,而是重新画边界。
treefmt、pre-commit、prek、devenv、git-hooks.nix 分别管什么
dotfiles 里最绕的一段,是 pre-commit、prek、treefmt、devenv、git-hooks.nix 这套东西。
一开始的直觉很自然:既然我是 Nix 用户,那 devenv + treefmt + git-hooks.nix 会不会比传统 pre-commit 更适合?它看起来更 Nix-native,工具版本跟 nixpkgs pin 走,配置也能声明式管理。
但真迁一轮、核一轮之后,判断变得更克制。这里不能按工具名混着聊,必须拆成四层:
| 层 | 问题 | 更适合的工具 |
|---|---|---|
| Formatter 配置源 | 哪些文件怎么格式化 | treefmt / treefmt-nix |
| Hook 触发器 | commit 前跑什么 | pre-commit / prek |
| Tool 版本来源 | 二进制从哪里来 | Nix / nixpkgs / hook env |
| Dev environment | shell、env、services、scripts、hooks 是否统一 | devenv |
这样拆开后,很多问题就清楚了。
treefmt / treefmt-nix 适合做 formatter 聚合和配置源。pre-commit / prek 更适合做 hook 触发器。git-hooks.nix 的优势是 hook 版本跟 nixpkgs pin 走,但代价是牺牲 pre-commit 每个 hook 独立 rev 和生态灵活性。devenv 只有在接管完整 dev shell、tools、env、services、scripts、git-hooks、treefmt 时才有较高 ROI。
也就是说,如果只是为了 hooks 或 treefmt 引入 devenv,收益并不充分。它会把 .pre-commit-config.yaml 变成 Nix 生成物,引入 Nix eval、生成文件、CI 环境路径等新问题。除非同时吃到完整 dev environment 的收益,否则这个间接层未必值得。
这个判断对我很重要,因为它推翻了一个看起来很整齐的迁移方向:不是所有项目都应该为了 Nix-native 而迁到 devenv。
docs / docs-alfred 这种非 Nix 或 Nix 价值不明显的项目,直接用 pre-commit 或 prek 更务实。dotfiles 这种 Nix 原生仓库,可以继续推进 treefmt-nix 或 Nix 管理 formatter 工具链。devenv 则留给真正需要完整开发环境声明的项目。
rules 和 tools 是两件事
这套工具链里另一个容易绕圈的问题,是 rules 和 tools 的归属。
rules 放项目里,CI 和其他人都能拿到,但个人多个项目之间可能 drift。rules 放全局,个人一致性更好,但 CI 和其他人拿不到。tools 让 pre-commit 下载,项目更自包含,但和本地 Nix 工具链重复。tools 用 Nix 管,个人体验更统一,但非 Nix 项目和 CI 要处理环境问题。
所以这里不能只问“要不要统一”。更好的问法是:rules 是项目行为契约,tools 是执行环境,它们是否必须放在同一个地方?
我的阶段性判断是:
| 场景 | rules | tools | trigger |
|---|---|---|---|
| 公司/协作项目 | 跟项目走 | 项目声明或 CI 安装 | pre-commit / CI |
| 普通个人项目 | 跟项目走或轻量复制 | 可以用 Nix 全局 tools | prek / pre-commit |
| Nix 原生 dotfiles | 可以由 Nix 生成或集中管理 | nixpkgs pin | treefmt-nix + 视情况 hooks |
| 完整 dev env 项目 | devenv 统一声明 | devenv / nixpkgs | devenv + git-hooks.nix |
这张表不是永久答案,但它能避免一个常见误区:把 rules、tools、trigger、runtime 全部混成“统一工具链”。一旦混成一个词,讨论就会开始绕圈。
treefmt 管 formatter,不管 linter
还有一个很具体的例子:treefmt 和 linters 的边界。
我一开始也容易把“代码风格工具”全塞进 treefmt 的心智模型里,但这其实不对。treefmt 管的是 formatters 的编排和配置,linter rules 仍然是另一类契约。
实际检查后很清楚:stylua.toml 已经被 treefmt 覆盖,属于重复配置;但 golangci-lint.yml、markdownlint.yml、yamllint.yml、.eslintrc.yml 不属于 treefmt 管理范围。因为 treefmt 管 formatter,不管 linter。
这个细节很小,但它反映了同一个问题:工具的边界不能靠感觉定。把 markdownlint、yamllint、golangci-lint 误认为 treefmt 会覆盖,只会让配置归属更乱。
第三方 Nix flakes 不是银弹
第三方 Nix flakes 这条线也类似。
dotfiles 已经用了 flake-parts、nixos-facter、disko、home-manager、nix-darwin、sops-nix、nix-homebrew、stylix、nvf 等一批关键组件。剩下的 Nix 代码里,大头是具体软件和服务配置:yazi、Zed、zsh、K8s、fcitx5、sing-box/mihomo 这类东西。这些不是换一个 flake framework 就能干净消掉的。
评估下来,srvos 值得小范围试 server baseline,但它的默认值可能和现有安全/host meta 语义冲突,比如 sudo、timezone、authorizedKeysFiles、firewall。nixos-unified 可以隔离 POC,因为它基于 flake-parts,可以验证 autowiring 是否能替代部分 outputs 装配;但如果未来撤掉 Darwin,只剩 NixOS fleet,它的价值会下降。snowfall-lib 结构意见太强,迁移成本高。flakelight 是另一套 flake framework,也不适合作为当前主线。
最后结论还是:保留 flake-parts,不急着换框架。
第三方 flake 的价值不是“能不能少写几行 Nix”,而是它的默认抽象是否和我的仓库边界一致。否则只是把自己的复杂度换成别人框架的复杂度。
5. 三条线合起来看:source of truth
把 docs、docs-alfred、dotfiles 放在一起看,会发现它们其实在反复问同一个问题。
| 项目 | 表面优化 | 真正问题 |
|---|---|---|
| docs | 换 Drawer、剥离 Naive UI | 前端状态、主题、组件依赖的归属 |
| docs-alfred | 加第三方包、拆 docs-cli | 命令语义、usecase 边界、输出协议的归属 |
| dotfiles | 迁移 hooks / treefmt / devenv | rules、tools、trigger、runtime 的归属 |
这几条线最后都回到 source of truth。
docs 里,主题状态和缓存不应该散在组件里。docs-alfred 里,业务决策不应该塞在 cmd 层。dotfiles 里,formatter rules、linter rules、hook trigger、tool version 不应该混在一个配置文件里。
所谓优化,不是把东西搬到一个更时髦的工具里,而是让每类决策有唯一且合理的位置。
6. 最有价值的是没做什么
这次复盘里,很多高质量决策不是“新增了什么”,而是“没有引入什么”。
没有因为 docs-cli 变大就立刻拆 binary。没有因为 Nix-native 就强行把 docs / docs-alfred 都迁到 devenv。没有因为 treefmt 能聚合 formatter 就让它背 linter 的锅。没有因为第三方 Nix flakes 看起来高级就引入 snowfall-lib / flakelight。没有因为 Go 项目可以加很多库就把业务校验交给通用 validator。
这些“不做”的判断很重要。个人项目最容易犯的错,就是为了追求一致性、现代化、声明式、可复用,把一个局部问题升级成新的平台迁移。工具链治理的目标不是让所有东西看起来都属于同一套,而是让每个问题都落在合适的复杂度层级上。
7. 下一步怎么收束
这篇 review 之后,真正要做的不是继续开更多方向,而是把已经形成的判断收束成行动。
docs-alfred 这边,应该把架构 review 拆成小任务:usecase 层、gh source vs catalog 命名、输出协议、scope 内部化。这里最怕继续停留在评论里的长讨论,必须变成可执行的 refactor checklist。
dotfiles 这边,应该写稳定的 hooks / treefmt / devenv 策略文档,明确个人项目、Nix 原生项目、协作项目分别怎么选。尤其要把 rules、tools、trigger、runtime 拆开写清楚,避免以后每次遇到 lint/format 又重新绕一遍。
docs 这边,先做低风险前端优化:继续替换低收益 Naive UI 组件,收束主题 provider 和缓存状态,避免为了“优化前端架构”打开过大的重构面。
这次 review 对我最大的作用,是把“我要不要迁到某个工具”改写成了“这个决策到底应该归谁管”。一旦问题这样问,很多选择反而清楚了。