|
15 | 15 | * **语法起点**行 —— JSX 属性常换行)。为什么需要这一档:迁移**保留用户可见文案原文** ⇒ 文案串 |
16 | 16 | * 必然还在文件里;判据的真身是「这句文案**是否走原语渲染**」,不是「字符串是否还存在」。 |
17 | 17 | * |
18 | | - * 副作用:只读磁盘(遍历 `app/src` + 定点读例外行)。边界:文本级判据、不做 AST; |
19 | | - * 例外 5 处的**行号**逐字冻结 —— 行号漂了或那一行不再命中词表即红。 |
| 18 | + * 副作用:只读磁盘(遍历 `app/src` + 定点读例外锚点行)。边界:文本级判据、不做 AST; |
| 19 | + * 例外 5 处的**锚点片段**逐字冻结 —— 片段被删改即红(行号漂移不再误伤,T14 实测过 147 → 148)。 |
| 20 | + * |
| 21 | + * ★ 变异体实验(T13,2026-09-12,**每棵树各解一次导出树**,CONTROL = 同源未变异树): |
| 22 | + * M1 把切片内一处已迁移的空态还原成手写灰字 ⇒ ② 红(+ ⑤ 的 migrated 自检红); |
| 23 | + * M2 把余量冻结值抬高 1 ⇒ ④ 红(**第一版判据无牙、实测 6/6 绿,已改成「冻结值恰等于实测值」**); |
| 24 | + * M3 新增一个带词表的文件 ⇒ ④ 红;M4 删掉 `ChatLaunchMenu` 的必填 prop ⇒ `tsc` 红(vitest 不暴露); |
| 25 | + * M0 反方向:把这套守卫放进**迁移前的基线树** ⇒ ② 红(证明判据不是永真)。 |
20 | 26 | */ |
21 | 27 | import { readFileSync, readdirSync } from "node:fs"; |
22 | 28 | import { dirname, join } from "node:path"; |
@@ -138,6 +144,9 @@ const FROZEN_REST: Readonly<Record<string, readonly [number, number]>> = { |
138 | 144 | "components/SessionListBody.tsx": [1, 1], |
139 | 145 | }; |
140 | 146 |
|
| 147 | +/** 余量**总数**的独立校验和(5 = 5 文件 × 1 行;只许降 —— 见 ④ 里为什么单列这一条) */ |
| 148 | +const FROZEN_REST_TOTAL = 5; |
| 149 | + |
141 | 150 | /** |
142 | 151 | * 允许命中词表的新增文件。域**已经**排除了 `ui/primitives/**`(T1 口径),故这里只做**显式声明**: |
143 | 152 | * `sliceScan.ts` 是本任务析出的共享扫描仪器(新增件),它逐字带着那张词表 ⇒ 把「域外 + 新件」 |
@@ -189,14 +198,21 @@ describe("空态切片棘轮(B11)", () => { |
189 | 198 | } |
190 | 199 | }); |
191 | 200 |
|
192 | | - it("④ 余量文件逐文件冻结(只许减)+ 全仓总数 ≤ 44 + 无未登记的新命中文件", () => { |
| 201 | + it("④ 余量逐文件冻结(只许减)+ **余量总数恰为 5** + 全仓总数 ≤ 44 + 无未登记的新命中文件", () => { |
193 | 202 | const restFiles = [...new Set(ALL_HITS.map((h) => h.file))].filter((f) => !SLICE.includes(f)).sort(); |
194 | 203 | expect(restFiles, "余量集变了(新增未登记的命中文件,或余量被悄悄迁移)").toEqual(Object.keys(FROZEN_REST).sort()); |
195 | 204 | for (const [f, [min, max]] of Object.entries(FROZEN_REST)) { |
196 | 205 | const n = hitsOf(f).length; |
197 | 206 | expect(n, `${f} 的命中数 ${n} 越出冻结区间 [${min}, ${max}]`).toBeGreaterThanOrEqual(min); |
198 | 207 | expect(n, `${f} 的命中数 ${n} 越出冻结区间 [${min}, ${max}]`).toBeLessThanOrEqual(max); |
| 208 | + // ★ 冻结值必须**恰好等于**实测值(T13 变异体实测的教训):只判区间时,把某条上界抬高 1 |
| 209 | + // 会让 M2 **静默通过**(实测 6/6 绿 = 判据无牙)。真迁走一处时须**主动收紧**这一对数字。 |
| 210 | + expect(`${f}=${min}..${max}`, `${f} 的冻结值与实测不一致:声明 ${min}..${max} / 实测 ${n}(棘轮只许降,须手工收紧)`) |
| 211 | + .toBe(`${f}=${n}..${n}`); |
199 | 212 | } |
| 213 | + // ★ 余量**总数**是独立校验和:任何一条被抬高/篡改都会让 `sum > 5` 当场红(M2 的第二个抓手)。 |
| 214 | + const restTotal = Object.keys(FROZEN_REST).reduce((n, f) => n + hitsOf(f).length, 0); |
| 215 | + expect(restTotal, "余量命中总数不是 5(棘轮只许降:真迁走了就把这里改成新值)").toBe(FROZEN_REST_TOTAL); |
200 | 216 | expect(ALL_HITS.length, "全仓命中总数超过基线 44").toBeLessThanOrEqual(44); |
201 | 217 | const unexpected = [...new Set(ALL_HITS.map((h) => h.file))].filter( |
202 | 218 | (f) => !SLICE.includes(f) && !(f in FROZEN_REST) && !NEW_FILES_ALLOWLIST.includes(f), |
|
0 commit comments