# AGENTS.md · 硬性约定

给在本仓工作的人（和 AI）的硬约定。**违反其中任何一条，改动即视为未完成**——
不是「最好这样」，是「不这样就得回退」。

## 一、不许把「跳过」当「通过」

- 判定为跳过（缺上游、缺 cargo、缺基线）的阶段，输出里必须写「跳过不是绿」，
  且**不得**把跳过计成通过。
- 「一片绿里夹着一行报错」是本仓最忌讳的形态：它让人分不清是环境没配好还是代码坏了。
  所以缺件要么显式红，要么显式跳过并说明——不许静默。
- **三种跳过前缀，三档含义**（`gates.sh` 只认行首这三种）：
  `>> ⏭` 整段没量；`>> ~` **量了但有缺口**（子脚本说明缺哪一条）；`>> ✅` 全判。
  后两档都**不算绿**。把「一部分没量」写成 `>> ⏭` 会让报告说「这一段什么都没量」，
  而实际量了大半——读数就少了一半。
- **⚠️ 子脚本的退出码不许被管道或后续命令吞掉。** 本轮抓到两处假绿：
  `gates.sh` 用 `out="$(eval "$cmd" | tee)"` 读 `$?`（读的是 `tee` 的），
  `upstream-ledger.sh` 在 `rc=$?` 之后又跑了一句 `rm -f` 才 `exit $rc` 写成 `exit`。
  两处的后果一样：**判据红了，门禁显示 ✅**。所以 ——
  输出先落临时文件再读；`rc=$?` 必须紧跟被判的命令；脚本末尾显式 `exit "$rc"`。
  验收方式：故意把某个阶段弄红，`gates.sh` 必须返回非 0。
- **⚠️ 「跳过」的说明不许写进生成物。** 本轮抓到第二例假绿：`upstream-ledger --verify`
  把「同门仓缺席」那句话 `printf` 进表体，于是①下一轮同门仓在场时它混进现算侧、
  红在一句关于环境的话上；②缺席时两侧都带一行、**表体真漂了也判相等**。
  同样的形态还有一面：**扫描面必须剪掉 `_build`**（`moon` 在那里放一份格式化副本，
  `grep -r .` 会把每条用例数两遍）。五处修正与四条「不做」的裁决在
  [`P0 §6`](./docs/05-总览/P0-现状与结构.md#6-七轮复查修正与刻意不做)。

## 二、实现面的四条

1. **边界在 `lib/`。** 模块根的 `moon.pkg` 只做 README 文档测试宿主，不写库代码。
   库内只有一个实现包 `lib/path`（数值 + 几何 + 路径 + 描边 + 虚线）；`lib` 只做门面。
   （原 `lib/internal/num` 已并回：上游 `scalar.rs` / `floating_point.rs` 本来就在 `path/src` 内。）
   **不建 `lib/internal/geom`**：`Path::bounds` 返回 `Rect`，拆包会让消费者拿到
   「看得见类型、调不到方法」的 `Rect`——MoonBit 的方法只能与类型同包。
   依据与实测见 [`P1 §2.2`](./docs/05-总览/P1-移植与对拍.md#22-三处接缝逐条给判据)，
   文件级的归属表见 [`P0 §3.1`](./docs/05-总览/P0-现状与结构.md#31-文件归属新代码往哪放)。
2. **对外只从 `lib/` 这一层出。** 值由 `lib/facade.mbt` 转发，类型由 `lib/rexport.mbt`
   的 `pub using @path { type ... }` 导出。**`pub using` 只带类型名、不带方法**，
   所以方法形 API 的出口有两条：消费者直接 `import lib/path`，或门面补自由函数转发
   （`pub fn path_bounds(p : Path) -> Rect`）。给外来类型补 `pub fn X::m` 编译不过。
3. **数值语义对齐上游，结构不照抄。** MoonBit 没有生命周期与零成本借用，
   照抄 Rust 的借用形态只会得到一份「编译得过、语义不对」的实现。
4. **不做「看起来等价」的替换。** `f32` 就是 `Float`，不许用 `Double` 顶替；
   定点量纲（fdot6/fdot16）必须显式标注。同门仓的搬运约定是「用 `Double` 装 f32 值 +
   手工落回」，**照抄那条约定就是违反本条**：翻 `Float` 时删掉 `q32*` 系列。
   判据是 `bash scripts/float-gate.sh`（门禁 `float-gate` 段）：逐文件
   `Double\|to_double\|from_double` 计数为 **0**，例外必须在该脚本的 `EXCEPTIONS` 里
   逐条写理由（在册 **8 个文件**；清单与理由只认脚本里的表，别在这里抄第二份：
   `Int→Float` 的唯一写法、`@math` 的三个 f64 出口、`saturate_from_f64` 的入参、
   `dash` 的两处整数装箱、`transform` 的三角函数与求逆 / 拼接、两处 f64 判别式、
   `conic` 的两处距离、`stroker_join` 的模长）。
   为什么不靠 `--deny-warn`：`Double` 与 `Float` 混算是**隐式提升**，编译器一声不响，
   而漏掉的那一处精度更高，只在边界差最后一位——这类问题只能靠一条独立的计数判据。
5. **搬上游算式时括号一起搬。** 上游的 `times_2(x)` / `3.0 * (…)` 一旦手展开成
   `- p1 + p1` 这种「看起来能自己抵消」的写法，就会丢那一步的舍入。本轮因此查出五个真 bug
   （[`P1 §6.2`](./docs/05-总览/P1-移植与对拍.md#62-二十七条明细)），
   全部由上游字面量对拍抓到。

## 三、后端承诺

- `supported_targets = "+wasm+wasm-gc"`，`preferred_target = "wasm-gc"`。
- **不纳入** `native`（需系统 C 编译器）与 `js`。加后端 = 改 `moon.mod` +
  改 `scripts/backend-parity.sh` 的期望集合，两处一起改，缺一处判据就失明。

## 四、文档纪律

1. **两篇正文，其余是指针。** `docs/` 只有 `docs/README.md`（导航面）、
   [`P0 · 现状与结构`](./docs/05-总览/P0-现状与结构.md)（现状 / 结构 / 评审）与
   [`P1 · 移植与对拍`](./docs/05-总览/P1-移植与对拍.md)（范围 / 搬法 / 真值 / 坑）。
   被合并的篇目不删文件，降为指针放 `archive/`（`archive/` 自己的 `README.md` 是区说明，同样不作口径）。
2. **同一句结论只写一份**，另一处必须是指针——门禁拦得住死链，拦不住两份副本互相漂。
3. **生成物禁止手改**：文档清单由 `scripts/docs-index.sh --write` 维护，
   长度表由 `scripts/docs-length.sh --write` 维护。
4. **`README` 的 `mbt check` 代码块就是文档测试**：改公共 API 必须同 PR 改 `README`。
5. **新增文件之前先读 [`P0 §3.1`](./docs/05-总览/P0-现状与结构.md#31-文件归属新代码往哪放)**：
   文件按**上游 module** 分，一个 module 一个文件；文件内一个类型一段，不按类型数拆、
   也不按行数拆。一个类型的所有 `pub fn` 必须与类型同文件
   （[`P0 §3.4`](./docs/05-总览/P0-现状与结构.md#34-一个类型的公开面只有一个家)）。
6. **文件内也要有段序**：一个类型一段（公开面连续，不许被另一个类型劈开）、段内先公开面
   后私有助手、外来类型的 `pub fn` 回它自己的文件。反例与本轮的 14 条搬动见
   [`P0 §3.4`](./docs/05-总览/P0-现状与结构.md#34-一个类型的公开面只有一个家)。
7. **测试按可见性分两处，不按口味分。** 黑盒用例（只经公开面）放 `tests/`；
   白盒用例（要吃 `priv` 助手）只能放 `lib/path/*_wbtest.mbt`——MoonBit 没有
   `pub(crate)`，`for "test"` 的 import 在 `_wbtest.mbt` 里也不可见。
   **不要为了让测试留在 `tests/` 把内部助手提成 `pub`**：那是拿 API 换目录。

## 五、真值只从上游来

1. **测试里的数字要么是上游原文，要么是「跑上游」跑出来的。** 不许拿本仓输出当基线
   （那就是自证）。上游 `mod tests` 里写下的字面量是最硬的一档，优先抄它。
2. **不设容差的地方就别设。** 上游也是 `f32`、本仓也是 `Float`，所以断言写逐位相等；
   为「更宽的浮点」留的余量（如 `2e-3`）在这里只会让判据变松（`AGENTS.md §二.4`）。
3. **真值不在本仓落盘。** 需要真值时跑 `bash scripts/upstream-probe.sh`（L3），
   上游输出只进测试断言、不进仓库文件；`tests/baselines/` 出现时必须带 `.provenance`。
4. **否定式判据也算判据**，而且往往是最便宜的那一类：`Some`/`None`、拒收面 8 条、
   退化输入不 panic——这些一条都不需要精度，却挡得住「死循环 / 除零」这种最坏的失败。

## 六、注释与死代码

- `<id>` 形态的包级告警豁免只准豁免「工具约定」类告警，且必须在
  `scripts/check.sh` 的 `EXEMPT_ALLOWED` 里登记理由。豁免面只增不减 = 失明。
- 注释讲「为什么」，不复述代码。写「别那么干」的理由，比写「这里干了什么」有价值。

## 七、README 与 docs 的分工

仓库根 `README.md` 是**门面**（这是什么、怎么用、目录在哪），面向第一次来的人；
`docs/` 是**口径**（哪条结论用什么命令量出来的），面向要改它的人。
同一段解释不抄两份——门面写「去哪找」，口径写「是什么」。

## 八、提交与门禁

- 提交前跑 `bash scripts/gates.sh`（至少 `STAGES="toolchain-check fmt-check check test"`）。
  门禁报「跳过 N」时，先把缺的上游/同门仓检出来再跑一遍——**0 跳过才算看过这条链**。
- 脚本改的是宿主机还是仓库，要分清楚：`setup-*.sh` 改宿主机、**不进 gates**；
  `*-check.sh` 判仓库、**进 gates**。
