# checkdigit 开发回顾

本文对应 **2026 MoonBit 九月黑客松**「验收标准」中「可解释」一条:可以使用 AI 辅助,
但目标、路径与质量必须由参赛者掌握。按该条的点名要求组织:**关键架构决策**、
**AI 工具在开发中的作用**、**对现有开源工作的引用**。

文中所有数字都是可复核的,复核命令写在每节末尾。

---

## 1. 项目与范围

checkdigit 是一个纯 MoonBit 的校验位算法与标识符校验库,只做一件事:判断一个字符串
是否符合某项标识符标准,并在需要时把其中的字段拆出来。

范围边界是刻意划窄的:

- **只管单个标识符字符串内部的算术校验位。** 不解析 JSON,不定义 schema,不做网络或文件 I/O。
- **全部对外函数是全函数。** 输入不合法返回 `false` 或 `None`,不抛异常、不 `panic`、不 `abort`。
- **零外部依赖。** `moon.mod` 没有 `deps` 段,只用 `moonbitlang/core`。

规模(实测):

| 指标 | 数值 |
|---|---|
| 源码行数(库,不含 `cmd` 与测试) | 2 078 |
| 测试行数 | 1 171(3 个测试文件) |
| 合计行数 | 3 249 |
| 算法模块 | 18 |
| 支持的标识符 | 21 种,跨国家标准与国际标准 |
| 测试用例 | 97 |
| 外部依赖 | 0 |

复核:`moon test`;行数用 `find . -name '*.mbt'` 配合 `wc -l` 统计,库根目录、不含 `cmd`
与测试文件。

---

## 2. 关键架构决策

### 决策一:两层结构 —— 算法层与标识符层分离

`luhn` / `verhoeff` / `damm` 三个模块**不知道任何具体标识符的存在**。它们只接受一个
数字载荷字符串,返回校验结果或校验位。标识符层(银行卡、IBAN、VIN……)各自调用算法层。

为什么不把 Luhn 直接写进 `card.mbt`?因为同一个算法会被多个标识符复用:ISIN 用的是
Luhn,但它的字符要先做字母展开;IMEI 用的也是 Luhn,但载荷是 14 位定长。如果算法绑在
标识符里,这三处就会出现三份 Luhn 实现。分离之后,算法层是唯一事实源,新增标识符时
不需要碰算法代码。

代价:算法层的接口必须足够窄(只收数字载荷),所以像 ISIN 的字母展开、USCC 的 31 进制
码集这类转换,只能放在标识符层。这是有意的分工。

### 决策二:校验位用查表,不用算术推导

`cnid.mbt` 的权重和校验码是两个字面量数组:

```moonbit
let cn_id_weights : Array[Int] = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
let cn_id_codes : Array[Char] = ['1','0','X','9','8','7','6','5','4','3','2']
```

这些值理论上可以用 `2^k mod 11` 算出来,但标准把它们**固定成一张表**。用字面量表有两个
好处:一是审查时可以直接和标准文本逐位对照,不需要验证推导式;二是避免"我算对了但标准
用的是另一个下标起点"这类静默错误。同样的取舍出现在 Verhoeff(`d`、`p`、`inv` 三张表)、
Damm(全表)、BSN(权重)里。

代价:表占行数,而且拼错一个数字不会被编译器发现。这是靠测试向量兜住的 ——
本项目用公开标准样例(如 GB 11643 的 `11010519491231002X`)做参考向量,而不是自己造输入。

### 决策三:错误信号用 `Bool` / `Option`,不用异常

`x_valid(s) -> Bool` 表示"是否符合标准",`x_check_digit(prefix) -> Int?` 表示"算不出来"
和"算出来是某个值"是两种不同结果。`None` 一律表示**输入形态不对**(长度错、含非数字字符),
而不是"校验失败"。

为什么不用 Result 携带错误原因?因为调用方是表单校验和批量清洗,它们只需要一个布尔判断;
诊断信息在批量场景下没有位置放,还会诱导调用方去 `unwrap` 错误分支。把"为什么不合格"
留给调用方自己按需调 `x_region` / `x_birthdate` 这类提取函数。

### 决策四:提取函数以"整体有效"为前提

`cn_id_region` 和 `cn_id_birthdate` 都先调 `cn_id_valid`,不有效就返回 `None`。也就是说
提取函数不会从一个校验和都不对的串里"抠"出字段。

这是有意的:一个校验位错误的身份证号,它的行政区划字段并没有可信度,返回它会让调用方
误以为拿到了正确数据。反过来,如果调用方只想要字段、不在乎校验,那种需求应该显式说明,
而不是靠提取函数偷偷放宽。

### 决策五:字符处理不依赖 Unicode 大小写

`upper_ascii` 是一个 26 分支的 `match`,只认 `a-z`,其余字符原样返回。不使用语言内置的
大小写转换。

原因:这些标识符标准的字母表都是 ASCII 的。如果把全角数字 `１２３` 或别的 Unicode 字符
也卷进大小写转换,就可能出现"某个非 ASCII 字符转换后意外落进合法字符集"的情况。现在的
写法配合 `is_digit` / `is_alpha` 的显式 ASCII 范围判断,能保证非 ASCII 输入一定被拒。
对抗性测试里专门放了两组 Unicode 数字来钉住这个行为。

### 决策六:长度与边界显式检查,不用正则

MoonBit 侧没有引入正则,所有长度判断都是 `cs.length() != 18` 这种直白比较。Go 侧的命令行
包装也保持同样的直白风格。这样每个函数的前置条件在源码上一眼可见,审查成本最低。

### 决策七:命令行工具与库共用同一套函数

`cmd/main` 只做参数解析和输出排版,校验逻辑全部调用库里的 `pub fn`。因此 CLI 不存在
"另一份实现",也就不会出现库和 CLI 行为不一致的问题。CLI 提供 20 个子命令,与库里的
校验器一一对应。

### 决策八:`moon.pkg` 与依赖面保持最小

`moon.pkg` 只有一行 `warnings = "-deprecated_syntax"`。模块之所以能同时编译到
native / wasm / wasm-gc,正是因为**没有引入任何带平台假设的依赖** —— 没有文件系统、
没有网络、没有时钟。纯字符串进出让这三个目标共享同一份代码,不需要条件编译。

### 决策九:文档与代码同源

`README.mbt.md` 被 `moon.mod` 指定为 `readme`,它和 `README.md` 内容一致:前者供
mooncakes.io 渲染,后者供 GitHub 阅读。文档里的用法示例直接取自测试里验证过的调用方式。

### 决策十:测试分黑白盒两层

`checkdigit_test.mbt` 只调用 `pub fn`,模拟外部调用方;`checkdigit_wbtest.mbt` 直接测试
包内私有辅助函数(`substring`、`digits_of`、各张权重表)。分层的作用是让"公开契约"
和"内部实现细节"各自有独立的回归网 —— 私有函数可以重构,只要黑盒测试不动就说明契约没破。

---

## 3. AI 工具在开发中的作用

项目采用了 AI 原生的工作方式,但**职责划分是有意设计的**:AI 负责把已经确定下来的规格
翻译成代码、补测试、补齐文档骨架;作者负责决定架构、划定范围、审阅每一处取舍,并对
"这条标准到底怎么写"负最终责任。

具体而言,AI 参与并产生实质帮助的环节:

- **按标准逐条落地实现。** 把 GB 11643、ISO 13616、ISO 7064 这类文本规格转成 MoonBit
  函数与常量表,是最耗时的机械劳动。查表值抄错一个数字,人眼很难发现。
- **补参考向量。** 测试向量不是凭记忆写的,而是**用一个独立实现交叉验证**得到的:
  例如 cnid 的权重与校验码表,先用另一份独立实现算出期望值,并拿公开标准样例
  (`11010519491231002X`,加权和余数为 2,对应校验字符 X)做自校验,确认两份实现一致后
  才写进测试。这样避免"用被测代码生成自己的期望值"这种循环论证。
- **对抗性测试矩阵。** 用 23 种恶意输入(空串、纯空白、全角数字、阿拉伯-印度数字、
  超长串、内嵌空格、尾部 NUL 等)横穿 50 个公开函数调用点,验证"全函数、不 panic"这条承诺。
  这类穷举由 AI 生成性价比很高,人工写容易漏掉组合。
- **文档骨架与措辞统一。**

作者决策并负责的环节:两层架构、错误信号选型、提取函数的前置条件、字符集处理策略、
范围边界、以及本文所述的每一项取舍。

一条边界:**AI 生成的代码必须过测试与 `moon check` 才能进入仓库**,没有例外。仓库的
commit 历史保持线性、可读,每条信息对应一次真实的功能、测试或文档改动 ——
评审要求"完整提交历史",这里没有为了凑数量而拆分的空提交。

---

## 4. 对现有开源工作的引用

### 标准依据

实现的依据是公开标准文本,不是任何现有库的代码:

| 标准 | 用途 |
|---|---|
| ISO/IEC 7812 | 银行卡标识与 Luhn 校验 |
| ISO 13616 | IBAN |
| ISO 7064 MOD 11-2 | 身份证号校验位 |
| ISO 3779 | VIN |
| ISO 6166 | ISIN |
| ISO 17442 | LEI |
| GB 11643 | 中国居民身份证号 |
| GB 32100 | 统一社会信用代码 |
| 3GPP TS 23.003 | IMEI |
| RFC 4122 之外 — CPF/CNPJ | 巴西双 mod 11 |
| — | 荷兰 BSN 11 检验、印度 Aadhaar(Verhoeff)、意大利 codice fiscale |

Luhn、Verhoeff、Damm 三者都是公开算法,代码照算法定义独立编写,未移植或复制任何现有库。

### MoonBit 生态中的相邻项目

本库与以下项目是**互补而非重复**关系:

- `moon_zod` / `moonschema` / `jsonschema` —— JSON 与数据结构的 schema 校验,管字段类型和
  email、uuid 之类格式约束,不做算术校验位运算。
- `moovalid` / `maru` —— 校验组合子与错误累积框架。
- `mooncontract` —— OpenAPI 接口契约校验。
- `moonmrz` —— 面向护照/签证 ICAO 9303 机读区,用 7-3-1 加权方案。

在 mooncakes.io 上以 luhn、iban、imei、aadhaar、vin、isin、lei、uscc、cpf、cnpj、bsn、
codice fiscale 检索,目前无任何模块命中。

---

## 5. 已知限制

明确写出来,避免被当成缺陷重复报告:

1. **身份证出生日期只做浅层合理性检查。** 月份 1-12、日 1-31、年 1800-2199,但**不建模
   月份长度和闰年**,因此 `19490231`(2 月 31 日)在校验位吻合时会被接受。这是有意的
   取舍:完整实现需要引入日期库,而本库承诺零依赖。该行为已被测试固定
   (见 `cn_id accepts dates that are plausible but impossible`)。
2. **`native` 目标需要系统 C 编译器。** 纯 MoonBit 代码本身可编译到 native,但链接阶段
   依赖 cl/clang/gcc。CI 的 `build-matrix` 在 ubuntu-latest 上提供 gcc,因此该目标在 CI 中
   被真实验证;本地若无 C 编译器,`moon build --target native` 会失败并提示
   `no system C compiler found`。这不是代码缺陷。
3. **校验位算法只能发现形式错误,不能证明标识符真实存在。** 一个通过了 Luhn 的卡号仍然
   可能是未发行的号段。本库不查号段数据库。
4. **IBAN 长度表按国家静态维护。** 新增国家或长度调整需要更新表。

---

## 6. 验收标准对照

对照 2026 MoonBit 九月黑客松官方「验收标准」逐条自查:

| 验收条目 | 官方要求 | 本仓库现状 | 证据 |
|---|---|---|---|
| 主要语言 | 以 MoonBit 作为项目主要实现语言 | 全部源码为 MoonBit,零外部依赖 | `moon.mod`、`*.mbt` |
| 仓库公开 | 保留连续、可追踪的提交与开发记录 | 公开仓库,线性提交历史,每条对应一次真实的功能/测试/文档改动 | `git log` |
| 能够运行 | 清晰 README、可运行示例与必要测试 | README + `cmd/demo` 端到端演示 + 三层测试 | `docs/reproducible-demo.md` |
| 工作有效 | 已有项目须包含本期完成的实质新增工作 | 仓库整体建于本期(2026-09-12 起),本期持续新增 | `CHANGELOG.md` |
| 开源合规 | 认可的开源许可证,并说明移植或参考来源 | Apache-2.0;README 列出标准依据与相邻项目边界 | `LICENSE`、`README.md` |
| 可解释 | 可用 AI 辅助,但目标、路径与质量须由参赛者掌握 | 本文说明架构决策与 AI 分工 | 本文 |

**关于规模:** 官方 FAQ 明确「项目必须做得很大吗?——不必。比起堆叠功能,我们更看重清晰边界、
可靠测试、完整文档和真实可用性。」因此本项目不追求行数,而是把评审关注的四项做实:
2 078 行源码 + 1 171 行测试,21 种标识符共用 3 个算法模块而非各写一份 —— 这是有意抑制
重复代码的结果,不是未完成项。
