Files
wuguanganandClaude Opus 5 b8520c3b14 fix: Claude 勾掉的框不算证据;一条 todo 里的多件事要拆开标注
两条都来自用户的观察,两条我都犯了自己一直在防的错误。

## 勾掉 ≠ 做完

先前把 Claude 勾了 completed 的 todo 直接记成 .confirmed(已验收),
理由是"勾状态是它边干边写进文件的,比嘴上说可信"。这个理由站不住 ——
它勾自己的框和它嘴上说"做完了"是同一性质的东西:都是声明,都没有外部证据。
拿它自己勾的框当证据,正是这整个功能存在的理由所要防的事。

改成落到 .claimed(待复核),证据记成 .claimed(...)(isProof = false),
由旁路复核拿真实 git diff 判 confirmed / disputed。
用户的原话:「他勾掉的到底有没有做」—— 只有 diff 能回答。

顺带补上 baselineCommit:缺了它复核只能拿未提交的改动去比,结论全是噪音。

## 一条 todo 里常常不止一件事

「批次2:设备自动连接(Android 地基修复→状态机→UI→iOS)」是四件。
整条勾掉时里面做了三件还是四件,从条目本身看不出来 ——
而漏掉的往往正是最后那件。

拆分是机械的(按 →、;+/ 等明确分隔符),不调模型:这一步要对每条 todo
都跑,而它要解决的问题很窄。切不出来的("深色模式双端"其实是 iOS+Android)
就不切 —— 切错比不切更糟,用户会对着一堆碎片然后不再相信这个标注。

界面上:行内直接显示「2/4 项」,展开逐项勾。藏在展开区里等于没标,
用户不会为了看有没有分项去逐条展开。

全部分项勾完**不自动**把整条标成已验收 —— 那等于绕过"拿 diff 核对"这一步。

修 bug:标题前缀门槛原来是 12 字,把「把整个权限体系重构一遍:改A、改B」
的前半句当标题剥掉了。中文里 11 个字是实打实的内容,压到 6 字。

411 个测试。

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-07 22:51:03 +08:00

92 lines
3.6 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import XCTest
@testable import HubCore
/// 把一条塞了好几件事的要点拆开。
///
/// 用户的观察:Claude 的一条 todo 里常常不止一件事,整条勾掉时里面
/// 做了三件还是四件从条目本身看不出来 —— **而漏掉的往往正是最后那件**。
final class SubPointSplitTests: XCTestCase {
private func split(_ text: String) -> [String] {
AcceptanceItem.split(text).map(\.text)
}
/// 本机真实数据里的样子。
func testSplitsARealClaudeTodo() {
XCTAssertEqual(
split("批次2:设备自动连接(Android 地基修复→状态机→UI→iOS)"),
["设备自动连接", "Android 地基修复", "状态机", "UI", "iOS"]
)
}
func testSplitsOnCommonDelimiters() {
XCTAssertEqual(split("写数据层、接 UI、补测试"), ["写数据层", "接 UI", "补测试"])
XCTAssertEqual(split("Realtime 冒烟 + /voice/session"), ["Realtime 冒烟", "voice", "session"])
XCTAssertEqual(split("改 A;改 B"), ["改 A", "改 B"])
}
/// **只有一件事就别拆。**
///
/// 返回空数组表示"这条本来就是单一的"—— 不是没拆出来,是不该拆。
/// UI 靠这个区分要不要显示「n 项」徽章。
func testSingleThingIsNotSplit() {
XCTAssertTrue(AcceptanceItem.split("修复登录按钮点不动").isEmpty)
XCTAssertTrue(AcceptanceItem.split("发布新版本").isEmpty)
}
/// **切不出来就不切。** 「深色模式双端」其实是 iOS + Android 两件,
/// 但没有分隔符 —— 硬猜会切错,而切错比不切更糟:
/// 用户会对着一堆莫名其妙的碎片,然后不再相信这个标注。
func testDoesNotGuessWithoutDelimiters() {
XCTAssertTrue(AcceptanceItem.split("批次4:深色模式双端").isEmpty)
}
/// 冒号前的短前缀是标题,不算一件事。
func testDropsShortTitlePrefix() {
XCTAssertEqual(split("阶段一:改 A、改 B"), ["改 A", "改 B"])
}
/// 长前缀本身就是内容,不能当标题剥掉。
func testKeepsLongPrefixesAsContent() {
let parts = split("把整个权限体系重构一遍:改 A、改 B")
XCTAssertTrue(
parts.contains { $0.contains("权限体系") },
"标题前缀是「批次2」那种量级;再长就是内容,剥掉就丢信息了:\(parts)"
)
}
/// 切碎的噪音要滤掉。
func testFiltersOutFragments() {
XCTAssertFalse(split("改 A、、、改 B").contains(""))
}
/// 构造要点时自动拆分,不用调用方记得做。
func testItemSplitsOnConstruction() {
let item = AcceptanceItem(text: "写数据层、接 UI", origin: .assistantTask)
XCTAssertEqual(item.parts.map(\.text), ["写数据层", "接 UI"])
XCTAssertEqual(item.partsTotal, 2)
XCTAssertEqual(item.partsDone, 0)
}
/// 显式传了 parts 就用传进来的(从盘上读回来的场景)。
func testExplicitPartsWin() {
let item = AcceptanceItem(
text: "写数据层、接 UI", origin: .assistantTask,
parts: [SubPoint(text: "写数据层", done: true), SubPoint(text: "接 UI")]
)
XCTAssertEqual(item.partsDone, 1)
}
/// 没有分项的要点,进度按整条算。
func testUnsplitItemCountsAsOne() {
var item = AcceptanceItem(text: "发布新版本", origin: .userPrompt)
XCTAssertEqual(item.partsTotal, 1)
XCTAssertEqual(item.partsDone, 0)
item.status = .confirmed
XCTAssertEqual(item.partsDone, 1)
}
}