nix-flake-update-check · diff
git:20260920.dac80a7 to git:20260927.c5022ea
10 added, 5 removed. Audit A to A.
---
name: nix-flake-update-check
description: 检查任意 nix flake 仓库中软件包的上游版本更新并升级——按包型(npm / cmake / Rust / fetchurl / python)分流的 hash 更新流程与自托管 forge(Gitea)取源、开工前的交互式澄清(支持提问的智能体须主动使用以确保一次跑完)、同账户子项目的链式并行检查(含回环与依赖冲突防护)、版本语义变化时文档须重写而非机械替换、文档外部链接的失效审计、GitHub Actions 的 SHA 固定更新检查、外部自动化 PR 的 hash 修补、flake.lock 处置、补丁内版本检查与 nixpkgs 漂移陷阱。仓库特有的文档与日志环节经「仓库适配层」注入。
---
# nix flake 软件包更新检查(通用)
检查 nix flake 仓库中各软件包的上游发布更新并执行版本升级。
本技能只包含**与仓库无关**的通用方法。仓库特有的环节(文档同步、维护日志、
内置插件清单等)由**仓库适配层**提供——见文末「仓库适配层」。
## 本技能的文件构成
**主流程在本文档**;两份配套参考按需查阅,不必通读:
| 文件 | 内容 | 何时读 |
|---|---|---|
| **`SKILL.md`**(本文) | 交互式澄清 + 第 1~9 步主流程 + 适配层契约 | 始终 |
| [`builders.md`](builders.md) | 按 builder 的 hash 更新流程、`flake.lock` 处置 | 第 4 步 |
| [`traps.md`](traps.md) | nixpkgs 漂移陷阱、fail-closed 校验、外链审计、Actions 更新、补丁内版本 | 第 2 步末尾(Actions)与第 7 步自检命中时 |
## 交互式澄清:一次流程内解决所有待定项
**在开始前先判断所运行的智能体是否支持交互式提问**(如 DSH 提供
`ask_user_question`,可一次提出多个问题并等待用户选择)。**支持就积极使用**
——凡是不确定、有多种合理做法、或必须由用户拍板的事项,**当场问**,不要
先按自己的猜测执行完、再让用户在下一条消息里纠正。
**为什么这条是强要求**:软件更新是**多决策点**流程(升级策略、跨大版本取舍、
依赖冲突的解法、通道选择、是否部署)。若每个决策点都退化成「猜一次 → 被纠正
→ 重来」,一次更新会膨胀成多轮往返,且**每次重来都要重跑构建**——而构建是
本流程最贵的环节(Rust/python 包动辄数十分钟)。把决策点前置到开始之前,
整个流程才可能**一次跑完**。
### 必须提问的情形
| 情形 | 为什么必须问 |
|---|---|
| **跨大版本升级**(3.x → 4.x) | 可能含 breaking change、新的硬性要求,或需要用户接受行为变化 |
| **依赖冲突有两种以上合理解法** | 如「抬依赖版本」vs「跳过该包」vs「打补丁放宽校验」——代价与风险差别很大,且有的会破坏上游安全契约 |
| **需要新增 overlay / 补丁 / 额外 vendored 包** | 增加维护面,属结构性改动 |
| **构建时长或体积显著增加** | 用户可能宁愿暂缓 |
| **通道选择**(stable / next / alpha) | 取决于用户想跟哪条线,技能无法推断 |
| **是否部署到运行中的系统** | 影响生产环境 |
| **发现的信息与文档记载冲突** | 可能是文档过时,也可能是现状异常,需用户判断 |
### 提问的写法
- **一次问全**:把本轮所有待定项**合并成一批**提问,不要挤牙膏式地一问一等。
- **给出可选项与代价**:每个选项写清影响(尤其「破坏上游契约」这类后果)。
- **给出推荐项并说明理由**:把推荐项放第一位,标注「推荐」。
- **能自己查证的不要问**:先查 release notes / 上游源码 / 构建报错,
把问题收敛到真正需要人决策的那几个。
> **不支持交互式提问时**(如纯命令行 agent):把上述待定项**汇总成一份清单**
> 一次性输出,并说明每项的影响与推荐做法,然后**停下等待**——同样避免
> 「猜完再被纠正」的多轮往返。宁可一次问完,不要边做边猜。
### 与「先做完再汇报」的取舍
默认行为是自主推进,本技能是**例外**:因为更新流程的返工成本极高(重建代价),
**提问的成本远低于猜测的错误成本**。但**只问真正需要决策的**——机械步骤
(取 hash、改版本号、同步文档、记日志)自行完成,不要拿流程细节去打扰用户。
## 适用前提
- 仓库是 nix flake(根目录存在 `flake.nix`)。
- 包定义以独立 `.nix` 文件形式存在,通常位于 `packages/`(或其他目录),
由 `flake.nix` 经 `callPackage` 引用。
- 若仓库布局不同(如所有包写在单个 `flake.nix`、或位于 `nix/` 等目录),
以第 2 步的自动发现结果为准,不必强行套用目录名。
## 第 1 步:确认本地仓库
```bash
test -f flake.nix && echo "OK: $(grep -oP 'description\s*=\s*"\K[^"]+' flake.nix | head -1)" || echo "ERROR: not in a flake project"
```
## 第 2 步:发现待检查的软件包
从 `flake.nix` 自动提取被 `callPackage` 引用的包文件,再按排除规则过滤。
**不要硬编码包列表**——从仓库现状动态发现:
```bash
# 自动发现所有被引用的包文件(按仓库实际路径调整正则)
grep -oP '\./[a-zA-Z0-9_/-]+\.nix' flake.nix | sort -u > /tmp/all_pkgs.txt
# 排除自建包(src 指向本地路径)
while read -r f; do
grep -q 'src\s*=\s*\./\.\.\|src\s*=\s*\.\/' "$f" 2>/dev/null && echo "SKIP self-hosted: $f"
done < /tmp/all_pkgs.txt
# ⚠️ 提取版本号时必须同时匹配 `=` 与 `?`(参数默认值),否则漏包
grep -oP '^\s*version\s*[?=]\s*"\K[^"]+' "$f"
```
### ⚠️ 用 `version\s*=` 提版本会**静默漏掉**参数化默认值的包
参数化包把版本写成**函数参数默认值**,而不是普通赋值:
```nix
# packages/dsh.nix —— 主定义,通道包装靠传参覆盖它
{ version ? "0.1.5-rc.2", hash ? "sha256-...", ... }:
```
`version\s*=` **匹配不到** `version ? "..."`:该包会被当成"没有版本"而**从检查
范围里消失**——不报错、不出现在报告里,与"已是最新"无法区分。
```bash
# 判定:同时匹配两种形态,并区分「默认值」与「覆盖值」
grep -nP '^\s*version\s*[?=]\s*"' $(grep -oP '\./[a-zA-Z0-9_/-]+\.nix' flake.nix | sort -u)
```
| 形态 | 含义 | 检查对象 |
|---|---|---|
| `version ? "x"` | **参数默认值** —— 主定义,stable 通道的实际版本 | **主定义的默认值** |
| `version = "x"` | **覆盖值** —— 厚封装调用主定义时传入(如 `dsh-alpha`、`ruyi-alpha`) | 该通道自身 |
**覆盖值不是重复项**:主定义 + 各通道覆盖**都要逐个检查**,因为通道跟的是不同
上游线(stable / beta / alpha 各自的发布节奏不同)。
> 判据:**主定义里带 `?` 的那一行,就是 stable 通道的真版本**。文件顶部常有一句
> `# version/hash overridden for other channels` 的注释——它正是识别此类包的标志。
`flake.nix` 中 packages 段的其余包均纳入检查。
### ⚠️ 软件包之外还有一类**必须检查**的更新:固定 SHA 的 GitHub Actions
**只扫 `flake.nix` 的 `./*.nix` 会漏掉整整一类可更新项**——CI 工作流里固定到
提交 SHA 的第三方 action。它们同样有上游版本、同样会过期,却**不在包定义的
发现范围内**:发现脚本读的是 `flake.nix`,而 action 写在 `.github/workflows/`。
```bash
# 固定 SHA 的 action 有几处?为 0 才可跳过本节
grep -rhoE 'uses: [^ ]+@[0-9a-f]{40}' .github/workflows/*.yml | sort -u
```
**不要凭"这仓库没有"跳过**——先跑上面这条命令看实际输出。完整检查流程
(查最新 tag → 取 tag 指向的 commit → 回写 SHA 与注释版本号)见配套文件
[`traps.md`](traps.md) 的「[检查 GitHub Actions 的更新](traps.md#检查-github-actions-的更新)」。
> **为何强调这一条**:把 action 固定到 SHA 之后**就收不到更新通知了**
> ——这是固定 SHA 的固有代价,**任何采纳该做法的人都会继承它**:仓库维护者、
> 提交 PR 的贡献者、以及评估接手成本的人都一样。通知渠道关掉了,就必须有人
> 主动去查:若仓库同时还有「不引入外部自动化」的边界(托管平台不提供该服务、
> 或明确拒绝此类集成),那么**本技能就是唯一会发现它过期的途径**;即使平台
> 已启用某种自动化,也应跑一遍作**交叉核对**(那些工具只在其配置的版本通道
> 内推进,未必覆盖到你关心的那条线)。漏掉本节,这类更新就彻底无人过问。
> ⚠️ **只读、低成本、任何人可跑**:下面的发现命令只是读取 workflow 文件,
> 不修改任何东西。**发现 ≠ 必须升级**——若你只是路过(无写权限、或与手头
> 任务无关),在报告/PR 描述里指出即可,不必自行改动(升级属需评审的 CI
> 变更,处置建议见 `traps.md`「若你不是维护者:升级 action 也应走 PR」)。
## 排除的软件包
以下类别**不检查**(无固定上游发布版本)。通过 `.nix` 文件特征自动分类:
- **自建软件包**:`src` 指向本地路径(`./` 或 `../` 开头),如
`rustPlatform.buildRustPackage { src = ./src; }`
- **动态版本追踪**:`version` 从外部输入动态读取,如 `builtins.readFile`、URL 抓取、flake input
- **跟随 nixpkgs 版本**:使用 `overrideAttrs` 仅追加 patch,不定义独立 version
- **补丁内硬编码版本**:`.patch` 文件中直接包含 `${version}` 或 wheel URL 及 hash
(见配套文件 [`traps.md`](traps.md) 的「检查补丁内版本」节)
## 第 3 步:检查上游版本
对每个外部包,从其 `.nix` 文件确定上游仓库,然后比对:
```bash
check() {
local pkg="$1" current="$2" repo="$3"
latest=$(gh api "repos/$repo/releases/latest" --jq .tag_name 2>/dev/null)
# 回退到 tag;仍取不到就显式报错,不要当成"最新"
[ -z "$latest" ] && latest=$(gh api "repos/$repo/tags" --jq '.[0].name' 2>/dev/null)
if [ -z "$latest" ]; then
echo "ERROR: $pkg 无法取得上游版本(网络/限流/仓库路径)—— 必须人工确认"
elif [ "${current#v}" = "${latest#v}" ]; then
echo "OK: $pkg $current"
else
echo "UPDATE: $pkg $current → $latest"
fi
}
```
### ⚠️ 用 `gh api` 而不是裸 `curl`
**匿名 `curl` 到 `api.github.com` 可能返回空响应**,而管道下游的 `grep` 同样
静默返回空——于是**每个包都被判成"已是最新"**,整轮检查给出虚假的"全部正常"。
这是本流程最危险的失败形态:**不报错、只撒谎**。
| 做法 | 结果 |
|---|---|
| ✅ `gh api repos/<owner>/<repo>/...` | 走已认证身份(5000 次/小时),失败时**非零退出** |
| ❌ `curl -s https://api.github.com/...` | 未认证(60 次/小时,极易耗尽);耗尽时**静默返回空** |
**两条硬性纪律**:
1. **空结果必须视为错误**,绝不能当作"已是最新"——上面 `check()` 的
`ERROR:` 分支就是为此存在。**整轮没有任何 `UPDATE:` 时,先怀疑取数失败**,
用一两个已知有更新的包回归验证取数链路是否真的通。
2. **首次运行先自检取数链路**:
```bash
# 这条必须返回真实仓库信息;返回空或报错就说明取数链路不通,先修它
gh api repos/<owner>/<known-repo> --jq .full_name || echo "取数链路异常 —— 停止检查"
```
> 非 GitHub 来源(PyPI、crates.io、自托管 forge)同理:**用能明确报错的取数方式,
> 并把"空/异常响应"当作失败处理**,不要用 `curl -s | grep` 这种失败即静默的组合。
上游不一定用 GitHub Release。按实际来源选择:GitHub tag(`/tags`)、PyPI
(`/pypi/<pkg>/json`)、crates.io(`/api/v1/crates/<pkg>`)、上游 CHANGELOG 等。
## 第 4 步:更新构建配置
**按包型选择流程**——包型由 `.nix` 文件中的 builder 决定,不是由包名决定。
> **📖 详见配套文件 [`builders.md`](builders.md)**,其中按 builder 列出:
>
> | builder | 流程要点 |
> |---|---|
> | `buildNpmPackage` | 两次构建取 source hash + `npmDepsHash`;vendored lock 须含 peer 条目 |
> | `stdenv.mkDerivation` + cmake | 单次构建取 hash |
> | `buildRustPackage` | **必须同步 `Cargo.lock`**(最易遗漏) |
> | `fetchurl`(预编译二进制) | 逐个 URL 取 hash,可能需多次构建 |
> | `fetchzip` / `fetchFromGitea` | 自托管 forge:**取源路径可能整体 403**,见下 |
>
> 同文件还包含 `flake.lock` 的三路处置(已 gitignore → 跳过;有动态版本 →
> 必须排除;其他 → 随 hash 一并提交)。
**通用 hash 注意**(无论包型):SRI 用标准 base64、`fetchFromGitHub` 的
hash 不能从 archive tarball 预算、`npmDepsHash` 用 `lib.fakeHash` 占位。
完整说明见 `builders.md` 开头。
## 第 5 步:更新文档
**文档布局因仓库而异。** 按仓库实际结构选择:
- 若仓库有 `docs/<lang>/<pkg>.md` 多语体系 → 在各语言文档中同步版本号
- 若仓库只有单语 README / 包内注释 → 更新对应位置
- 若仓库无文档 → 跳过本步
```bash
# 多语示例(语言列表从仓库实际目录发现,不要硬编码)
for lang in $(ls docs/ | grep -v '\.md$'); do
[ -f "docs/$lang/<pkg>.md" ] && sed -i "s/$OLD_VER/$NEW_VER/g" "docs/$lang/<pkg>.md"
done
```
同时检查并更新 `.nix` 文件中的 `meta.changelog` URL。
> 仓库若有**额外的文档同步要求**(如内置插件清单、特定章节),由仓库适配层补充。
### ⚠️ 何时必须**重写**文档而非机械替换版本号
版本号替换(`sed s/OLD/NEW/`)只处理「数字变了」。**当上游改变了版本语义时,
文档里的旧表述会变成事实错误**——此时必须重写,而不是让新版本号挂在旧描述上。
**触发判据**(命中任一条就要人读一遍文档,不能只跑 sed):
| 触发条件 | 为什么旧表述会失效 |
|---|---|
| **依赖从范围变为精确锁**(`>=x` → `==x.y.z`) | 「≥ 3.4.0」会让读者以为版本可自由浮动,而实际已是 fail-closed 校验 |
| **新增启动期 / 构建期硬校验** | 文档若没提,用户升级后才会在运行时撞上 `RuntimeError` |
| **依赖项增删** | 表格条目数就不对了 |
| **构建方式变化**(fetcher 换型、新增 overlay / patch) | 「来源」列已不准确 |
| **平台 / 版本要求变化**(如 `requires-python` 收窄) | 兼容性描述失效 |
```bash
# 快速发现「文档所列依赖」与「包定义实际依赖」的差异
grep -A15 '^ dependencies' packages/<pkg>.nix # 实际
sed -n '/依赖\|Dependencies/,/^## /p' docs/zh/<pkg>.md # 文档所写
```
> **判据的边界**:本节只规定**何时必须重写**(更新流程的职责),
> **不规定文档该怎么写**(那是文档编写技能的职责)。命中触发条件时,
> 交给文档技能处理,或至少按上表逐项核对——**不要**在这一步临时发明
> 一套文档风格。
**来源实例**(取自一个实际仓库,可用上面两条命令在该仓复现):`packages/godot-ai.nix`
列出 **9** 个运行时依赖,而 `docs/zh/godot-ai.md` 的依赖表只列 **6** 个、且版本
要求全部写成 `≥` 范围;4.0 起上游改为 fail-closed 精确锁后,该表已成事实
错误——**它正是本节要防的那类漏改:版本号升了,表述却没跟着变语义**。
## 第 6 步:检查本地安装版本
```bash
nix eval --raw .#<pkg>.version 2>/dev/null
which <binary> 2>/dev/null && <binary> --version 2>/dev/null
```
## 第 7 步:输出汇总报告
以表格呈现:包名、旧版本 → 新版本、构建状态、本地安装版本。
- ### 提交前的八问自检
+ ### 提交前的九问自检
- **构建通过 ≠ 升级完成**。下面八条都是从「已通过构建、却仍然出错」的实际
+ **构建通过 ≠ 升级完成**。下面九条都是从「已通过构建、却仍然出错」的实际
事故中提炼的,逐条问过再提交——它们比通读全部陷阱章节更快命中要害:
| # | 自问 | 若不查会怎样 |
|---|---|---|
| 1 | **这个包有几个变体?** `ref`/换行搜索包名,看 `flake.nix` 是否按架构或通道分流到多个文件 | 只改一个变体 → 本机构建通过,**另一架构部署后版本没变**(实测发生) |
| 2 | **包定义里的依赖列表,与文档依赖表一致吗?** 数目与语义都要对 | 文档写「≥ 范围」而实际是精确锁 → 读者照做撞 `RuntimeError`(实测发生) |
| 3 | **取源方式还有效吗?** 对 `fetchFromGitea` 等非 GitHub 源,逐 tag 探测 `archive` 与 `api` 两条路径 | 上游改了路径 → 缓存掩盖问题,**干净环境才失败**(实测发生) |
| 4 | **真的运行过产物吗?** `nix build` 成功只说明能构建 | 上游 fail-closed 校验会在**运行时**拒绝启动(实测发生) |
| 5 | **版本号之外的文档表述还成立吗?** 见「何时必须重写文档……」的触发判据 | 版本号升了、描述没跟着变语义 → 文档成为事实错误(实测发生) |
| 6 | **`flake.lock` 该不该提交?** 按本文档「flake.lock 同步」的前置检测 | 该提交而未提交 → 复现不一致;不该提交而提交 → 锁死浮动输入(见该节) |
| 7 | **取数链路真的通吗?** 本轮**没有任何 `UPDATE:`** 时,先怀疑是取数失败而非"全都最新"(见第 3 步的 `gh api` 纪律) | 匿名 `curl` 静默返回空 → 报告虚假的"全部最新",**漏掉全部真实更新**(实测发生) |
| 8 | **`.github/workflows/` 里固定 SHA 的 action 查过了吗?** 见第 2 步末节与 `traps.md` | 固定 SHA 后收不到通知,**这类更新彻底无人过问**(来源仓库实测:3 个 action 共 6 处从未被检查) |
+ | 9 | **「验过了」验的是失败会发生的那一层吗?判据自己能失败吗?** | 验证点比失败点高一层 → 判据全绿而实际已坏;且**从不报警的判据**无法区分「没问题」与「没量到」(实测发生:插件改名让预设挂不起来,而验证停在构建期) |
- > **这八问的来源**:全部是**来源仓库**在 2026-09-17 ~ 09-20 期间**实际踩到的
+ > **这九问的来源**:全部是**来源仓库**在 2026-09-17 ~ 09-28 期间**实际踩到的
> 坑**,每一问都对应一次真实返工或缺陷。**不是假想的检查表**——括号里的数字
> 是该仓库当时的实测记录,你的仓库不一定相同,判据本身才是重点。
>
> 第 1、3、5 问尤其值得单独跑一条命令确认——它们的共同特征是
> **构建与 CI 都不会失败**,只有主动核对才看得见。
>
- > 第 7、8 问共享同一个特征:**它们的失败是"什么都没发现"**。前六问的疏漏
- > 会在构建或运行时报错,而后两问的疏漏**只会让报告看起来一片正常**——
+ > 第 7、8、9 问共享同一个特征:**它们的失败是"什么都没发现"**。前六问的疏漏
+ > 会在构建或运行时报错,而后三问的疏漏**只会让报告看起来一片正常**——
> 这也是它们最容易长期潜伏的原因。
+ >
+ > **第 9 问还多一层**:它质疑的不是「查得够不够」,而是**判据本身有没有区分度**。
+ > 救济办法是给判据配一份**已知坏掉的夹具**(反证)——能把它判出来,判据的 ✓
+ > 才算数。写新判据时同理:**先自证能失败,再去判别人**。
## 第 8 步:记录变更
按仓库约定记录本次更新(维护日志 / CHANGELOG / 无)。**具体用哪套由仓库适配层
指定**——例如采用维护日志的仓库会用其配套的记录技能;没有该约定的仓库则退化为
CHANGELOG 或直接跳过本步。
## 第 9 步:同账户子项目链式检查
当仓库引用了**同一账户下的其他仓库**(典型形态是薄封装:本仓只固定子仓的
`rev` + hash,源码与文档都在子仓),则**子仓自身的上游版本也要检查**——
否则子仓会长期停在旧版本,而主仓的薄封装只是忠实地钉住那个旧版本。
```bash
# 本仓属于哪个账户
OWNER=$(git remote get-url origin | sed -E 's#.*[:/]([^/]+)/[^/]+(\.git)?$#\1#')
# 本仓引用了同账户的哪些仓库(薄封装坐标 = owner + repo)
# 注意用 -h 去掉 "文件名:行号:" 前缀,否则 awk 的字段号会错位
grep -rh -B2 -A4 'fetchFromGitHub {' packages/*.nix 2>/dev/null |
awk -v o="$OWNER" '
/owner[[:space:]]*=/ {owner=$3; gsub(/[";]/,"",owner)}
/repo[[:space:]]*=/ {repo=$3; gsub(/[";]/,"",repo);
if (owner==o) print repo}' | sort -u
```
> ⚠️ **必须 `-h`**:带 `-n` 时输出为 `path:line:content`,`content` 会落到
> 第 3 个字段之后,`$3` 取到的是行号而非值——症状是**匹配结果为空**(而非报错),
> 极易误判为「本仓没有子项目」。
**引用关系不止 `fetchFromGitHub`**:flake input(`github:<owner>/<repo>`)、
git submodule、vendored 源码目录都算。以实际引用方式为准,逐个核对。
### 前提:确认不会引发回环或依赖冲突
链式检查**先验证再执行**。以下任何一条不成立,就**不链式**,退化为在报告里
提示「子仓 X 存在更新,请单独在其仓库内运行」:
| 前提 | 判据 |
|---|---|
| **无回环** | 沿引用关系展开形成的是 **DAG**,不是环。子仓不得(直接或间接)引用回本仓 |
| **无版本冲突** | 子仓的升级目标版本与主仓的其他依赖不冲突(如两包共享同一 peer 版本约束) |
| **可独立升级** | 子仓有独立的上游发布,且升级不需要主仓同步改代码(否则属主仓的普通升级) |
| **账户一致** | 沿链展开时维持同一账户,不越界到第三方仓(第三方的升级由第 3 步照常处理,不属本条) |
#### 回环检测
```bash
# 收集链条;每进入一层前,检查该仓是否已在访问集合中
declare -A VISITED
MAX_DEPTH=3
chain_check() {
local repo="$1" depth="$2"
if [ -n "${VISITED[$repo]}" ]; then
echo "ABORT: 回环 — $repo 已在访问集合中(链条: ${!VISITED[*]})"; return 1
fi
if [ "$depth" -gt "$MAX_DEPTH" ]; then
echo "ABORT: 超出深度上限 $MAX_DEPTH — 疑似意外的长链"; return 1
fi
VISITED[$repo]=1
}
```
> **深度上限是安全网,不是配额**:真正的薄封装链通常只有 1~2 层。命中上限
> 往往意味着引用关系被误读(如把 vendored 的第三方源码当成子仓)。
**回环的实际形态**(子仓可能不是 flake,所以不能只查 flake input):
```bash
# 子仓以任何方式引用回本仓,都构成回环——逐个检查子仓的引用面
SUB=<sub-repo>
gh api "repos/$OWNER/$SUB/contents" --jq '.[].name' | grep -E '^(flake\.nix|\.gitmodules)$' \
&& echo "有 flake.nix / submodule → 必须展开核对"
gh api "repos/$OWNER/$SUB/contents/package.json" --jq '.content' 2>/dev/null |
base64 -d | grep -E '"(@[^"]*/)?'"$MAIN_REPO"'[^"]*":' # 依赖名里出现主仓名
```
> 子仓**没有** `flake.nix` 也**不等于**不可能回环——它仍可能以 submodule、
> 依赖名、CI 脚本等方式拉回主仓。判据是「子仓的任何引用面是否指向主仓」,
> 不是「子仓是否是我认识的形态」。
#### 依赖冲突检测
子仓与主仓共享同一依赖时,**比对两边的目标版本**:
```bash
# 主仓侧该依赖的版本约束
grep -rn '<dep-name>' packages/*.nix | grep -oP 'version\s*=\s*"\K[^"]+'
# 子仓侧的目标版本
gh api "repos/$OWNER/<sub-repo>/contents/package.json" --jq '.content' |
base64 -d | grep -oP '"<dep-name>":\s*"\K[^"]+'
```
两侧指向同一大版本 → 可并行升级。若子仓升级会把主仓的 peer 约束顶到
不兼容区间,**先升主仓侧**(或同步升),不要并行。
> ⚠️ **运行时由宿主解析的依赖不构成冲突**——这是最容易误报的一类。
> 当子仓声明的是 `peerDependencies`(或实际表现为 peer:声明版本低于宿主、
> 由宿主树在运行时解析,如 dsh 生态的 `@deepseek-ai/dsh-*`),则
>
> | 现象 | 判定 |
> |---|---|
> | 子仓声明 `0.1.1-rc.2`,宿主提供 `0.1.5-rc.2` | **不冲突**——运行时取宿主树 |
> | 子仓声明 `0.2.0`,宿主提供 `0.1.5-rc.2`(子仓要求**更高**) | 冲突——宿主满足不了 |
>
> 判据:**只查「子仓要求的版本是否高于宿主能提供的版本」**,而不是「两侧是否相等」。
> 宿主版本更高是正常状态,不是漂移。主仓侧薄封装一律用 `--legacy-peer-deps`
> 跳过 peer 解析,正因如此它才能正常工作。
### 执行:链式并行
前提全部成立时,各子仓的检查**并行**执行,互不阻塞:
| 规则 | 说明 |
|---|---|
| **并行** | 多个子仓(及其各自的子仓)同时展开,无依赖的检查不必串行等待 |
| **各自独立** | 子仓的检查在**子仓自己的工作副本**中进行,不得在主仓目录内改子仓文件 |
| **失败隔离** | 某个子仓失败不阻断其他子仓;失败项进入报告,其余照常推进 |
| **不越权提交** | 子仓的 commit / push 在子仓仓库内进行;主仓只提交自己的薄封装更新 |
```bash
# 并行展开(每个子仓一个工作副本)
for sub in $SUBPROJECTS; do
( git -C "/tmp/$sub" fetch -q origin &&
cd "/tmp/$sub" && <本技能第 1~8 步> ) &
done
wait
```
> **子项目不一定是 nix flake**:被引用的子仓常常是普通 npm / python / rust
> 项目(主仓的薄封装只负责用 `buildNpmPackage` 等包装它)。此时**不要**套用
> 本技能的 flake 专属步骤(`nix flake check`、flake.lock 处置)——按子仓
> 自身的构建体系升级(`npm version` / `pyproject.toml` / `Cargo.toml`),
> 只在主仓侧重新计算薄封装的 `rev` + hash。
### 判据:子仓何时需要在主仓侧跟进
子仓有新提交 ≠ 主仓薄封装要动。按子仓变更的**性质**分流:
| 子仓变更 | 主仓侧动作 |
|---|---|
| **版本号变更**(`version` 字段变了) | **必须跟进**——更新薄封装的 `version` + `rev` + 两侧 hash |
| **仅源码变更、版本未变**(同为 docs/修复提交) | 分情况:<br>• 变更影响**运行时行为** → 跟进 `rev` + src hash(`npmDepsHash` 可能不变)<br>• 仅改文档/README/注释 → **不跟进**,主仓无需重新钉住文档 |
| **仅依赖变更** | 跟进,且需重算 `npmDepsHash`(依赖集变了) |
| **仅清单文件的发布元数据变更**(`package.json` 的 `publishConfig` / `repository` / `keywords` 等) | **不跟进**——文件字节变了但语义输入未变,详细字段分界见下 |
> ⚠️ **别把「子仓 HEAD ≠ 主仓 rev」直接当成待办**。薄封装钉的是**版本坐标**,
> 不是「子仓最新提交」。子仓的 README 改动对主仓构建产物毫无影响,为它重钉
> rev 只会制造无意义的构建与缓存失效。
>
> 判据:**先 diff 判断变更性质,再决定是否跟进**——而不是 rev 不等就升级。
```bash
# 判断子仓在 pin 之后改了什么
gh api "repos/$OWNER/$SUB/compare/$PINNED_REV...$SUB_HEAD" \
--jq '.files[] | "\(.status) \(.filename)"'
# 版本号是否变了(最硬的判据)
gh api "repos/$OWNER/$SUB/contents/package.json" --jq '.content' |
base64 -d | grep -oP '"version":\s*"\K[^"]+'
```
#### ⚠️ 判据的真实分界:不是「有没有变」,而是「变的是不是构建输入」
版本号未变时,**不能只看「是否只有文档」**——`package.json` 这类**清单文件**
的改动尤其容易误导。必须进一步判断改动是否落在**构建输入**上:
| 子仓改动 | 是否构建输入 | 主仓动作 |
|---|---|---|
| `README` / `docs/` / 注释 | 否 | **不跟进** |
| `.github/workflows/`、`.gitignore` | 否(主仓薄封装不消费子仓 CI) | **不跟进** |
| **清单文件中的发布元数据**(`publishConfig`、`repository`、`keywords`、`description`、`bugs`、`homepage`) | **否** | **不跟进** |
| `dependencies` / `devDependencies` / `peerDependencies` / 锁文件 | **是** → 影响 `npmDepsHash` | **跟进** |
| `files` / `main` / `exports` / `scripts` | **是** → 影响打包产物 | **跟进** |
| `version` | **是**(最硬的信号) | **必须跟进** |
| `lib/`、`src/` 等源码 | **是** → 影响产物 | **跟进** |
**关键区分**:`package.json` **整个文件**都是构建输入的一部分(它进 tarball,
src hash 会变),但**它的诸多字段中只有一部分是语义输入**。判据落在**字段级**:
```bash
# 提取 package.json 的实际 diff,逐字段判定
gh api "repos/$OWNER/$SUB/compare/$PINNED_REV...$SUB_HEAD" \
--jq '.files[] | select(.filename=="package.json") | .patch'
```
> **两种结论的取舍**:
>
> - **不跟进**(仅发布元数据/文档变化):主仓薄封装确实与子仓 HEAD 不再逐字节
> 一致,但**构建产物与运行时行为完全相同**。重钉只会制造一次无意义的全架构
> 重建与缓存失效。这是**正确的**选择。
> - **跟进**(任何语义字段变化):必须重钉 `rev`;依赖相关字段变了还要重算
> `npmDepsHash`;仅源码/元数据变了则 `npmDepsHash` 通常不变。
>
> 若无法确定某字段是否影响产物,**按「跟进」处理**——多一次构建远好过漏掉
> 一次真实变更。
### 结果归属
| 场合 | 归属 |
|---|---|
| 汇总报告 | 子仓结果**视为主仓结果**,一并呈现(报告读者关心的是主仓最终钉住的版本) |
| 主仓维护日志 | 记录**薄封装坐标变更**(`rev` / hash),并提供**指向子仓维护条目章节的链接** |
| 子仓维护日志 | 记录**子仓自身的版本变更**(它是该项目的完整记录所在) |
主仓条目里的链接必须**锚定到具体章节**,而不是只给日志文件:
```markdown
**摘要**:<子项目名> <旧版本> → <新版本> — 薄封装 rev/hash 同步(子仓变更见
[子仓变更记录](https://github.com/<owner>/<sub>/blob/main/<日志文件>#<条目锚点>))
```
> 锚点用子仓条目的**时间戳**(如 `#2026-09-17t11-21-34-09-00`)——它在该仓库内
> 唯一,且子仓与主仓都用同一套标题格式时可直接推导。**具体的推导规则与易错点
> 由仓库适配层给出**(它知道本仓用哪套记录格式);给不出锚点就退化为给记录
> 文件链接 + 条目标题文本。
## 仓库适配层
通用流程覆盖到第 8 步为止(第 9 步是可选扩展)。**仓库特有的环节不由本技能
定义**,而由该仓库的适配层补充——通常是同名的「仓库适配层」文档或技能,
也可能只是仓库自己的一份约定说明。
**执行方式**:若当前仓库存在适配层,**先读取它**再执行本技能(不必假定宿主
提供"加载技能"的能力,能读到即可);两者冲突时以适配层为准(适配层了解本仓库
的真实约定)。**没有适配层时**,按第 5 步(文档)与第 8 步(记录)给出的分支
自助处理,**不要臆造约定**——那两步都写了"无该结构则跳过"的退路。
适配层通常补充以下内容:
| 环节 | 适配层需说明的内容 |
|---|---|
| 文档同步(第 5 步) | 文档路径、语言清单、需同步的特殊章节 |
| 变更记录(第 8 步) | 该仓库使用的记录技能或文件(如维护日志) |
| 动态版本输入 | 仓库是否有不可锁定的浮动 input,及其影响 |
| 已知事故教训 | 该仓库历史上因更新导致的故障与规避方式 |
| 额外同步项 | 内置清单、生成文件等需随版本一并更新的内容 |
| 子项目清单(第 9 步) | 本仓引用了哪些同账户子仓、各自的构建体系、子仓日志的路径与语言约定 |