fix(rectification): plain-word opening with examples in the stem, plain de-duplicated step receipt (BUG-1049, BUG-1050)

- BUG-1049 (recurrence of BUG-504): the opening stem carries six examples and
  an example answer again and is server-owned on the zero-evidence opening;
  the body is two plain sentences (no 大运/盘面/代表分钟/精确到秒, no year, no
  question). Stem de-dup compares whole sentences / near-equality instead of a
  12-char prefix, which had deleted the body's examples sentence since
  aa7ccb30 (BUG-604) + dd8f35f7 (BUG-648).
- BUG-1050: plain step labels; a finished step label shows once and
  「已完成 N 步」counts shown rows; failed rows read 「…未完成」 from the
  in-progress wording.
- Skill 10.0.30 -> 10.0.31 (OpeningPolicy); 10.0.30 kept as deprecated.
- VOICE / DESIGN / CHANGELOG / BUG_HISTORY / PROGRESS / real-device checklist
  and screenshots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017eEAG8HD3mm8gsKXgk8uU8
This commit is contained in:
Jesse_Chen
2026-09-26 22:04:56 +08:00
co-authored by Claude Opus 5.5
parent e53052a258
commit 8fc1a5bded
56 changed files with 1562 additions and 134 deletions
@@ -529,7 +529,8 @@ test("zero-evidence opening persists the server-owned first question", async ()
// 原因:BUG-604
assert.equal(spokenFollowupForUser(followup), GENERIC_COLLECT_QUESTION);
assert.doesNotMatch(spokenFollowupForUser(followup) ?? "", /^也可以再/);
assert.match(spokenFollowupForUser(followup) ?? "", /先说你最容易想起的一两件/);
// 原值: /先说你最容易想起的一两件/ / 新值: /先说一两件你记得的大事,比如/ / 原因: BUG-1049(复发自 BUG-504)开场题干带例子
assert.match(spokenFollowupForUser(followup) ?? "", /先说一两件你记得的大事,比如/);
const expected = GENERIC_COLLECT_QUESTION;
const accounting = fakeAccounting({
@@ -620,10 +621,21 @@ test("GENERIC_COLLECT_QUESTION invites one or two memories without listing years
// 旧:从你最容易想起来的一件事开始就好——比如哪年上的大学、哪年换的工作、哪年搬的家
// 新:先说你最容易想起的一两件,年月大概就行。领域清单在开场正文。
// 原因:BUG-604 决策 1,题干不再列年份例子
assert.equal(GENERIC_COLLECT_QUESTION, "先说你最容易想起的一两件,年月大概就行。");
assert.doesNotMatch(GENERIC_COLLECT_QUESTION, /比如/);
assert.doesNotMatch(GENERIC_COLLECT_QUESTION, /(?:19|20)\d{2}/);
assert.match(source, /GENERIC_COLLECT_QUESTION = "先说你最容易想起的一两件,年月大概就行。"/);
// 原值: "先说你最容易想起的一两件,年月大概就行。",断言不含「比如」、不含任何四位年份
// 新值: 带六个例子(不带年份)+ 一个固定示例回答「2015 年夏天换了工作」;含「比如」「例如」;唯一的年份在示例回答的「」里
// 原因: BUG-1049(复发自 BUG-504,防复发「开场题文案必须带具体例子」):BUG-604 把例子挪进正文后被题干去重删掉,用户看到的开场没有例子。产品 2026-09-26 D1 决定例子回到题干并加示例回答
assert.equal(
GENERIC_COLLECT_QUESTION,
"先说一两件你记得的大事,比如上大学、第一份工作、搬到别的城市、谈恋爱或结婚、家里添丁、生病住院。说个大概年月就行,例如「2015 年夏天换了工作」。",
);
assert.match(GENERIC_COLLECT_QUESTION, /比如/);
assert.match(GENERIC_COLLECT_QUESTION, /例如/);
for (const example of ["上大学", "第一份工作", "搬到别的城市", "谈恋爱或结婚", "家里添丁", "生病住院"]) {
assert.ok(GENERIC_COLLECT_QUESTION.includes(example), example);
}
assert.deepEqual(GENERIC_COLLECT_QUESTION.match(/(?:19|20)\d{2}/g), ["2015"]);
assert.match(GENERIC_COLLECT_QUESTION, /「2015 年夏天换了工作」/);
assert.match(source, /GENERIC_COLLECT_QUESTION = "先说一两件你记得的大事,比如上大学、第一份工作、搬到别的城市、谈恋爱或结婚、家里添丁、生病住院。说个大概年月就行,例如「2015 年夏天换了工作」。"/);
});
test("degraded spoken collect does not keep discriminator identity or block a later card", async () => {