# CREDITS

# 名录与署名承诺

> 本文件是**承诺的登记处**。承诺不落在文件里，就只是一句话：项目换维护者、
> 换域名、换托管，它跟着消失，而当事人无从主张。
>
> 机器可读的同一份记录是 `credits.json`；两者由 `repo_lint` 的 R12 / R13 强制一致。

## 0. 为什么单独一份文件

`NOTICE` 只声明**权利人**（谁拥有这套东西），`CONTRIBUTING` 只说**怎么提交**
（提交时签什么）。两者都不回答「张三做过什么、凭什么记他、名字写在哪」。

名录要能被执行，需要四样东西，缺一样就落不了地：

| 要什么 | 在哪 | 缺了会怎样 |
| --- | --- | --- |
| 一条**可核验的依据** | 本文件「依据」列（commit / 数据集版本 / 日期） | 只剩口头说法，争议时无法证明 |
| 一个**公开位置** | 本文件 + `credits.json`，随仓库公开 | 只在对话里说过，等于没有 |
| 一条**防抹除**的痕迹 | §8 名录快照（逐版记 sha256） | 后来者删掉条目，无人可察 |
| 一条**认领与退出**通道 | §6 | 该记的记不上，不想记的撤不掉 |

## 1. 三类人，三种法律基础（不可混为一谈）

把它们混成一张「感谢名单」，是最常见的错——因为三类人的**救济手段完全不同**。

| 类 | 谁 | 法律基础 | 对方能主张什么 | 违约/侵权时怎么办 |
| --- | --- | --- | --- | --- |
| **A 著作权贡献者** | 写了代码、文档、规范的人 | 署名权是著作人身权（《著作权法》第十条第一款第二项），不可转让、不可剥夺、保护期不受限制 | 要求在**他写的那部分**上署名，且不得被歪曲、篡改 | 停止侵害 + 赔礼道歉；不因项目换许可、换托管而消灭 |
| **B 数据贡献者** | 录入、校核、清洗账目的人 | 数据按 CC-BY-4.0 发布，**署名是许可条件**（§3(a)） | 要求在其贡献的数据集上署名 | 不署名即无权分发该数据集——分发方自己变成侵权方 |
| **C 支持者** | 出钱、出服务器、出域名、出场地的人 | **没有著作权**。单纯给钱不产生作品，不产生署名权 | 只能主张**约定**：本文件 §4 写了什么，就能要什么 | 按约定处理；约定没写「永久」，就不能要求永久 |

一个必须说清的推论：**C 类拿不到「署名权」，只能拿到「列名」**。
所以对支持者承诺时说「给你署名权」是错的——承诺了一个法律上不存在的东西，
将来兑现不了，反而构成违约。本文件对 C 类只承诺「列名 + 可核验的记录」，
并把期限与撤除条件写明（§4、§7）。

## 2. A 类：著作权贡献者

登记要求：`identifier`（长期笔名或真名）、`email`、`attribution`（要写进文件头的整行）、
`evidence`（可核验的 commit）。四项齐全才登记。

| 标识 | 署名文本 | 依据 | 登记日 |
| --- | --- | --- | --- |
| 老夫 | `Copyright 2026 老夫 <2379731846@qq.com>` | `84cf110`、`4fb2ae5`、`e62d659`、`22e4b73`、`0894d9d`、`a72b296`、`e34788c`（以本名标识提交）、`ba27972`、`d135c2b`（以「老夫」提交） | 2026-11-08 |

```credits
laofu | 老夫 | 2379731846@qq.com | release
```

逐条说明：

- **署名文本是给文件头直接抄的整行**，不是名字。写成整行是为了避免「写谁、写在哪一年、
  带不带邮箱」每次都重新讨论一遍——这类讨论每发生一次，就有一个文件漏署名。
- **依据必须指得到具体提交**。R12 会逐条验证 commit 在 git 里存在；写不存在的提交，
  体检就红。这是防「虚构贡献者」的唯一机器手段。
- **同一个人可能有多个提交邮箱**。本条合并了两组标识：一组是 `<noreply@drawcommons.org>`
  上的 6 次（2026-11-10 至 11-11，含 DDL v1.3 与 2.0 冻结说明），另一组是
  `老夫 <2379731846@qq.com>`。这里的「次数」有两个口径，别混着读：
  **按 author 统计，截至 `638f157` 是 6 次与 8 次**（此后每落一次提交就变，故表格一律标
  「截至某提交」，不写「目前」）；**按本表依据统计，是上列 9 条**——其中 6 条以本名标识署名、
  2 条以「老夫」署名、1 条是基线提交（集体名，见 `accounts`）。合并依据是 2026-11-11
  本人确认两者为同一人，
  不是凭名字相似推断的。前一组标识所用的姓名是作者本名，**本人要求不公开，本文不重复**——
  但要说清这只是「不在正文重复」，不是「查不到」：那 6 条提交的 author 字段里写着它，
  一 clone 就能看见（见 §5.2、§11.3）。
  `credits.json` 的 `emails` 字段记录全部已知邮箱，R12 据此判定「不漏记」。
- **2026-11-10 之前的提交挂在维护者账号下**（`admin@drawcommons.org`，13 次）。
  账号不是作者，故在 `credits.json` 的 `accounts` 段声明归属，不单独列人名。
  原始提交的 author 字段不可改写，也不该改写——改写历史等于销毁证据。
- **发布权是单独一栏，不是贡献的附属品。** 上面清单第 4 格写 `release` 的人，才有权签
  `v*` tag 并让钩子出包（`credits.json` 里是 `can_release`）。有贡献不等于有发布权；
  反过来更要紧——**给一个人发布权，必须同时给他这里的一行记录和 `allowed_signers` 里的一把钥**，
  三处对齐由 R14 强制。这样「签发权被悄悄扩大」这件事在体检里藏不住，
  见 §11.2。

## 3. B 类：数据贡献者

数据贡献者的署名**不能写进账本数据行**：导出件的字节被 sha256 锚定，往数据行里加
任何字段都会改变锚值，使全部历史锚值失效（见 `NOTICE`「署名边界」）。

所以 B 类的署名落在**数据集层面**，三处：

1. 数据集伴随文件（`data/` 下每份数据集的同名 `.credits` 文件，或 `data/CREDITS.md`）；
2. 本文件 §3 表格；
3. 站点点名（数据页脚注）。

粒度是「某人贡献了某个数据集的哪个范围」，不是「某人贡献了第 1234 条记录」。
逐条署名既做不到（锚值），也没有意义（一条记录的著作权几乎不可主张）。

当前登记：无。理由是本项目尚未接入任何外部数据贡献者；`evidence/ledger_w4.db`
是构建产物（由 `build_ledger.py` 生成），其内容由维护者录入，已并入 §2。

## 4. C 类：支持者

分三档，承诺强度不同。**不写「永久」**——域名会过期、服务器会停、项目可能终止，
承诺做不到的事，等于给自己埋一个必然违约的条款。

| 档 | 支持形式 | 承诺 | 期限 |
| --- | --- | --- | --- |
| C1 资金 | 现金、报销、代付 | 站点点名 + 本文件列名 + 金额区间（经本人同意才写具体数额） | 支持事实存续期间；服务终止后保留在最后一版 tag 的名录里 |
| C2 基础设施 | 服务器、带宽、对象存储、域名 | 同上 + 部署文档中标注提供方 | 同上 |
| C3 传播 | 长期转载、翻译、教学使用 | 本文件列名 | 同上 |

共同约束三条：

1. **不承诺商业回报、股权、收益分成**。本项目不产生可分配收益，承诺分成就是空头。
2. **列名不等于背书**。名录里出现某人，不表示此人认可项目的全部内容；
   反之项目也不为支持者的言行背书。这句话必须写，否则双方都可能被牵连。
3. **撤回即撤名**。支持者随时可要求撤除，30 日内处理，历史版本不改（§7）。

当前登记：无。本项目至今未接受任何外部资金、资源或场地支持。

## 5. 历史标识：四个变三个

git 历史里出现过 4 个作者标识，落成 **3 条记录**：1 个自然人（占其中 2 个标识——笔名一个、
本名标识一个）+ 2 个非人账号。处置如下：

| 标识 | 出现 | 处置 | 理由 |
| --- | --- | --- | --- |
| `老夫 <2379731846@qq.com>` | 8 次（截至 `638f157`） | 登记为 A 类（§2） | **笔名**，对外署名一律用它 |
| `<noreply@drawcommons.org>` 那一组 | 6 次（全部） | **并入上条**（2026-11-11 本人认领） | 作者本名，本人要求不公开；与「老夫」为同一人 |
| `drawcommons <admin@drawcommons.org>` | 13 次 | 记入 `accounts`，归属老夫 | 维护者账号，非自然人 |
| `数字丐帮 2.0 维护者 <maintainer@example.invalid>` | 1 次 | 记入 `accounts`，归属老夫 | `example.invalid` 是保留域名，永不可达；集体名的典型反例 |

### 5.1 关于那组 `noreply` 标识的认领过程

这 6 次提交包含 DDL v1.3、2.0 冻结说明等实质内容。此前挂起未登记，理由是：

- 归并到「老夫」名下 —— **等于替对方主张（或放弃）署名权**。署名权是人身权，
  除本人外任何一方都无权处分，包括项目方。
- 单独登记为一个作者 —— 等于凭一个 `noreply` 域、无法回信的标识，
  给一个可能不存在的人发署名。

两条都不对，所以当时挂起等认领。2026-11-11 本人确认该组标识用的是本名、「老夫」是笔名，
两者为同一人，故合并为一条。**本名不写进本文**（见 §5.2）。合并的依据是本人确认，
不是名字相似或邮箱域相同——后者只能作为线索，不能作为证据。

### 5.2 为什么对外仍只用笔名

笔名署名不削弱任何权利。《著作权法》第二十四条意义上的署名权，包含**决定是否署名、
以何种方式署名**；用笔名发表，作品的著作权归属不变。本名与笔名的对应关系**由一个不入库
的核验记录固定**（核验日期 2026-11-11，方式为本人确认），公开文件只记「已核验、本人要求
不公开」，不记姓名本身。需要主张权利时走 §6 的联系方式，由本人自证，不靠公开姓名。

反过来，把真名写进每个文件头也不是"更严谨"：文件头的版权行会随每个文件复制到各处，
实名一旦散布到无法回收的位置（他人镜像、缓存、打包件），就不能再收回。**笔名对外、
实名留档**是这两者之间唯一可逆的折中。

一件容易被误读的事要说清：**本名在 git 历史里，只是不在本文里**。那 6 条提交的 author
字段写着本名，`git log` 谁都看得见；改写它等于撕证据，且已签的 tag 会一起失效（§11.3）。
所以本文不写名字，性质是「不在正文重复」，不是「查不到」——真要查不到，只有不公开
`.git` 一条路，而发布包本来就是 `git archive` 出来的，不含历史。

**2026-11-12 决定：接受公开历史里的本名，不再走「隐藏 `.git`」这条路。** 公开仓库会带完整
历史，那 6 条提交的 author 照旧是本名，谁 clone 都看得见。这个决定**只决定「历史公开与否」，
不改变本文的写法**：正文仍不重复本名，因为正文会随每个文件被复制到各处（镜像、缓存、打包件），
收不回来；而 git 历史只有一份、不可改写、`git log` 一次就能查全。**该留痕的地方留痕，该克制
的地方克制**，两者不冲突。

决定旁边的限度也要一起写：这一步不可逆。仓库一公开，clone 出去的副本就收不回来，删库也只能
让新访客看不到。所以「接受」是在信息完整的前提下做的选择，不是「先公开再说」。

`credits.json` 的 `emails` 字段同时记两个邮箱，是为了让 R12 认得出那 6 次提交属于这条
记录——否则体检会把这 6 次判成「漏记」，逼着维护者去编一个假条目。

## 6. 认领与登记

- **认领**（针对 §5 的 `pending`）：用提交所用邮箱，或能证明对该邮箱控制权的任何方式，
  发信到 `admin@drawcommons.org`，说明要登记的名字与联系方式。核实后 14 日内登记。
- **自我登记**（新贡献者）：提交 PR 改 `CREDITS.md` 与 `credits.json` 两处，
  或直接发信。登记不设门槛——**只要贡献可核验，登记是义务不是恩惠**。
- **改名**：随时可改显示名，`id` 不变。旧名在 `aka` 字段留痕，不删除。
  这是为了让人能顺着旧名找到新名，而不是让旧名凭空消失。
- **本名不公开**：不算改名，不进 `aka`。`aka` 只记公开用过的名字；把本名写进去，
  等于在一份会跟着发布包散出去的公开文件里，把笔名和身份证上的名字对上号，
  之后再撤也收不回（§5.2）。核验只留「已核验、本人要求不公开」这一句，姓名留在不入库的记录里。

## 7. 承诺什么、不承诺什么

| 承诺 | 不承诺 |
| --- | --- |
| 名录随仓库公开，任何人可 clone 后自行核验 | 「永远排在第一位」「字号最大」这类无法量化、必然引发争议的事 |
| 条目只在本人书面要求时删除；删除前在 `CHANGELOG` 留一条 | 每个 commit 单独列名（粒度是贡献范围，不是提交次数） |
| 文件头署名一经写入，不因维护者更替而改动 | 项目永久存续。项目终止时，最后一版 tag 与 `credits.json` 快照即为永久记录 |
| 支持者列名、注明支持形式、可随时撤回 | 商业回报、股权、收益分成 |
| 署名不进入账本数据与导出件（那会破坏锚值，见 §3） | 在数据行内逐条署名 |
| 退出后 git 历史不动（历史 author 不改写） | 「把历史里的名字也抹掉」——那等于伪造历史，任何人都做不到，也不该做 |

**关于退出**：署名权可以放弃，不能被剥夺。本人要求隐去时，按**上表第二行**处理：
删现行名录条目、`CHANGELOG` 记一条、git 历史保持原样。这里要分清两件事——
**删的是工作副本里的现行条目**；已发布版本的 tag 与 §8 快照里的记载**改不动**，
删了也不会让 `v2.1-rc3` 的包里少一个字。历史里仍能查到当初的 `author`，
这不是违约——那是客观发生的提交记录，抹掉它才是对所有人的不诚实；快照表不跟着删，
正是为了让「什么时候删的、删之前长什么样」留在案上。

## 8. 名录快照

每次发版把 `credits.json` 的 sha256 记在这里。作用是**防抹除**：后来者若删掉
某条目，当期文件可以改，但已发布版本里的这一行改不掉——旧 tag 仍指向旧快照，
比对即可发现删改。

| 版本 | 日期 | `credits.json` sha256 | 条目数 | 待认领 |
| --- | --- | --- | --- | --- |
| v2.1-rc11 | 2026-11-12 | `5710a71c146ffb7d976d875447db401c8d989fc71dbc2298405f7b6550b677b3` | 1 | 0 |
| v2.1-rc10 | 2026-11-12 | `5710a71c146ffb7d976d875447db401c8d989fc71dbc2298405f7b6550b677b3` | 1 | 0 |
| v2.1-rc9 | 2026-11-12 | `5710a71c146ffb7d976d875447db401c8d989fc71dbc2298405f7b6550b677b3` | 1 | 0 |
| v2.1-rc8 | 2026-11-12 | `aa87743d2aa2a22a6edd710ccb4597f4496e818edbee60717e289d8b8c5ce014` | 1 | 0 |
| v2.1-rc7 | 2026-11-11 | `f466e6e8a16bae1dbabc6a7f2dd155d1b015720deb9586ca6e0f95004010ad7d` | 1 | 0 |
| v2.1-rc6 | 2026-11-11 | `f466e6e8a16bae1dbabc6a7f2dd155d1b015720deb9586ca6e0f95004010ad7d` | 1 | 0 |
| v2.1-rc5 | 2026-11-11 | `a3810376d8ace5956c1ec323229d09e4f27e14ae962156085c035a98f1c21006` | 1 | 0 |
| v2.1-rc4 | 2026-11-11 | `a3810376d8ace5956c1ec323229d09e4f27e14ae962156085c035a98f1c21006` | 1 | 0 |
| v2.1-rc3 | 2026-11-11 | `1cee27f17e53f2b4cd4c3bd118cc636804b31db74225de57234d3541f18f64ad` | 1 | 1 |

`v2.1-rc5` 与 `v2.1-rc4` 哈希相同，是因为这一轮只加了 §10（验签）与 `allowed_signers`，
名录条目本身一条未动——快照哈希不变，恰好说明名录没被顺手改动。`rc9`–`rc11` 三行同哈希
同理：rc10 改的是公开文本（本名不进公开正文），rc11 加的是仓库根 README 与 §11.3 的实证
结论，**都动的是"怎么写"，没动"记了谁"**。

`v2.1-rc6` 的哈希变了，因为它加了 `can_release`（发布权）与 §11——**名录内容真的改了，
哈希就该变**。两次变化放在一起看，正好说明这个快照不是摆设：没改时它不动，改了它必动。

`v2.1-rc7` 的哈希与 `rc6` 相同，因为那一轮只加了审查记录（`REVIEWS.md` / `reviews.json`）与
R16、服务端 `update` 钩子、页面链接——**名录一个字没动，哈希就不该动**。补记这一行是因为
这张表的用处是「每一版都能对上」：漏掉 rc7，后来者无从判断那一版的名录是没变、还是没记。

`v2.1-rc8` 的哈希又变了，因为 `aka` 被清空（本人要求本名不公开，见 §5.2）。**条目数仍是 1、
待认领仍是 0**：变的是「记不记本名」，不是「记不记这个人」——人还在名录里，依据 9 条一条没少。
这张表恰好替这件事留证：将来若有人把名字「顺手补回去」，rc8 这一行对不上。

`v2.1-rc9` 的哈希再变一次，只因为订正了一个数字（`evidence.note` 里 author 统计 8 次、
依据 9 条两个口径分开写）。**这不是「又改了名录」，是「把错字改回来」**——rc8 已出包，
里面那处说法是错的，故 rc8 与 rc9 并列保留，谁都能看出改了什么、什么时候改的。

这条记录同时是「本文件与 `credits.json` 未分叉」的旁证：快照里的哈希对不上
当期 `credits.json`，说明两者之间有人改过而没同步。

## 9. 怎么改这份文件

1. 改 `CREDITS.md` 的表格 **和** `credits.json` 的 `people` / `pending` / `supporters`；
2. 跑 `python evidence/repo_lint.py repo`——R12（名录 ↔ git 双向一致）、
   R13（两份源记录集与发布权相同）、R14（公钥指纹两处一致、签发权 ↔ 名录双向、一人一钥）、
   R15（已签名提交的签名者就是作者）必须通过；
3. 删改条目的，`CHANGELOG` 加一条，写明依据（谁要求的、什么时候）；
4. 发版时更新 §8 快照；
5. 换签名密钥时，同步改 `repo/allowed_signers` 与 §10.3 的指纹——**两处一起改**，
   只改一处会被 R14 判红；
6. 授予或收回发布权，改三处：`credits.json` 的 `can_release`、本文件清单块的 `release` 格、
   `allowed_signers` 里那行钥。**只改前两处会红在 R14（有发布权却没钥），
   只改第三处会红在同一处（有钥却没发布权）。**

**本条不得因为「只是改个名字」而跳过**：名录分叉过一次，就再也说不清哪份有效。

## 10. 签名：证明「这个版本是我发的」

名录回答「谁贡献了什么」，签名回答「这个版本是不是权利人发的」。两件事不能互相替代：
名录防自己人出错（漏记、虚构、偷偷删条目），签名防外面的人冒充（伪造「官方 v3」、
中间人替换下载包）。

### 10.1 怎么验（一条命令，不需要装别的工具）

```sh
git clone https://drawcommons.org/repo.git
cd repo
git -c gpg.ssh.allowedSignersFile=repo/allowed_signers verify-tag v2.1-rc5
```

期望输出：

```
Good "git" signature for 2379731846@qq.com with ED25519 key SHA256:3vOTR4UHVjuwcOywJRalSKEA+G+Ckln6SVF4Zep9MZE
```

`Good` 是签名对；`for 2379731846@qq.com` 是这个签名主张的身份。两句都要对——
密钥对但 principal 不在 `allowed_signers` 里，会报 `No principal matched` 且退出码 1。

不想装 git 也可以，公钥直接取：`https://drawcommons.org/download/allowed_signers`。
用 `ssh-keygen -Y verify` 手工验也可以，但没必要——`git verify-tag` 做的就是这件事。

**查单条提交有没有签名，必须带上同一条配置**：

```sh
git -c gpg.ssh.allowedSignersFile=repo/allowed_signers log --show-signature -1
```

不带的话，`%G?` 会显示 `N`（当作「没有签名」），并额外报
`error: gpg.ssh.allowedSignersFile needs to be configured`。那个 `N` 说的是
「没配验证器」，不是「没签名」——两者显示出来一模一样，很容易看错。
`repo_lint` 的 R15 内部就是显式传了这条配置，所以它在全新 clone 里也能正确数出签名数。

### 10.2 签名证明什么、不证明什么

| | 内容 |
| --- | --- |
| **证明** | 该 tag 对象由持有对应私钥的人创建；tag 的提交指针在签名后被改动即验不过 |
| **不证明** | commit 的 `author` 是谁——除非那条提交本身带了签名，见 §10.6 与 R15 |
| **不证明** | 私钥持有人就是作者（签名只绑密钥，身份绑定靠 §10.3 的指纹分发） |
| **不覆盖** | 下载到的 `.tar.gz` 本身（那是 `.sha256` 与账本锚值的事） |

一句话：签名证明**发布行为**，不证明**创作归属**。完整的链条是
「签名 tag → commit → author 邮箱 → 名录条目 → 具体的人」，缺一环就断在那里。

### 10.3 指纹与信任根（这条必须诚实）

| 项 | 值 |
| --- | --- |
| 公钥指纹 | `SHA256:3vOTR4UHVjuwcOywJRalSKEA+G+Ckln6SVF4Zep9MZE` |
| 算法 | ED25519（`ssh-ed25519`），只签名不用于登录 |
| 记载处 | 本节 + `repo/allowed_signers`（R14 强制两处一致） |
| 与部署钥的关系 | **完全分离**：部署钥 `drawcommons-deploy` 能推送，签名钥只能证明发布 |

**局限，写在明面上**：本节与公钥都在同一个仓库里。能改仓库的人，可以把公钥和本节
的指纹一起改掉，此时 R14 依然会通过——它只能防「自己改漏」，防不了「两份一起改」。
所以仓库内的指纹只能算**方便**，不能算**信任根**。

真正的信任根必须来自仓库外，两条路：

1. **DNS TXT 记录**（推荐，随域名走）：`_sign.drawcommons.org` 放同一指纹。
   **当前未设置**——这是 §10.5 的待办之一。
2. **第三方账号的公钥**：GitHub 账号上传同一把公钥并启用 `vigilant mode`。
   **当前未做**——要等 G3 建好仓库。

在两条路都落地之前，对外只能这样说：**这些 tag 已自签名，可验证一致性；
尚未经仓库外渠道交叉验证。** 说得比这更满，就是拿一个可被一起改掉的文件当保证。

### 10.4 从哪一个版本开始签

| 版本 | 签名 |
| --- | --- |
| `v2.0-final`、`v2.1-rc1`–`rc4` | 未签名（机制建立前发布） |
| `v2.1-rc5` 起 | 已签名 |

**旧版本不补签。** 补签必须移动 tag 指向的对象，而已下载的包与 tag 的对应关系会脱钩——
一个「补过的签名」比「没有签名」更糟：它把一段无法自证的历史包装成已自证的。
未签名这件事本身也留在 `CHANGELOG` 里，后来者能看见。

### 10.5 密钥的处置（丢失、泄露、轮换）

| 情形 | 做 法 |
| --- | --- |
| 丢失私钥 | 生成新钥 → 加入 `allowed_signers`（**保留旧钥**，历史 tag 还要靠它验）→ 更新 §10.3 指纹 → 公告 |
| 私钥泄露 | 立即从 `allowed_signers` 删除该公钥、`CHANGELOG` 记一条并写明「此时间点之后用该钥签的一切无效」；已发布 tag 不动（改 tag 等于伪造历史） |
| 例行轮换 | 新钥并行一段时间，新 tag 用新钥；旧钥在全部旧 tag 不再需要验证前不删 |
| 口令遗忘 | 与丢失同处理。口令只保存在密码管理器里，不写在本仓库任何位置 |

**不做的事**：不在 tag 里塞公钥、不用同一把钥既签名又部署、不把私钥的任何部分
（含指纹以外的派生信息）写进仓库。指纹是公开的，私钥不是。

### 10.6 签名的密度：一个人时签 tag，多人时必须签 commit

只签 tag 的理由：逐条签 commit 会把「改一个错别字」变成一次密钥操作，换来的是
`git log` 里一串 `Good signature`——而它证明的仍是「这个提交由持钥人提交」，不是
「内容正确」。发布的可信边界是 **tag**：用户下载的是 tag 对应的包，不是某一条 commit。

**但这个结论只对一个人成立。** 一旦有第二个人提交：

- commit 的 `author` 字段是自由填写的，谁都能写成别人的名字。没有签名，这句话就只是自称。
- 别人可以拿自己的钥签一条「作者写着你」的提交，`verify-commit` 会报
  `Good signature for 别人`——看起来一切正常。
- 名录能证明「这个名字登记过」，证明不了「这条提交出自他手」。签名 tag 只覆盖 tag 对象，
  覆盖不进里面那串 commit。

**签名之前还有一道闸：提交要先过审查（§12）。** 审的是「这段东西算不算他的贡献」，
签的是「这条提交确实出自他手」。两件事都不能省，也不能互相顶替——维护者替你签，
等于把你的作者身份改写成他（R15 会判冒名）。

所以规则按人数切换：

| 情形 | 签名密度 | 依据 |
| --- | --- | --- |
| 只有一个著作权人 | 每次发布签一个 tag | 本节前半 |
| 有两个及以上提交者 | 每个提交者用自己的钥签**自己通过审查的**提交（`git commit -S`） | R15 + §12 |

R15 现在就盯着这件事：凡带签名的提交，签名者 principal 必须是该作者登记过的邮箱之一；
签名验不过（坏签名、钥不在 `allowed_signers`）直接判红。**未签名的提交只统计、不判红**——
历史既成事实，补签要改写提交，毁掉的正是原来的可核验性。

这条规则的实际效果是：新人签一个提交，就锁死一个；锁的是「以后不能再说这是他写的」，
而不是「以前的都作废」。

## 11. 多人协作时的署名与签名

到这里，两套机制各自管什么已经清楚了：

| | 名录（R12/R13/R14） | 签名（R14/R15 + 钩子） |
| --- | --- | --- |
| 回答 | 谁贡献了什么、谁有权发布 | 这个版本、这条提交是不是他本人发出的 |
| 防 | 自己人出错：漏记、虚构、偷删条目、悄悄扩权 | 外人冒充：伪造版本、冒名提交、中间人换包 |
| 失效形态 | 承诺写了但指不到人 | 验签通过但签的不是他 |

**签名不能替代名录，名录不能替代签名。** 单靠签名，一个持钥人可以把名录里所有人删掉、
再签一个「干净」的版本，验签依然 Good；单靠名录，任何人都能宣称自己发了 v3.0。
两者串起来才是完整链条：`签名 tag → commit → author 邮箱 → 名录条目 → 具体的人`。

### 11.1 三类人在协作里各能做什么

| 角色 | 提交权 | 审查 | 署名 | 发布权 | 签什么 |
| --- | --- | --- | --- | --- | --- |
| 著作权贡献者 | 有 | 必须有（§12） | 文件头 + §2 表格 + 提交 author | 默认无，需单独授予 | 自己通过审查的提交 |
| 数据贡献者 | 数据集层面 | 必须有（§12） | 数据集说明 + §3 表格 | 无 | 不需要 |
| 支持者 | 无 | 不需要 | §4 列名（可撤回） | 无 | 不需要 |

**提交权不等于发布权。** 一个能提交的人，能改的是仓库内容；能不能代表项目发版，
是另一件事——那由 `can_release` 与一把属于他自己的钥共同决定。这条边界不是形式：
发布权一旦和提交权绑在一起，任何能提交的人都能签一个「官方 v3.0」，钩子还会放行。

### 11.2 给一个人发布权：四步，缺一步就红

1. **他自己生成一对钥**，私钥不外传、加口令、离线备份。不要把你的私钥给他——
   共用一把钥等于「验得出有人发了版，验不出是谁发的」，署名权在这一刻落空。
2. **`repo/allowed_signers` 加一行**：`<他的邮箱> namespaces="git" ssh-ed25519 AAAA... 注释`。
   principal 必须是他自己的邮箱，不能借用别人的。
3. **改名录两处**：`CREDITS.md` §2 表格那行的第 4 格加 `release`，`credits.json` 给该条
   `"can_release": true`。R13 会比对这两处；只改一处即红。
4. **`CREDITS.md` §10.3 补上他的公钥指纹**（R14 比对 `allowed_signers` 与本节的指纹集合）。

跑 `python evidence/repo_lint.py repo`，R13、R14 必须全绿。四步里任何一步漏掉，
要么是「键在、权不在」（R14 ④ 抓），要么是「权在、键不在」（R14 ④ 抓），
要么是「两份名录不一致」（R13 抓）。

### 11.3 公开历史不许改写（服务端两道闸）

抹掉一个人的署名，最省事的办法不是改名录，是 `rebase` 掉他的提交——名录里还写着他的名字，
git 里已经查无此提交。R12 ③ 能查「依据指向的提交是否还存在」，但那是**事后**发现，
而且能改仓库的人可以顺手把名录一起改了。

所以真正的防线在推送口，两条，都实测过：

| 闸 | 位置 | 拦什么 | 实测 |
| --- | --- | --- | --- |
| `receive.denyNonFastForwards = true` | 裸仓配置 | 分支强推、`push -f`、`rebase` 后强推 | `! [remote rejected] test-ff~2 -> test-ff (non-fast-forward)` |
| `hooks/update` | 裸仓 `hooks/` | **移动已存在的 tag** | `REFUSED: 拒绝移动已存在的 tag` |

第二条必须单独写，因为它推翻了一个看起来足够的假设：**`denyNonFastForwards` 拦不住 tag 更新。**
实测把一个已有 tag 用 `push -f` 指到别的提交，服务端输出
`+ 02a229b...b7dc962 (forced update)` 并接受——理由是 git 认为 tag 本来就不存在快进关系，
「非快进」这条判断对 tag 不成立。而移动 tag 是最隐蔽的一种改写：`git log` 看不出异常，
已下载的包与 tag 悄悄脱钩，包本身的 `sha256` 却仍然通过，**没有人会察觉**。

这个空洞只能由服务端钩子补：新建 tag 放行、删除 tag 放行、**改指向一律拒绝**。
删除之所以放行，是因为作废一个 tag 应该留下可见痕迹（tag 消失 + `CHANGELOG` 记一条），
可见的错误优于不可见的正确。

**这两道闸只在自建裸仓上成立。** 第三方托管（GitHub 等）没有 `hooks/update`，可用的只有平台
自带的 tag 保护规则，且部分套餐不提供。所以把仓库镜像出去时，必须在那一边重新确认一遍：
**哪一边漏了，哪一边就是改写历史的入口。** 发布口仍以自建裸仓为准，镜像只作公开可读副本。

2026-11-12 对 GitHub 做过实证：规则集覆盖 `v2.1-*` 与 `v2.0-final`，勾齐
`Restrict deletions` + `Restrict updates`（+ `Block force pushes`）后，删除与强移均被拒
（三次推送实测）；只勾前两条时**强移会被放行**——对 tag 而言，拦移动的是 `Restrict updates`，
不是 `Block force pushes`。但规则本身可被账号持有者关闭或改动，故此实证只证明"当天拦得住"，
不改变"镜像站不作为信任来源"这一条。

代价要说明白：**代价是「不能后悔」。** 提交里混进了密钥、把别人的名字写错了、
某个 tag 打歪了——都不能靠改写历史抹掉，只能追加一个提交去更正，错误本身留在历史里。
这是有意的：历史可改写，署名就不可主张；两者只能选一个。

真需要例外时（例如撤销一个已经泄露口令的 tag），做法是**删除 tag 并在 `CHANGELOG` 记明**
「某 tag 因某原因作废」，而不是悄悄移动到另一个提交上。删除是可见的，移动是不可见的。

### 11.4 人退出、改名、要求匿名

| 情形 | 做法 |
| --- | --- |
| 改名或改署名 | 加 `aka`，旧名保留在 §5 与历史快照里，不删——删了就断开了「这条旧提交属于谁」。 |
| 要求本名不公开 | 保留笔名条目与全部依据，`aka` 里不记本名，只记「已核验、本人要求不公开」。这与上一行不冲突：撤的是**身份信息**，不是**署名**——人在名录里，依据在，只是不把笔名和身份证上的名字公开对上号。 |
| 要求匿名或不列名 | 尊重。署名权是人身权，本人可以决定署什么名、也可以不署；请其指定一个替代标识（笔名，或「匿名贡献者」加一个 `id`），否则同一份文件上其他人的署名记载会失去参照。**现行条目**按 §7 处理（本人书面要求可删），**已发布版本改不动**——`v2.1-rc3` 的包里仍是当初那个名字，这不是拒绝，是技术上做不到。 |
| 退出项目 | 名录条目保留（历史贡献不因离开而消失），`can_release` 置否、`allowed_signers` 删钥、`CHANGELOG` 记一条。 |
| 主张某条提交不是他写的 | 优先信提交签名；没有签名时，以当时的工作记录与第三方可见的往来为准，并在 `CHANGELOG` 记明争议与结论。**不悄悄删条目。** |

### 11.5 这套机制防不住什么（写在明面上）

1. **同时改两处的人。** 能改仓库的人可以同时改 `allowed_signers` 和 §10.3 的指纹，
   R14 会通过。信任根只能在仓库外（§10.3 的两条路，均未落地）。
2. **拿到部署钥的人。** 他能往服务器推任何东西，钩子拦不住——钩子拦的是手滑，
   不是有权者。这条边界靠密钥管理，不靠代码。
3. **从未签过名的人。** 一个人若始终不签名，他的提交永远只有自称的 author；
   名录能证明「这个名字登记过」，不能证明「这条提交出自他手」。R15 只能锁住签过名的部分。

这三条都不打算用代码假装解决。写在文件里的好处是：别人评估这个项目时，
能看见边界在哪，而不是只能相信「有签名，所以应该没问题」。

## 12. 贡献审查：审过才进名录

2026-11-11 的决定：**不逐条签 commit**（理由见 §10.6），但也不接受「提交了就算贡献」。
两种极端都不对——前者把每次改错别字变成一次密钥操作，后者让名录退化成提交列表，
垃圾提交也能主张署名权。中间那一层是审查记录：`REVIEWS.md`（人读）+ `reviews.json`（机读），
由 **R16** 强制与 git、与名录对齐。

顺序是唯一的：

```
提交 → 审查（§12） → 进 §2 依据 → 作者本人签 → R15 锁身份
```

### 12.1 四条判据（能不能落成「是/否」）

| # | 判据 | 不满足的典型 |
| --- | --- | --- |
| ① | 改动可指认且非机械 | 只动空白、行尾、文件排序、全仓替换 |
| ② | 作者能解释「为什么这么改」 | 整段贴入、说不清来源的代码 |
| ③ | 可复现：干净 clone 里判据仍全绿 | 依赖只有作者本机才有的路径、口令、服务 |
| ④ | 边界守住：许可头/伴生文件齐，引用标明来源 | 把别人的代码写进自己名下 |

四条全满足记 `accept`；任一条不满足记 `reject` 并写明卡在哪条。**`reject` 也是记录**，
它防的是「过几个月有人重提一遍，大家从头吵」。

### 12.2 维护者不得代签

这条放在最前面，因为它是最容易犯的错。签名在 git 里绑的是**作者身份**，不绑背书关系：
别人签你的提交，`verify-commit` 报 `Good signature for 别人`——读起来像背书，
技术上却是「这条提交的作者是别人」。R15 抓的就是它。

所以「审核确实有贡献之后再签名」只能是这个意思：**审核决定它进不进名录，签名由作者自己做。**
维护者能做的是审、是记、是发版时签 tag；不能做的是替别人签 commit。

### 12.3 基线：为什么用「范围」不逐条列举

`reviews.json` 的 `baseline` 指向 `638f157`（含）之前 28 条提交，作者全部是唯一著作权人本人
（两个维护者账号按 `accounts` 折算到同一人）。不逐条补登的理由有三：

1. 没有争议时写记录，是把成本花在不存在的问题上；
2. 记录一多，人就不看了——28 行「自己审自己」会让第一条真正的外部记录淹没；
3. 基线可以指认：`git rev-list 638f157` 就是它覆盖的清单，不依赖文档措辞。

基线之后换成硬约束：**作者不是有发布权的人，提交就必须有审查记录**（R16 ⑥）。

### 12.4 R16 查的六件事

| 检查 | 防的错 |
| --- | --- |
| 审阅人有发布权 | 没被这套机制认过的人给别人的贡献作保 |
| 记录指向的提交真实存在、前缀唯一 | 记录指向不存在的提交，等于凭空的依据 |
| 不得重审基线已覆盖的提交 | 让基线形同虚设 |
| `accept` 必须进 §2 的依据 | **审了不记**：口头认了，名录里查无此事 |
| 基线之后的外部提交必须有记录 | **不审就记**：把提交一律算成贡献 |
| `REVIEWS.md` ↔ `reviews.json` 不分叉 | 只改一处，承诺指向另一份 |

### 12.5 审查不证明什么

- 不担保质量（审的是「有没有贡献」，不是「好不好」）——质量由 `REPRODUCE.md` 的判据与后续修改承担；
- 不转移著作权（著作权自创作完成即归作者，审查记录不改变归属）；
- 不替代 `DCO`（DCO 是作者声明「我有权提交」，审查是维护者确认「有贡献」，两件事）；
- 也不防审阅人自己造假——那只能靠名录与历史的公开性：改记录要在 `CHANGELOG` 留痕，
  改历史被服务端两道闸拦住（§11.3）。

### 12.6 现在是什么状态

`reviews.json` 的 `reviews` 数组是空的，`REVIEWS.md` 只有一行基线。**这不是占位，是事实：
至今没有第三方提交。** 第一条记录要等第一个人——那时它才有意义，也才有必要被写成一份记录。

