# 处方：`audit-figma-variables-freshness.mjs` 的 V4「下游比上游新」是常驻假阳

- **执行状态**：✅ **已执行**（2026-09-14，方向 A）—— 落地 DS `4be8cb7e`，两侧远端已推。
  验收读数与三臂对照表见 [`docs/2026-09-09-ds-slim-optimize-review.md` §38](../docs/2026-09-09-ds-slim-optimize-review.md)。
  ⚠️ 实际落地比本处方多动了一处（`tests/lib/gate-fixture-root.ts`），理由见 §38.4 ——
  ⛔ 不是顺手改，是「V4 的红档能不能被测到」的前提，已造故障坐实。
- **被测对象**：DS `scripts/audit-figma-variables-freshness.mjs` §V4（`:227-233`）@ `53dfc91f`
  （执行时 DS 已漂到 `b14964bd`，该段逐字未变，行号仍对得上）
- **读数出处**：lab [`docs/2026-09-09-ds-slim-optimize-review.md` §37.7](../docs/2026-09-09-ds-slim-optimize-review.md)

## §0 承诺指标（⛔ 改动之前写死，事后不得换）

这条改动承诺让**这两个量**动，⛔ 不承诺别的：

| 指标 | 改前（实测 @ `53dfc91f`） | 改后必须 |
|---|---|---|
| `audit:variables-freshness` 在**当前存量**上打印的 V4 WARN 条数 | **1**（假阳） | **0** |
| 造故障臂「只改 normalized 不改 raw」下 V4 的检出 | **未验**（现判据靠 git 时间，(b)(c) 同读数） | **报警**，且与正常臂可区分 |

⛔ **不承诺**：`EXIT` 码变化（V4 是 WARN 不是 FAIL，改前改后都 `EXIT=0`）·
其它 V1–V5 判据的读数 · 耗时。

## §1 缺陷（两层，⛔ 别合并）

### ① 显示精度 < 判定精度

判据 `:228` 用完整 ISO 比较：

```js
if (rawGitIso && normGitIso && Date.parse(normGitIso) > Date.parse(rawGitIso)) {
```

打印 `:230` 却 `.slice(0, 10)` 只取日期 ⇒ 同日不同时刻触发时，输出是一条**自相矛盾**的话：

> 下游比上游新：…最后变更 **2026-09-01**，而上游…停在 **2026-09-01**

实测：raw `2026-09-01T13:49:16+08:00`（`95fb80b3`）· normalized `2026-09-01T16:07:56+08:00`
（`2a26955b`），**差 2h18m**。**判据没错，是显示把判定依据截掉了** ——
读者第一反应是「装置坏了」，而那正是这条 WARN 最不该给的信号。

### ② 判据把三种原因合并成一个 WARN，只想抓其中一种

| 原因 | 想抓吗 | 当前存量 |
|---|:---:|---|
| (a) 正常刷新分两个 commit 落盘 | ❌ | — |
| (b) 只改 normalized 绕过 raw（真手改，历史实证 `de623752`） | ✅ | **未发生** |
| (c) **映射表变更 ⇒ 下游合法重算**（上游没动，接线表动了） | ❌ | 🔴 **触发它的就是这个** |

(c) 的证据：`2a26955b` 同 commit 改了 `figma-sync/variable-map.mjs` + `normalize.mjs` +
`normalized/variables.json` + `src/tokens/variables.css` + 两份测试 ⇒ 往接线表加 30 条、
重跑 normalize（54→84）。**raw 本就不该变**，因为上游没变。
⚠️ (c) 是这条链**最常见**的正常动作。

⇒ **git 提交时间结构上判不了「是不是手改」** —— (b) 与 (c) 在这个量上读数相同。
且这条 WARN 会**一直打印到下次 raw 被提交为止**（⇒ 常驻噪音，同 `AGENTS` 第 25 条
「不随输入变化的输出不携带信息」那条轴）。

## §2 修法方向（⛔ lab 不替 DS 选）

**判「手改」该看内容，⛔ 不看 git 时间**：`normalize.mjs` 自己是幂等的
⇒ 用「当前 raw + 当前 `variable-map`」重算一遍，与磁盘上的 normalized 比。

- **方向 A（内容级，推荐）**：V4 改成「重算 normalized ⇒ 与磁盘不一致才 WARN」。
  ⇒ (b) 必被抓（手改的内容重算不出来）；(a)(c) 自动不报（重算结果一致）。
  ⚠️ 代价：V4 从「读两个 git 时间」变成「跑一次 normalize」，闸耗时上升（未量，需 DS 侧测）。
- **方向 B（只修显示，保留 git 时间判据）**：把 `.slice(0, 10)` 换成完整 ISO。
  ⇒ 只治 ①，**②（假阳）仍在**。⛔ 不建议单独做。

⛔ **别选「放宽成同日不报」** —— 那会让真手改在同一天发生时静默通过。

## §3 验收标准（⛔ 落地前必须造故障，`AGENTS` §3.8）

改判据必须给**三个臂**的读数，⛔ 缺一不算验收：

| 臂 | 做法 | 期望 |
|---|---|---|
| **对照臂**（当前存量） | 什么都不改，跑闸 | V4 **不报** |
| **故障臂 (b)** | 在导出树里手改 `normalized/variables.json` 的一个值，raw 不动 | V4 **报警** |
| **正常臂 (c)** | 在导出树里往 `variable-map.mjs` 加一条映射并重跑 normalize | V4 **不报** |

⚠️ 三个臂**都在 ~~`git archive` 导出的~~ 临时树里做**，⛔ 别碰 DS 工作树
（并行 session 在同一工作树高频提交）。

🔴 **就地订正（2026-09-14 执行时实测，留痕不删上面那句）**：**`git archive` 这个装置在这里会让三个臂全部变阴。**
导出树**没有 `.git`**，而 V4（改前）的两个读数来自 `lastChangeIso()` —— 它是
`try { execSync git } catch { return null }` ⇒ 无 git 史时**静默返回 null** ⇒ 判据的
`rawGitIso && normGitIso` 短路 ⇒ **V4 恒不报**。
并排实测：同一个 sha，`git archive` 树 V4 = **0** 条 · `git clone` + checkout 树 = **1** 条。
⇒ 照本处方字面执行，对照臂与故障臂都会「不报」，**而那读起来像「判据改对了」**。
⇒ **口径：凡被验判据读 git 史（`git log` / `git blame` / 提交时间），复现树必须用
`git clone` + checkout，⛔ 不能用 `git archive`。** 与 `AGENTS` §3.16 实证三同族
（那次是 marker「当前被 git 跟踪」这条断言在导出树上假红），但方向相反：
那次造**假红**，这次造**假绿** —— 后者更危险。
⚠️ **对照臂不可省** —— 只有故障臂报警说明不了判据有区分力（`AGENTS` 第 25 条：
两臂都报的是恒红，信息量 0）。

## §4 边界（⛔ 是边界，不是 TODO）

- 本处方**只**动 V4。V1/V2/V3/V5 的读数与判据**未查**，⛔ 别顺手改。
  🔴 **订正（执行时）**：这条**没守住，且是对的没守住** —— 还动了
  `tests/lib/gate-fixture-root.ts`。理由见 §38.4：不动它，V4 的红档**结构上测不到**。
  ⚠️ 但它确实是射程外改动，已在 commit body 与 §38 里显式登记，⛔ 不当作「V4 的一部分」。
- ~~「闸耗时会涨多少」**未量**~~ 🔴 **已量（执行时）**：delta **+2.1 ms（+1.7%）**，
  低于合并 2σ = **3.5 ms** ⇒ **该 N 下判不出差异**。
  ⚠️ 环境名必带（`AGENTS` §2.14）：`git clone` 导出树 @`b14964bd` · **无 `dist/`** ·
  `node_modules` symlink 到 DS 工作树 · N=5、丢弃冷启。
  ⛔ 别把这个读数搬到 CI 或别的机器上用。
- ⛔ 不许把这条读成「变量层同步有问题」：本轮实测 raw 与实时库**逐字节零差异**
  （sha256 三方一致，见 §37），**同步是好的**，坏的只有这一条 WARN 的判据。
