From fd1be276ac6fec7700387b4aab157688129a1cb6 Mon Sep 17 00:00:00 2001 From: Joe Wong Date: Mon, 14 Sep 2026 18:49:49 +0800 Subject: [PATCH] =?UTF-8?q?fix(drift):=20=E6=BC=82=E7=A7=BB=E6=A3=80?= =?UTF-8?q?=E6=9F=A5=E6=BC=8F=E6=8E=89=E4=BA=86=E4=B8=A4=E4=B8=AA=E5=AE=BF?= =?UTF-8?q?=E4=B8=BB=20plugin=EF=BC=8C=E8=80=8C=E7=BA=A2=E7=BA=BF=E6=AD=A3?= =?UTF-8?q?=E5=A5=BD=E6=8C=87=E7=9D=80=E6=BC=8F=E7=9A=84=E9=82=A3=E4=B8=80?= =?UTF-8?q?=E5=8D=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 2026-09-14 调研宿主接入时发现:`plugins/testpilot-claude` 与 `-codex` 停在 09-12, 落后四个文件——`.claude-plugin/testpilot.json`、`testpilot-design/REFERENCE.md`、 `REFERENCE-domain-perp.md`,以及当天刚改过的 `testpilot-stories/SKILL.md`。 也就是说「长故事覆盖短故事」那条新规则,宿主 agent 整整两天没收到过。 检查本来是有的。`build-claude-plugin.mjs --check` / `build-codex-plugin.mjs --check` 一直在 `pnpm check:drift` 那个三步串里。问题出在 CLAUDE.md 的红线写的是 `node scripts/check-drift.mjs`——**只有第一步**。照红线跑,它一直是绿的, 后两步从没跑过。一个有两种拼法、而其中一种会漏的检查,等于没有这个检查。 所以修的不是那行字,是入口:三步收进 `check-drift.mjs` 一个入口, `package.json` 的 `check:drift` 退回成单条命令,两种拼法从此等价。 `--write` 也顺带重新生成宿主副本——改完 skill 跳版本的那一下,本来就该把副本一起带上。 验过它真能抓到:故意在 `testpilot-claude/skills/testpilot-stories/SKILL.md` 末尾加一行, 窄跑法当场报「与真源不一致(1 处)」并列出文件名;改回去就绿。 顺带把两个宿主副本重新生成到与真源一致。CLAUDE.md 那条红线也改了, 并在旁边写清楚为什么值得记着这件事。 Co-Authored-By: Claude Opus 5 (1M context) --- CLAUDE.md | 3 +- package.json | 2 +- .../.claude-plugin/testpilot.json | 6 +- .../testpilot-design/REFERENCE-domain-perp.md | 98 +++++++------------ .../skills/testpilot-design/REFERENCE.md | 6 +- .../skills/testpilot-stories/SKILL.md | 5 + .../testpilot-design/REFERENCE-domain-perp.md | 98 +++++++------------ .../skills/testpilot-design/REFERENCE.md | 6 +- .../skills/testpilot-stories/SKILL.md | 5 + scripts/check-drift.mjs | 26 ++++- 10 files changed, 125 insertions(+), 130 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index ca2a7e6..9bc7fe5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,7 +40,8 @@ - 本会话用户已授权本地真实模型/浏览器联调,不需要再次索要 `.env` 或确认这类验收。新会话按其授权范围执行;不得把旧默认限制当成本会话的额外审批。`git commit` / `git push` / 对外发布仍未授权。 - 不改 `benchmark/*/gold.json`、`human-labels.json`、`held-out/`、`rubric/`;不把它们放进任何提示词。 - 不让自愈改 `oracle`。 -- 动 `packages/harness-testing/src/casegen/prompts.ts` 或 `plugins/testpilot/skills/**` 时:`node scripts/check-drift.mjs` 必须绿,`plugins/testpilot/plugin.json` 的 `skillVersions` 跳版本。 +- 动 `packages/harness-testing/src/casegen/prompts.ts` 或 `plugins/testpilot/skills/**` 时:`pnpm check:drift`(等价于 `node scripts/check-drift.mjs`)必须绿,`plugins/testpilot/plugin.json` 的 `skillVersions` 跳版本。 + 改完 skill 记得 `node scripts/build-claude-plugin.mjs` 与 `build-codex-plugin.mjs` 重新生成宿主副本——检查会拦,但拦住之前先想着它:2026-09-14 那次,改过的 skill 有四个文件整整两天没到宿主 agent 手里。 ## 验收命令 ```bash diff --git a/package.json b/package.json index 03dfe30..f3ae4bc 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "server:dev": "pnpm --filter testpilot-server dev", "check:i18n": "node scripts/check-i18n.mjs", "test:hooks": "node --test \"plugins/testpilot/hooks/test/*.test.mjs\"", - "check:drift": "node scripts/check-drift.mjs && node scripts/build-claude-plugin.mjs --check && node scripts/build-codex-plugin.mjs --check", + "check:drift": "node scripts/check-drift.mjs", "build:claude-plugin": "node scripts/build-claude-plugin.mjs", "cost:report": "node scripts/cost-report.mjs", "doctor": "node scripts/testpilot-setup.mjs doctor", diff --git a/plugins/testpilot-claude/.claude-plugin/testpilot.json b/plugins/testpilot-claude/.claude-plugin/testpilot.json index 9844c86..2871b9d 100644 --- a/plugins/testpilot-claude/.claude-plugin/testpilot.json +++ b/plugins/testpilot-claude/.claude-plugin/testpilot.json @@ -4,8 +4,8 @@ "skillVersions": { "testpilot-generate": "2026-09-10.1", "testpilot-explore": "2026-09-03.1", - "testpilot-stories": "2026-09-10.1", - "testpilot-design": "2026-09-12.1", + "testpilot-stories": "2026-09-14.1", + "testpilot-design": "2026-09-13.1", "testpilot-run-c": "2026-09-12.3", "testpilot-evaluation": "2026-09-03.1", "testpilot-benchmark-design": "2026-09-04.1", @@ -16,7 +16,7 @@ "skillDigests": { "testpilot-generate": "12ee15239b74568a", "testpilot-explore": "49cd2ccdaf361117", - "testpilot-stories": "078a845773d390a4", + "testpilot-stories": "0e71db20690374b0", "testpilot-design": "17734a1342ef759e", "testpilot-run-c": "dc8f6953b1b9efd9", "testpilot-evaluation": "3d1bc1c2aaeae65e", diff --git a/plugins/testpilot-claude/skills/testpilot-design/REFERENCE-domain-perp.md b/plugins/testpilot-claude/skills/testpilot-design/REFERENCE-domain-perp.md index ac5c25e..b695329 100644 --- a/plugins/testpilot-claude/skills/testpilot-design/REFERENCE-domain-perp.md +++ b/plugins/testpilot-claude/skills/testpilot-design/REFERENCE-domain-perp.md @@ -1,88 +1,66 @@ -# 合约交易前端的不变量(领域 REFERENCE) +# 领域不变量怎么变成屏幕判据(领域 REFERENCE) > 这一份对应 `prompts.ts` 里的 `DOMAIN_PERP`。它是**可选的一段**:在 skill 世界里,「可选」= 这份文件在不在。 > 装上它是一臂,卸掉它是另一臂(`ablate: ["domain-perp"]`)。它存在的目的是被比较: -> 对着观察写的用例只能发现产品**变了**,对着这些规则写的用例才有资格说产品**错了**。 -> -> 只写**做过合约前端的人才知道、而且能变成判据**的规则。通用测试方法不在这里(那是 `REFERENCE.md`)。 -> 每条规则附一个判据示例,**全部打在屏幕上**:出现了哪一行、那一行里是什么值、哪个数必须不变、 -> 前端的拒绝文案。这个产品产出的是端到端 UI 测试——去问被测产品自己的接口,判的就不是用户 -> 看得见的那件事(2026-09-12 用户口径,见仓库 CLAUDE.md)。 -> **没有一条钉在易变读数上**——有测试扫这份文件(`domain-perp.test.ts`)。 +> 对着观察写的用例只能发现产品**变了**,对着领域不变量写的用例才有资格说产品**错了**。 -被测对象是合约交易页(下单面板、持仓、挂单)时,设计用例前先读它;不是就跳过。 +## 2026-09-13:不变量的正本在规则包里,不在这份文件里 -## 规则与判据 +这份文件原来自己列着十条合约交易的不变量(步长、80% 价格带、杠杆分档、平仓后消失…)。 +那是**领域业务知识放错了地方**: -### 1. 先确认数量单位、步长及该产品的输入归一化规则 -只有规格明确要求截断的前端(如本仓库 tier4-demo 演示契约),0.001 步长输入 0.0016 才应提交 0.001。交易所接受的精度限制不能证明前端必须截断;有的入口会直接拒绝非整步数量。以下示例仅用于明确的截断契约。0.001 到 0.002 是翻倍,不能描述成十倍。 -```json -{"expected": "仓位表里出现 BTC 的一行,数量列显示 0.001", "oracle": {"kind": "count", "value": "BTC", "op": "eq", "n": 1}} -``` +- 它对每个项目都下发,换一个产品(电商、后台)收到的是一段用不上的合约散文; +- 它和规则包里的 `R-SIZE-PRECISION` / `R-PRICE-PRECISION` / `R-LEVERAGE` 说同一批事,两处会各自漂移; +- **最要命的是它以权威口吻写**。同样的内容搬进规则包之后,校验当场拦了两次: + 「domain-reference 这类来源只能支撑假设,撑不起要求」,以及「没有产品来源的假设不能自称 P0」。 + 于是那五条进去时只能是 `claimType: hypothesis`、不带 `riskFloor`—— + 而假设在下游有明确待遇:**只能变成开放问题,永远不能变成验收标准**。 + 它们在这份散文里被当成事实用了很久。 -### 2. 价格按 tickSize 对齐;离参考价超过交易所的带宽(Hyperliquid:80%)的限价单在前端就被拒 -80% 是历史演示契约,不是所有交易所或当前所有产品的固定规则。先获取被测版本的带宽与拒绝文案;下面只有在规格明确该文案时适用。 -```json -{"expected": "页面显示「Order price cannot be more than 80% away from the reference price」", "oracle": {"kind": "text", "value": "Order price cannot be more than 80% away from the reference price"}} -``` +**所以:这个产品的领域不变量,看你这个单元材料里的 `rules`。** 每条规则自带 +`claimType`(要求 / 观察 / 假设)、`sourceRefs` 与 `riskFloor`——那三样决定了你能拿它做什么: -### 3. 杠杆上限随名义价值分档;账户最终的杠杆以仓位行显示的为准 -```json -{"expected": "合约头部的杠杆徽标显示 5x,且仓位行的杠杆与它一致", "oracle": {"kind": "text", "value": "5x"}} -``` +| claimType | 你能用它做什么 | +|---|---| +| `normative` | 可以写成验收标准与用例的期望;`riskFloor` 是这条用例优先级的下限 | +| `observed` | 可以写成用例,但期望只能是「观察到的那样」,不能说产品「应该」如此 | +| `hypothesis` | **只能变成开放问题**。不要把它写成一条会判失败的断言——那是在拿没证实的东西判产品有罪 | -### 4. 逐仓 / 全仓切换:模式与余额重算是独立验收点 -下面仅证明模式已切换;余额/强平价重算需要冻结行情、计算规格和独立判据。模式通过不能证明重算正确。 -```json -{"expected": "下单面板的保证金模式控件显示 Isolated", "oracle": {"kind": "text", "value": "Isolated"}} -``` +## 剩下的部分:怎么把一条领域规则变成屏幕上的判据 + +规则说的是产品**应该**怎样,判据说的是**屏幕上**能看到什么。这中间的翻译有固定几招: -### 5. 触发单类型与价格关系分别验证 -相对入场价的方向规则仅在该产品规格要求时适用;盈利后移动止损等场景不能套用统一方向。下面只证明触发单类型存在,触发价必须另有精确判据。 +### 1. 「某件事发生了」→ 出现了哪一行 ```json -{"expected": "当前委托表里出现一行 Take Profit Market,市场列是 BTC", "oracle": {"kind": "count", "value": "Take Profit Market", "op": "eq", "n": 1}} +{"expected": "仓位表里出现 BTC 的一行,数量列显示 0.001", "oracle": {"kind": "count", "value": "BTC", "op": "eq", "n": 1}} ``` -### 6. 一键平仓后该币种从仓位表里消失 +### 2. 「某件事被撤销了」→ 那一行不见了 ```json {"expected": "平仓后仓位表不再列出 BTC(它是唯一持仓时显示空态文案)", "oracle": {"kind": "noText", "value": "BTC-USD"}} ``` -### 7. 超过可用保证金的下单被拒,名义仓位不变(tier 2:两次读数的关系) +### 3. 「产品应该拒绝」→ 拒绝文案逐字出现 +拒绝文案是产品自己说的话,**逐字抄**,不要翻译也不要自己编一句更顺口的。 ```json -{"expected": "下单被拒,当前委托表的行数在提交前后不变", "oracle": {"kind": "delta", "value": "当前委托", "direction": "unchanged"}} +{"expected": "页面显示「Order must have minimum value of $10.」", "oracle": {"kind": "text", "value": "Order must have minimum value of $10."}} ``` -### 8. 撤单后挂单从当前委托表里消失 +### 4. 「这一步不该改变什么」→ 两次读数的关系(tier 2) ```json -{"expected": "撤单后当前委托表里没有 BTC 的挂单", "oracle": {"kind": "noText", "value": "BTC-USD"}} +{"expected": "下单被拒,当前委托表的行数在提交前后不变", "oracle": {"kind": "delta", "value": "当前委托", "direction": "unchanged"}} ``` -### 9. 资金费率、倒计时、24h 量、标记价 / 预言机价是**易变读数**,禁止钉在判据里 -能断言的只有「字段存在」或一个关系。 +### 5. 易变读数只能断言存在或关系,不能钉住数值 +标记价、预言机价、资金费率、倒计时、24h 量都在动。钉住一个数的用例下一分钟就红, +而它红的时候产品什么事都没有。 ```json -{"expected": "账户权益字段在页面上存在(只断言存在,不钉数值)", "oracle": {"kind": "count", "value": "Account Equity", "op": "gte", "n": 1}} +{"expected": "标头显示 Funding / Countdown 字段", "oracle": {"kind": "text", "value": "Funding / Countdown"}} ``` -### 10. 断线重连后挂单列表与重连前一致 -下面的 exists 仅是接口存在性检查,**不能证明重连后 UI/API 一致**。一致性需按同一订单 ID 比较快照;当前单一 oracle 无法组合时标为未覆盖,不能用存在性替代。 +### 6. 判决永远从屏幕读,不去问被测产品自己的接口 +接口说下单成功而屏幕上没有那一行,这条用例会通过,而产品其实是坏的。 +会动的数字不是去问接口的理由——那恰恰是上面第 4、5 条的理由。 ```json -{"expected": "重连后当前委托表仍列出同样多的行", "oracle": {"kind": "delta", "value": "当前委托", "direction": "unchanged"}} +{"expected": "屏幕显示「Limit order placed.」", "oracle": {"kind": "text", "value": "Limit order placed."}} ``` - -## 写法约束 - -- 涉及资金或仓位状态的用例**必须**带 `api` 判据;`text` 只用于前端的拒绝文案。 -- 接口地址与账户地址走 `${env.*}` 占位符,不写字面量。 -- 数组元素按字段选(`[position.coin=BTC]`),不按下标——下标随账户里有几个仓位而变。 -- `settleMs` 给 2–4 秒:交易所接口在下单后有传播延迟。 - -## 适用范围与依据(2026-09-09 核验) - -这是一组测试设计候选规则,不是跨交易所统一交易规则。优先级:被测版本需求/契约 → 该交易所官方规格 → 待确认建议。每个 sourceRef 存在只证明定位成功,不证明它支持结论;人工审核需核对规则族、参数、单位和版本。 - -- [Hyperliquid tick and lot size](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size) 定义精度限制,不定义所有前端如何修改用户输入。 -- [Hyperliquid order book](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/order-book) 定义 tick/lot 对齐。 -- [Binance Spot filters](https://github.com/binance/binance-spot-api-docs/blob/master/filters.md) 是现货过滤器资料;不能未经核对推广至合约。 - -金额/数量优先使用十进制字符串,API 判据精确比较,不自动引入容差。数组需唯一币种/订单 ID;多项匹配不可观测。`unit: {path,value}` 可验证单位;`freshness: {timestampPath,maxAgeMs}` 检查毫秒时间戳或 ISO 时间。前后关系必须在相同币种、账户、环境复位条件下取样。等待固定时间不证明状态已传播;过期或缺失时间应记录未观测。 diff --git a/plugins/testpilot-claude/skills/testpilot-design/REFERENCE.md b/plugins/testpilot-claude/skills/testpilot-design/REFERENCE.md index 2a7d05c..8083ec9 100644 --- a/plugins/testpilot-claude/skills/testpilot-design/REFERENCE.md +++ b/plugins/testpilot-claude/skills/testpilot-design/REFERENCE.md @@ -52,11 +52,15 @@ }, "risk": { "impact": "funds-and-exposure", "reason": "数量取整错误会改变实际敞口", "ruleRefs": ["R-SIZE-PRECISION"] }, "testData": { "fixtureRef": "perp-deterministic-v1", "values": [{ "name": "size", "value": "0.001", "unit": "BTC", "source": "R-SIZE-PRECISION" }] }, - "assertions": [{ "id": "A-SIZE", "statement": "接口里的持仓数量等于 0.001 BTC", "ruleRefs": ["R-SIZE-PRECISION"], "oracle": { "kind": "api", "url": "${env.HL_INFO}", "method": "POST", "body": "…", "path": "…szi", "op": "eq", "value": "0.001" } }], + "assertions": [{ "id": "A-SIZE", "statement": "仓位表里 BTC 那一行的数量列显示 0.001", "ruleRefs": ["R-SIZE-PRECISION"], "oracle": { "kind": "text", "value": "0.001" } }], "readiness": { "design": "candidate", "execution": "requires-fixture", "reason": "缺可控持仓 fixture" } } ``` +> `assertions[].oracle` 和顶层 `oracle` 是同一套判据,**同样只能从屏幕读**(CLAUDE.md 红线; +> 门禁规则 `oracle-offsite` 两边都查)。2026-09-13 之前这里的示例写的是 `kind: "api"`—— +> 红线定了之后它没跟上,实测有用例照着它写出了断言被测站接口的判据,复核时被驳回。 + | 字段 | 给了就会被核对的事 | |---|---| | `scenarioType` | 和 `designMethod` 分开:负例同样是用某种方法设计出来的。标了负向场景又标了方法,却不给 `design`,会被拒 | diff --git a/plugins/testpilot-claude/skills/testpilot-stories/SKILL.md b/plugins/testpilot-claude/skills/testpilot-stories/SKILL.md index d771eb2..caf933b 100644 --- a/plugins/testpilot-claude/skills/testpilot-stories/SKILL.md +++ b/plugins/testpilot-claude/skills/testpilot-stories/SKILL.md @@ -71,6 +71,11 @@ rule bf648e58 When the budget of stories is smaller than the material, spr - `acceptance` 写成 Given / When / Then,一条判据一项,界面文案**逐字引用规格自己的话**。 「Given 购物车里有一件商品 / When 用户点 Checkout / Then 页面是 Checkout: Your Information」。 **没有 When 的是描述,不是判据。** +- **一条长故事走完,就顺带走完了几条短故事——用 `subsumes` 说出来。** + 展示型的短故事(「页头显示标记价」)单独立一条,下游只会长出「打开页面 + 看一眼」的两步用例, + 而那种用例的判据在初始页面上就已经成立,它通过时什么都没证明。 + 一条真实旅程本来就会路过它:把它写进 `subsumes`,被覆盖的故事不再单独出用例, + 它的判据改由这条长旅程路上的断言了结。只覆盖一层,别覆盖一条自己也在覆盖别人的故事。 - 规格有自己的 id(US-01 之类)就沿用;没有就编号 S-01、S-02…… - 规格明说不在范围内的东西,不要为它产故事。 - 材料可能是**好几份文档**,每份以 `===== 路径 =====` 开头。**每一份都要覆盖到。** diff --git a/plugins/testpilot-codex/skills/testpilot-design/REFERENCE-domain-perp.md b/plugins/testpilot-codex/skills/testpilot-design/REFERENCE-domain-perp.md index ac5c25e..b695329 100644 --- a/plugins/testpilot-codex/skills/testpilot-design/REFERENCE-domain-perp.md +++ b/plugins/testpilot-codex/skills/testpilot-design/REFERENCE-domain-perp.md @@ -1,88 +1,66 @@ -# 合约交易前端的不变量(领域 REFERENCE) +# 领域不变量怎么变成屏幕判据(领域 REFERENCE) > 这一份对应 `prompts.ts` 里的 `DOMAIN_PERP`。它是**可选的一段**:在 skill 世界里,「可选」= 这份文件在不在。 > 装上它是一臂,卸掉它是另一臂(`ablate: ["domain-perp"]`)。它存在的目的是被比较: -> 对着观察写的用例只能发现产品**变了**,对着这些规则写的用例才有资格说产品**错了**。 -> -> 只写**做过合约前端的人才知道、而且能变成判据**的规则。通用测试方法不在这里(那是 `REFERENCE.md`)。 -> 每条规则附一个判据示例,**全部打在屏幕上**:出现了哪一行、那一行里是什么值、哪个数必须不变、 -> 前端的拒绝文案。这个产品产出的是端到端 UI 测试——去问被测产品自己的接口,判的就不是用户 -> 看得见的那件事(2026-09-12 用户口径,见仓库 CLAUDE.md)。 -> **没有一条钉在易变读数上**——有测试扫这份文件(`domain-perp.test.ts`)。 +> 对着观察写的用例只能发现产品**变了**,对着领域不变量写的用例才有资格说产品**错了**。 -被测对象是合约交易页(下单面板、持仓、挂单)时,设计用例前先读它;不是就跳过。 +## 2026-09-13:不变量的正本在规则包里,不在这份文件里 -## 规则与判据 +这份文件原来自己列着十条合约交易的不变量(步长、80% 价格带、杠杆分档、平仓后消失…)。 +那是**领域业务知识放错了地方**: -### 1. 先确认数量单位、步长及该产品的输入归一化规则 -只有规格明确要求截断的前端(如本仓库 tier4-demo 演示契约),0.001 步长输入 0.0016 才应提交 0.001。交易所接受的精度限制不能证明前端必须截断;有的入口会直接拒绝非整步数量。以下示例仅用于明确的截断契约。0.001 到 0.002 是翻倍,不能描述成十倍。 -```json -{"expected": "仓位表里出现 BTC 的一行,数量列显示 0.001", "oracle": {"kind": "count", "value": "BTC", "op": "eq", "n": 1}} -``` +- 它对每个项目都下发,换一个产品(电商、后台)收到的是一段用不上的合约散文; +- 它和规则包里的 `R-SIZE-PRECISION` / `R-PRICE-PRECISION` / `R-LEVERAGE` 说同一批事,两处会各自漂移; +- **最要命的是它以权威口吻写**。同样的内容搬进规则包之后,校验当场拦了两次: + 「domain-reference 这类来源只能支撑假设,撑不起要求」,以及「没有产品来源的假设不能自称 P0」。 + 于是那五条进去时只能是 `claimType: hypothesis`、不带 `riskFloor`—— + 而假设在下游有明确待遇:**只能变成开放问题,永远不能变成验收标准**。 + 它们在这份散文里被当成事实用了很久。 -### 2. 价格按 tickSize 对齐;离参考价超过交易所的带宽(Hyperliquid:80%)的限价单在前端就被拒 -80% 是历史演示契约,不是所有交易所或当前所有产品的固定规则。先获取被测版本的带宽与拒绝文案;下面只有在规格明确该文案时适用。 -```json -{"expected": "页面显示「Order price cannot be more than 80% away from the reference price」", "oracle": {"kind": "text", "value": "Order price cannot be more than 80% away from the reference price"}} -``` +**所以:这个产品的领域不变量,看你这个单元材料里的 `rules`。** 每条规则自带 +`claimType`(要求 / 观察 / 假设)、`sourceRefs` 与 `riskFloor`——那三样决定了你能拿它做什么: -### 3. 杠杆上限随名义价值分档;账户最终的杠杆以仓位行显示的为准 -```json -{"expected": "合约头部的杠杆徽标显示 5x,且仓位行的杠杆与它一致", "oracle": {"kind": "text", "value": "5x"}} -``` +| claimType | 你能用它做什么 | +|---|---| +| `normative` | 可以写成验收标准与用例的期望;`riskFloor` 是这条用例优先级的下限 | +| `observed` | 可以写成用例,但期望只能是「观察到的那样」,不能说产品「应该」如此 | +| `hypothesis` | **只能变成开放问题**。不要把它写成一条会判失败的断言——那是在拿没证实的东西判产品有罪 | -### 4. 逐仓 / 全仓切换:模式与余额重算是独立验收点 -下面仅证明模式已切换;余额/强平价重算需要冻结行情、计算规格和独立判据。模式通过不能证明重算正确。 -```json -{"expected": "下单面板的保证金模式控件显示 Isolated", "oracle": {"kind": "text", "value": "Isolated"}} -``` +## 剩下的部分:怎么把一条领域规则变成屏幕上的判据 + +规则说的是产品**应该**怎样,判据说的是**屏幕上**能看到什么。这中间的翻译有固定几招: -### 5. 触发单类型与价格关系分别验证 -相对入场价的方向规则仅在该产品规格要求时适用;盈利后移动止损等场景不能套用统一方向。下面只证明触发单类型存在,触发价必须另有精确判据。 +### 1. 「某件事发生了」→ 出现了哪一行 ```json -{"expected": "当前委托表里出现一行 Take Profit Market,市场列是 BTC", "oracle": {"kind": "count", "value": "Take Profit Market", "op": "eq", "n": 1}} +{"expected": "仓位表里出现 BTC 的一行,数量列显示 0.001", "oracle": {"kind": "count", "value": "BTC", "op": "eq", "n": 1}} ``` -### 6. 一键平仓后该币种从仓位表里消失 +### 2. 「某件事被撤销了」→ 那一行不见了 ```json {"expected": "平仓后仓位表不再列出 BTC(它是唯一持仓时显示空态文案)", "oracle": {"kind": "noText", "value": "BTC-USD"}} ``` -### 7. 超过可用保证金的下单被拒,名义仓位不变(tier 2:两次读数的关系) +### 3. 「产品应该拒绝」→ 拒绝文案逐字出现 +拒绝文案是产品自己说的话,**逐字抄**,不要翻译也不要自己编一句更顺口的。 ```json -{"expected": "下单被拒,当前委托表的行数在提交前后不变", "oracle": {"kind": "delta", "value": "当前委托", "direction": "unchanged"}} +{"expected": "页面显示「Order must have minimum value of $10.」", "oracle": {"kind": "text", "value": "Order must have minimum value of $10."}} ``` -### 8. 撤单后挂单从当前委托表里消失 +### 4. 「这一步不该改变什么」→ 两次读数的关系(tier 2) ```json -{"expected": "撤单后当前委托表里没有 BTC 的挂单", "oracle": {"kind": "noText", "value": "BTC-USD"}} +{"expected": "下单被拒,当前委托表的行数在提交前后不变", "oracle": {"kind": "delta", "value": "当前委托", "direction": "unchanged"}} ``` -### 9. 资金费率、倒计时、24h 量、标记价 / 预言机价是**易变读数**,禁止钉在判据里 -能断言的只有「字段存在」或一个关系。 +### 5. 易变读数只能断言存在或关系,不能钉住数值 +标记价、预言机价、资金费率、倒计时、24h 量都在动。钉住一个数的用例下一分钟就红, +而它红的时候产品什么事都没有。 ```json -{"expected": "账户权益字段在页面上存在(只断言存在,不钉数值)", "oracle": {"kind": "count", "value": "Account Equity", "op": "gte", "n": 1}} +{"expected": "标头显示 Funding / Countdown 字段", "oracle": {"kind": "text", "value": "Funding / Countdown"}} ``` -### 10. 断线重连后挂单列表与重连前一致 -下面的 exists 仅是接口存在性检查,**不能证明重连后 UI/API 一致**。一致性需按同一订单 ID 比较快照;当前单一 oracle 无法组合时标为未覆盖,不能用存在性替代。 +### 6. 判决永远从屏幕读,不去问被测产品自己的接口 +接口说下单成功而屏幕上没有那一行,这条用例会通过,而产品其实是坏的。 +会动的数字不是去问接口的理由——那恰恰是上面第 4、5 条的理由。 ```json -{"expected": "重连后当前委托表仍列出同样多的行", "oracle": {"kind": "delta", "value": "当前委托", "direction": "unchanged"}} +{"expected": "屏幕显示「Limit order placed.」", "oracle": {"kind": "text", "value": "Limit order placed."}} ``` - -## 写法约束 - -- 涉及资金或仓位状态的用例**必须**带 `api` 判据;`text` 只用于前端的拒绝文案。 -- 接口地址与账户地址走 `${env.*}` 占位符,不写字面量。 -- 数组元素按字段选(`[position.coin=BTC]`),不按下标——下标随账户里有几个仓位而变。 -- `settleMs` 给 2–4 秒:交易所接口在下单后有传播延迟。 - -## 适用范围与依据(2026-09-09 核验) - -这是一组测试设计候选规则,不是跨交易所统一交易规则。优先级:被测版本需求/契约 → 该交易所官方规格 → 待确认建议。每个 sourceRef 存在只证明定位成功,不证明它支持结论;人工审核需核对规则族、参数、单位和版本。 - -- [Hyperliquid tick and lot size](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size) 定义精度限制,不定义所有前端如何修改用户输入。 -- [Hyperliquid order book](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/order-book) 定义 tick/lot 对齐。 -- [Binance Spot filters](https://github.com/binance/binance-spot-api-docs/blob/master/filters.md) 是现货过滤器资料;不能未经核对推广至合约。 - -金额/数量优先使用十进制字符串,API 判据精确比较,不自动引入容差。数组需唯一币种/订单 ID;多项匹配不可观测。`unit: {path,value}` 可验证单位;`freshness: {timestampPath,maxAgeMs}` 检查毫秒时间戳或 ISO 时间。前后关系必须在相同币种、账户、环境复位条件下取样。等待固定时间不证明状态已传播;过期或缺失时间应记录未观测。 diff --git a/plugins/testpilot-codex/skills/testpilot-design/REFERENCE.md b/plugins/testpilot-codex/skills/testpilot-design/REFERENCE.md index 2a7d05c..8083ec9 100644 --- a/plugins/testpilot-codex/skills/testpilot-design/REFERENCE.md +++ b/plugins/testpilot-codex/skills/testpilot-design/REFERENCE.md @@ -52,11 +52,15 @@ }, "risk": { "impact": "funds-and-exposure", "reason": "数量取整错误会改变实际敞口", "ruleRefs": ["R-SIZE-PRECISION"] }, "testData": { "fixtureRef": "perp-deterministic-v1", "values": [{ "name": "size", "value": "0.001", "unit": "BTC", "source": "R-SIZE-PRECISION" }] }, - "assertions": [{ "id": "A-SIZE", "statement": "接口里的持仓数量等于 0.001 BTC", "ruleRefs": ["R-SIZE-PRECISION"], "oracle": { "kind": "api", "url": "${env.HL_INFO}", "method": "POST", "body": "…", "path": "…szi", "op": "eq", "value": "0.001" } }], + "assertions": [{ "id": "A-SIZE", "statement": "仓位表里 BTC 那一行的数量列显示 0.001", "ruleRefs": ["R-SIZE-PRECISION"], "oracle": { "kind": "text", "value": "0.001" } }], "readiness": { "design": "candidate", "execution": "requires-fixture", "reason": "缺可控持仓 fixture" } } ``` +> `assertions[].oracle` 和顶层 `oracle` 是同一套判据,**同样只能从屏幕读**(CLAUDE.md 红线; +> 门禁规则 `oracle-offsite` 两边都查)。2026-09-13 之前这里的示例写的是 `kind: "api"`—— +> 红线定了之后它没跟上,实测有用例照着它写出了断言被测站接口的判据,复核时被驳回。 + | 字段 | 给了就会被核对的事 | |---|---| | `scenarioType` | 和 `designMethod` 分开:负例同样是用某种方法设计出来的。标了负向场景又标了方法,却不给 `design`,会被拒 | diff --git a/plugins/testpilot-codex/skills/testpilot-stories/SKILL.md b/plugins/testpilot-codex/skills/testpilot-stories/SKILL.md index d771eb2..caf933b 100644 --- a/plugins/testpilot-codex/skills/testpilot-stories/SKILL.md +++ b/plugins/testpilot-codex/skills/testpilot-stories/SKILL.md @@ -71,6 +71,11 @@ rule bf648e58 When the budget of stories is smaller than the material, spr - `acceptance` 写成 Given / When / Then,一条判据一项,界面文案**逐字引用规格自己的话**。 「Given 购物车里有一件商品 / When 用户点 Checkout / Then 页面是 Checkout: Your Information」。 **没有 When 的是描述,不是判据。** +- **一条长故事走完,就顺带走完了几条短故事——用 `subsumes` 说出来。** + 展示型的短故事(「页头显示标记价」)单独立一条,下游只会长出「打开页面 + 看一眼」的两步用例, + 而那种用例的判据在初始页面上就已经成立,它通过时什么都没证明。 + 一条真实旅程本来就会路过它:把它写进 `subsumes`,被覆盖的故事不再单独出用例, + 它的判据改由这条长旅程路上的断言了结。只覆盖一层,别覆盖一条自己也在覆盖别人的故事。 - 规格有自己的 id(US-01 之类)就沿用;没有就编号 S-01、S-02…… - 规格明说不在范围内的东西,不要为它产故事。 - 材料可能是**好几份文档**,每份以 `===== 路径 =====` 开头。**每一份都要覆盖到。** diff --git a/scripts/check-drift.mjs b/scripts/check-drift.mjs index 0b89a94..8f3cc04 100644 --- a/scripts/check-drift.mjs +++ b/scripts/check-drift.mjs @@ -36,7 +36,7 @@ import { createHash } from "node:crypto"; import { readFileSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; -import { execSync } from "node:child_process"; +import { execSync, spawnSync } from "node:child_process"; import { fileURLToPath, pathToFileURL } from "node:url"; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); @@ -179,7 +179,27 @@ async function main() { if (WRITE) { writeFileSync(pluginPath, JSON.stringify(plugin, null, 2) + "\n"); console.log("drift baseline written: 认领块与 skillDigests 已更新"); - return 0; + } + + /** + * **宿主 plugin 的副本也归这里查。** + * + * 2026-09-14 实测:`plugins/testpilot-claude` 与 `-codex` 是构建产物(真源只有 + * `plugins/testpilot/`),而它们停在 09-12,落后四个文件——其中就有当天刚改过的 + * `testpilot-stories/SKILL.md`。也就是说「长故事覆盖短故事」那条新规则, + * 宿主 agent 从来没收到过。 + * + * 检查本来是有的(`build-*-plugin.mjs --check`),只是挂在 `pnpm check:drift` 这个 + * 三步串里,而 CLAUDE.md 的红线写的是 `node scripts/check-drift.mjs`——只有第一步。 + * 照红线跑,它一直是绿的。**一个有两种跑法、而其中一种会漏的检查,等于没有这个检查。** + * + * 所以把三步收进这一个入口:两种拼法从此等价,漏不掉。 + */ + for (const script of ["build-claude-plugin.mjs", "build-codex-plugin.mjs"]) { + const full = path.join(path.dirname(fileURLToPath(import.meta.url)), script); + const r = spawnSync(process.execPath, WRITE ? [full] : [full, "--check"], { encoding: "utf8" }); + if (r.status !== 0) problems.push(`${script}:${(r.stderr || r.stdout || "").trim().split("\n").slice(0, 6).join("\n ")}`); + else if (WRITE) console.log((r.stdout || "").trim()); } if (problems.length) { @@ -187,7 +207,7 @@ async function main() { for (const p of problems) console.error(" - " + p + "\n"); return 1; } - console.log(`check-drift: ${PAIRS.length} 对提示词的规则全部认领,${Object.keys(plugin.skillVersions).length} 个 skill 摘要一致`); + console.log(`check-drift: ${PAIRS.length} 对提示词的规则全部认领,${Object.keys(plugin.skillVersions).length} 个 skill 摘要一致,两个宿主 plugin 与真源一致`); return 0; }