pi 2026-09-15 两条都成立,第二条我**采纳并实现了**(`ef1c1a0` 之前的这轮改动)。 ## 一★ "正则字面量里的引号"是**假绿方向** —— 我实测,比你写的还准 我用真 `stripStrings` 跑了你说的样本,**方向确认为"躲",而且躲掉的是真写**: ``` 输入: const RE = /["']/; totalTests += 1; 抹除后: const RE = /[" ← 从这个 " 起一路吞到下一个 " 命中: 0 处 ← 你写的 0,我复现 ``` ★ 更要紧的是**假绿那一支**,我用"把第二处写藏进被吞区间"直接验了: ``` 输入: const RE = /["x]; totalTests += 1; OK"]/ ; totalTests += 1; 抹除后: const RE = /[" "]/ ; totalTests += 1; 命中: 1 处(**未抹除时是 2 处**) ``` ⇒ 真身**有两处写**,判据数出 **1** ⇒ **绿**。而"第二处写"正是这条判据**唯一存在的理由** ⇒ 这是**绕过**,不是误伤。我注释里原来写的是"能躲或误伤" —— 词是对的,**但读不出严重性**, 已按你的话改成"**这一侧是假绿**",并写明**绿的准确含义**: "**在它能看见的文本里**没有第二处写",不是"没有第二处写"。 ★ 你指的第二点也修了:三条样本原来只要求 `truePosRaw > 0`("至少一条命中") ⇒ 其中一条哪天不再是假阳性,自检**照样绿**。现在**逐条断言**"未抹 >0 且抹后 0", 并且失败报文分开说**是哪一侧**(未抹=0 ⇒ 这条样本白测了;真写被吃 ⇒ 假绿方向)。 变异验过:把样本 #1 的分号去掉 ⇒ 自检立刻响(旧写法**照样绿**)。 ## 二★★ 投影:我**采纳了**,而且它确实把整族一起消掉了 你说得对,纯函数**仍然可以被调用两次** —— 那只是把"覆盖哪条路径"换个地方问一遍。 按你的形状改了: ```js records.push({ file, checks: fileChecks, fail: …, exitCode: r.status }); // 唯一的写点 // 循环之后: const reportedRecords = records.filter(r => r.checks !== null); totalTests = reportedRecords.reduce((n, r) => n + r.checks, 0); totalFail = reportedRecords.reduce((n, r) => n + r.fail, 0); ranFilesCount = reportedRecords.length; unreportedFiles = records.length - reportedRecords.length; ``` **删掉的东西**(这才是重点,不是"又加了一版"): - 整条**源码扫描器**(注释 → 字符串 → `= X + n`/`X++` → 锚钉行首 vs 语句起点 → 正则引号,**五轮补丁**); - 它的**抹除器自检**(`mkWrite` / 三条样本 / 逐条断言)—— **没有对象可扫了**; - `stripStrings` 在 `run-all.mjs` 的 **import**(留着不用的 import 就是下一次"看着像还在用"的源头)。 `stripStrings` 本身留在 `read.mjs`(通用工具),但它那条"正则引号"的边界**现在没有任何判据依赖** —— 这一族的风险从"判据可能假绿"降级成"一个没被用的工具函数有已知边界"。 **行为判据**(替代那条文本判据): ```js if (records.length !== SUITE.length) { … 并列出重复的文件 … } ``` **它抓得住那个形状**(注入"同一个文件 push 两次"实测): ``` RESULT files=25 ran=50 checks=800 … ✗ 套件自检 5:跑出的记录数 50 ≠ 套件文件数 25 —— 每个文件**恰好一条**记录 重复的文件:test/xxx.test.mjs×2 ``` **行为等价性**:`RESULT files=25 ran=25 checks=400 pass=399 fail=1 red=4 broken=0 unreported=0` 与改前**逐字节相同**。 ★ 顺带修掉一处**我自己造的**同类毛病:`ok 5` 的报文原来还写着"四个计数器各只有一处'写', **由源码扫描判**" —— 判据已经换了,文案没换。已改成"四个计数器由 25 条记录**投影**而来 —— 判的是记录数==文件数这个**行为**"。**改了判据却留着旧文案,就是又一次"说法与实现不一致"。** ## 三、★ 我要修正你一句措辞(实测后才敢说) 你写"**'把一个文件计两次'没有对应的语句可以写**"。我实测后认为**过强**: `records.push` **仍然可以被复制粘贴两次**(上面那条注入就是), 而且那正是历史上真实发生的形状(搬动累加块时把旧路径留在原地)。 准确的说法是:**重复不再表现为"某个计数器被加两次",而表现为"同一条记录出现两次"** ⇒ 它可以被**行为**判据抓住,而不是只能靠猜文本。 ★ **这是"文本判据 → 行为判据",不是"可能错 → 不可能错"。** 前者可测,后者只是话好听 —— 我不想把这次改进说成后者。 (我第一版就在注释里写了"写不出来",然后自己想了想不对,改成上面这句。) ## 四、残余与射程 - 上面那条"记录数==文件数"是**唯一**的新判据;`ranFilesCount > SUITE.length` 留着当 "投影被改坏"的探测器(投影下有构造性保证,所以它是冗余的 —— 我在注释里写明了"冗余", 不假装它还是主力)。 - 【本轮未做】纯函数形状**没有**保留(按你的建议直接上投影了)。 - 其余状态:`ok 5`/`ok 6` 绿、红线仍 4 条(都不是我的)、探针残留 0。
105 lines
5.5 KiB
JavaScript
105 lines
5.5 KiB
JavaScript
/**
|
||
* 判据目录里**唯一**允许读文本文件的两个入口 —— 名字自己解释该选哪个。
|
||
*
|
||
* # 为什么要有这个模块(pi 2026-09-14 §4:同一处坑我踩了两次)
|
||
*
|
||
* 规范里写着"判代码读剥离版、判理由读原文",我 P5 写过一次、当天又踩了一次
|
||
* (`'rejected'` 那段**注释**里正好写着 `allowed-once`,被当成"这里会放行"误报)。
|
||
* **第二次犯规说明问题不在记性,在形态**:靠人记得执行的规范一定会有下一次。
|
||
* 所以把"用哪个读取器"从**记忆**变成**代码里的一个词**,并且可以被判据检查。
|
||
*
|
||
* - `code(path)`:**剥掉注释**。判"代码里有没有这个调用/这个值"时必须用它 ——
|
||
* 否则解释性注释("这里写 'rejected' 而不是 'denied',因为只认 allowed-once")
|
||
* 会被当代码读,产生假红/假绿。
|
||
* - `prose(path)`:**原文**。判"理由写清了没/文档里有没有这句话"时用它。
|
||
*
|
||
* 选错的典型症状:断言里的标识符恰好在同文件的注释里出现过(这类误报几乎都集中在
|
||
* "解释性注释与它解释的标识符同名"的地方)。
|
||
*/
|
||
import { readFileSync } from 'node:fs';
|
||
|
||
/**
|
||
* 剥掉注释:只用于"代码里有什么"。
|
||
*
|
||
* ★ **行号必须保持不变** —— 这一条是硬要求,不是风格问题。
|
||
* 原来块注释是用 `''` 直接抹掉的,而块注释**自带换行**,抹掉它就把后面所有行的行号
|
||
* 整体前移。后果实测(我自己的 `harmony-arkts` 判据报违规时):
|
||
* 报出"最后一个 import 在第 64 行、第 47 行已是语句",而**真实文件里是第 80 / 63 行** ——
|
||
* 全仓的判据都在用 `文件:行号` 定位(`grep -n`、编辑器跳转),**报出来的行号必须能直接用**,
|
||
* 否则读者第一步就得先猜"这是剥过的还是没剥的"。
|
||
* 修法:块注释里的每个换行都**换成等价数量的空行**(而不是整块删掉)。
|
||
*/
|
||
export function stripComments(src) {
|
||
return src
|
||
.replace(/\/\*[\s\S]*?\*\//g, (m) => '\n'.repeat((m.match(/\n/g) || []).length))
|
||
.replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // 行注释(避开 https:// 这类;它不含换行,行号天然不变)
|
||
}
|
||
|
||
/** 读文件并**剥掉注释** —— 判"代码里有什么"用这个 */
|
||
export function code(path) {
|
||
return stripComments(readFileSync(path, 'utf8'));
|
||
}
|
||
|
||
/**
|
||
* 把**字符串/模板串的"体"**抹成空白(保留引号与换行)。
|
||
*
|
||
* ★ 为什么需要它:`stripComments` 只去注释,**不去字符串**。于是"在源码里找某个写法"
|
||
* 的那类判据会**咬到字符串里的文字** —— 这个族本仓已经踩过三次
|
||
* (`@ohos`、`toISOString`、以及"自检的报错文案把自己数进去")。
|
||
* 实测(pi 2026-09-15 用它自己的正则复现,我也复现):
|
||
* probs.push(`oops; totalTests += 1`); ← 被 MATCHED(假阳性)
|
||
* 而代码是对的 ⇒ **判据开始消费散文**,下一个人会去改**文案**来哄判据。
|
||
*
|
||
* ★ **行号必须不变**(与 `stripComments` 同一条硬要求):字符串里的每个换行
|
||
* 换成等价数量的空行。而且**保留引号本身**,这样"这里原本有个字符串"仍然看得见。
|
||
*
|
||
* ⚠️ 已知限制,**方向是"假绿",不是"误伤"** —— 这一点我第一版写错了,pi 2026-09-15 测出真方向:
|
||
* 逐字符扫描,**不区分正则字面量**。单个**不成对**的引号就够把后面一大段当字符串吞掉:
|
||
*
|
||
* 输入: const RE = /["']/; totalTests += 1;
|
||
* 抹除后: const RE = /[" ← 从那个 " 起,一路吞到**下一个 "**
|
||
* 命中: 0 处
|
||
*
|
||
* 两种后果,**要防的是第二种**:
|
||
* ① 常见的是**假红**(唯一那处写被吞 ⇒ 报"出现 0 处",而文案还指不到真因);
|
||
* ② **假绿**:被吞的区间里正好藏着**第二处写**,而第一处写在区间之外 ⇒ 计数仍是 1
|
||
* ⇒ **"重复"就在那里,判据却绿**。
|
||
* 实测:`const RE = /["x]; totalTests += 1; OK"]/ ; totalTests += 1;`
|
||
* ⇒ 真身 **2** 处,抹除后只数出 **1** 处。
|
||
*
|
||
* ★ 而"第二处写"正是这条判据**唯一存在的理由** ⇒ ② 是**绕过**。
|
||
* 所以这条判据的绿只能保证:"**在它能看见的文本里**没有第二处写" —— 不是"没有第二处写"。
|
||
* **把绿当成证明,就会在这里栽。**
|
||
*
|
||
* (修它需要词法状态机 —— 不划算,因为下一版要改成"记录投影",
|
||
* 那时"计两次"根本没有对应的语句可写,这一族连扫描对象都不存在。)
|
||
*/
|
||
export function stripStrings(src) {
|
||
let out = '';
|
||
let i = 0;
|
||
while (i < src.length) {
|
||
const ch = src[i];
|
||
if (ch !== '"' && ch !== "'" && ch !== '`') { out += ch; i += 1; continue; }
|
||
const quote = ch;
|
||
out += ch; i += 1;
|
||
while (i < src.length) {
|
||
const d = src[i];
|
||
if (d === '\\') { out += ' '; i += 2; continue; } // 转义:两个字符都抹掉(等长)
|
||
if (d === '\n') { out += '\n'; i += 1; continue; } // 换行**保留**(行号不变)
|
||
if (d === quote) { out += d; i += 1; break; }
|
||
out += ' '; i += 1;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 读文件**原文** —— 判"注释/文档里写了什么"用这个 */
|
||
export function prose(path) {
|
||
return readFileSync(path, 'utf8');
|
||
}
|
||
|
||
/** 读**二进制**(安装包、图片等)—— 需要 Buffer 时用它,别在判据里裸用 readFileSync */
|
||
export function bytes(path) {
|
||
return readFileSync(path);
|
||
}
|