AI SDLC: 使用 mattpocock-skills 进行适老化计算器 macOS 原生应用开发
安装
npx skills add mattpocock/skills
初始化(/setup-matt-pocock-skills)
/setup-matt-pocock-skills
在你的 agent 中,每个仓库运行一次。它会:
- 问你想使用哪个 issue tracker(GitHub、Linear 或本地文件)
- 问你在 triage ticket 时会给它们打什么标签(
/triage会用到标签) - 问你想把我们创建的文档保存在哪里
目录结构
.
├── CLAUDE.md
└── docs
└── agents
├── domain.md
├── issue-tracker.md
└── triage-labels.md
软件开发工作流程
grill-with-docs → to-spec → to-tickets → implement (tdd → code-review) → code-review
/grill-with-docs
description
一场为锐化计划或设计而不留情面的访谈,过程中还会同步创建文档(ADR 与术语表)。调用 Skill 工具两次,分别传入 “grilling” 和 “domain-modeling”。
详细介绍
grill-with-docs 就一个方案或设计对你进行访谈,直到你和 agent 对它共享同一个理解,并且在访谈过程中把词汇和硬决策写进你的仓库。它和 grill-me 跑的是同一场访谈(一轮问题,然后等待,然后下一轮),只是对准了一个 codebase。
它是 有状态的(stateful)。其他所有 grilling skill 都把 session 留在你脑子里;这一份把文件留在磁盘上。一个术语被敲定,它落地到 CONTEXT.md 的时机就是它被敲定的那一刻,而不是最后批量补交。一个决策通过三道闸门,它就以一条 ADR 落地。这就是全部的差别,也是人们用这个 skill 时大部分麻烦的来源:这些工件是真实仓库里的真实文件,所以它们可以在你以为存在时缺席,也可能在不止一个人往里写的时候漂移。
- 访谈中如果冒出”只能在纸上吵不出结果”的问题(比如新接口的手感),按主流程第 2 步绕道
/prototype。 - 访谈完成后看规模:
- 一个会话能做完 → 直接
/implement(内部驱动/tdd和/code-review)。 - 多会话 →
/to-spec→/to-tickets→ 逐 ticket/implement。
- 一个会话能做完 → 直接
Spec 和 Tickets
Spec 和 tickets 的存在理由:对抗上下文丢失
看主流程的 “Context hygiene” 规则(ask-matt/SKILL.md:28-32):
整条规则围绕一个事实:
/implement是在新窗口里冷启动的,它看不到你 grill 时的任何讨论。所以多会话构建必须把思考”固化”成可携带的制品:
- spec = 把盘问结论压缩成一份能跨窗口传递的文档(to-spec/SKILL.md:明确说 “no interview, just synthesis”,它不是思考工具,是序列化工具)
- tickets = 把 spec 切成自包含的块,让每次
/implement只带一块上下文,上一个 ticket 的上下文可以丢弃
单会话构建里,盘问结论还活在你当前的上下文窗口里,/implement 直接就在同一个窗口接着做。此时再写 spec,等于把模型已经知道的东西序列化一遍再让它读回来,纯属搬运。
单会话路径并非”零存档”
不要以为不留 spec 就是不留记录。持久的纸面记录由 /grill-with-docs 在盘问过程中产生,跟会话长短无关:
- CONTEXT.md:领域词汇表(glossary),术语对齐沉淀在这里
- ADR:难逆转的架构决策,比如”播报词决策与合成器分层,接缝切在哪”,这种决策会落成 ADR
也就是说,“为什么这么做”永远落盘(ADR + 词汇表);“具体做什么”只在需要跨窗口传递时才落盘(spec + tickets)。后者是易腐的执行计划,落成文档反而容易过时(to-spec 模板里特意禁止写文件路径和代码片段,就是这个原因)。
两个兜底机制
- smart zone:如果 grill 到一半发现这个重构比预想的大,会话逼近 ~150k token 的清晰思考区,就按 phase boundary 规则
/compact,或者升级路径,补走/to-spec。单会话判断不是签生死状,中途可以改道。 /code-review不省略:/implement收尾时仍会对 diff 做 Standards + Spec 双轴审查。单会话里”Spec”这一轴对照的就是你 grill 出来的结论(包括 ADR),所以审查环节一个不少。
一句话总结:spec 和 tickets 是跨会话的通信协议,不是流程的仪式感。会话内能做完的事,ADR 和词汇表已经留下了需要留下的东西。
会话
😀
/mattpocock-skills:grill-with-docs 给老人开发一款计算器 macOS 原生应用。
🤖
好的,环境已确认:目录为空(全新项目),已安装 Xcode 26.6 / Swift 6.3.3 / macOS 26。开发条件齐全。
现在我们开始第一轮”拷问”,把设计树的最外层分支铺开:
❓ Q1 - 目标用户的具体画像:这款计算器是给”老人”用的,但”老人”之间差异很大。请描述一下实际使用者的情况,比如:
- 是给特定的人(如您的父母/长辈)还是泛指老年群体?
- 他们的视力情况如何?(老花眼?白内障?严重视障?)
- 手部灵活性如何?(是否有手抖、关节炎,点按小目标困难?)
- 听力如何?(语音播报是否有意义?)
- 对电脑/鼠标的熟悉程度?(会用鼠标点击吗,还是只习惯触屏/实体计算器?)
➡️ 我的推荐:按典型场景假设——60-80 岁、有老花眼、能用鼠标或触控板点击大按钮、听力正常或轻度下降、习惯使用实体计算器。后续所有设计(字号、按钮尺寸、语音播报)都围绕这个画像展开。
❓ Q2 - 功能范围:计算器的功能边界在哪里?
- A. 极简:只有加减乘除、小数点、清除、等号
- B. 实用型:在 A 基础上加百分比、正负号、退格(逐位删除)、记忆功能(M+/M-/MR/MC)
- C. 增强型:在 B 基础上加计算历史记录(老人常需要核对”刚才算的什么”)
➡️ 我的推荐:C。历史记录对老人特别有价值——他们经常算完就忘了输入过程,需要回看核对。但要设计成”自动记录、一键查看”,而不是额外学习成本。
❓ Q3 - 界面语言:界面上的文字(按钮标签、菜单、提示)用什么语言?
➡️ 我的推荐:全中文界面,按钮用”加、减、乘、除、等于、清除”等中文字样(或”+ − × ÷“符号配中文辅助),避免英文 AC、M+ 这类老人看不懂的缩写。
❓ Q4 - 技术栈:macOS 原生应用有两种主流方案:
- SwiftUI:声明式 UI,开发快,做大字体/高对比度界面很顺手,与 macOS 26 深度集成
- AppKit:更成熟的命令式框架,对窗口行为控制更精细,但开发速度慢
➡️ 我的推荐:SwiftUI。你的环境是 Xcode 26.6 / Swift 6.3.3,SwiftUI 已非常成熟;这个应用的 UI 复杂度不高,SwiftUI 的开发效率和可维护性优势明显。
❓ Q5 - 分发方式:这个应用开发完之后怎么到达用户手里?
- A. 本地自用:直接构建后在您家人/自己的 Mac 上运行,不签名不公证
- B. 内部分发:打包成 .app 或 DMG 发给几位亲友(可能需要处理 Gatekeeper 提示)
- C. App Store 上架:面向公众(需要开发者账号、审核,周期长)
➡️ 我的推荐:A 或 B(先本地自用)。适老化应用最需要真实用户反馈迭代,先把产品做对,再考虑上架。
❓ Q6 - “适老化”的核心手段:适老设计有很多手段,哪些是您心目中的”必须有”?(可多选)
- 超大字体和超大按钮
- 高对比度配色(如深色背景配亮黄/白色数字)
- 语音播报(按键时报出数字和运算符,结果报读,如”一万两千三百”)
- 大号显示历史算式(显示”12 + 5 =“而不只是结果)
- 按键声音/触觉反馈
- 防误触设计(如双击才生效的清除键)
➡️ 我的推荐:超大字体按钮 + 高对比度 + 语音播报 + 完整算式显示。其中中文语音播报是适老计算器的灵魂功能——老人看不清时”听得见”比”看得见”更可靠。
请逐条回答(可以直接说”都按推荐”或针对某几条给出不同选择)。
😀
都按推荐
🤖
第一轮全部按推荐定案。先把已澄清的领域术语落进 CONTEXT.md(术语表),然后进入第二轮。
Write CONTEXT.md
# CONTEXT.md — 领域术语表
本项目是一款面向老年用户的 macOS 原生计算器应用。本文件只记录领域术语与共识,不含实现细节。
## 术语
### 目标用户(老年用户)
60–80 岁、有老花眼、听力正常或轻度下降、能使用鼠标或触控板点击大按钮、习惯使用实体计算器的人群。一切界面与交互决策以该画像为基准。
### 适老化设计
围绕目标用户的一组默认体验原则:超大字体与超大按钮、高对比度配色、语音播报、完整算式显示、按键声音反馈、防误触设计。这些不是可选功能,而是产品的核心定位。
### 语音播报
在用户操作时用中文语音朗读输入与结果,让"看不清"的用户可以"听得见"。适老化设计的核心功能。
### 完整算式显示
界面上始终显示当前完整算式(如"12 + 5 ="),而不只显示一个孤零零的数字,方便用户核对输入过程。
### 计算历史(历史记录)
自动记录每一次完成的计算(算式 + 结果),用户可随时回看核对。强调"自动记录、一键查看",不要求用户学习任何新操作。
### 记忆功能
传统计算器的 M+/M−/MR/MC 能力,允许把一个数字暂存起来稍后取用。
### 功能范围
基础四则运算(加、减、乘、除)、小数点、百分比、正负号、退格(逐位删除)、清除、等于,加上记忆功能与计算历史。不做科学计算。
### 防误触设计
对破坏性操作(如清除)采取防止误点的机制,避免用户因手抖误清掉输入内容。
术语已落档。第一轮的答案把设计树往下推了一层,现在问第二轮:
❓ Q1 - 语音播报的粒度与读法:播报多频繁、数字怎么念?
- 粒度 A:每按一个键就报(“一""二""加""五""等于""十七”)——反馈最密,但操作时话多
- 粒度 B:只报关键节点(运算符 + 结果)——安静一些
- 读法 A:按数值念,“17” 读”十七”、“12345” 读”一万两千三百四十五”——符合老人听钱数/账目的习惯
- 读法 B:按位念,“12345” 读”一二三四五”——实现简单,但大数字听着别扭
➡️ 我的推荐:粒度 A + 读法 A。按数值念是老人最自然的听觉习惯(他们平时说”一万二”而不是”一二零零零”);每个键都报能让视力差的用户不看屏幕也能操作。播报可在设置里关闭。
❓ Q2 - 记忆功能的呈现:M+/M−/MR/MC 这套缩写老人大概率看不懂。如何处理?
- A. 中文化标签:按钮写”存入记忆""记忆相加""取出记忆""清除记忆”,配大字
- B. 砍掉记忆功能:历史记录已经能回看之前的数字,记忆功能学习成本高、用处重叠
- C. 折中:只保留一个”暂存/取出”按钮(单一记忆位,无加减),中文标签
➡️ 我的推荐:B(砍掉)。历史记录已经覆盖”之前算的数字想再用”的场景;记忆键的隐式状态(屏幕上还得多一个 M 指示灯)对老人是纯负担。第一轮选 C 是冲着历史记录去的,记忆功能可以果断舍弃。
❓ Q3 - 窗口形态与布局:
- A. 固定尺寸大窗口:打开就是一个大窗口(约 800×1000),不能调小,避免用户误拖变小
- B. 可调窗口:允许缩放,字体按钮随窗口变大而变大
- C. 默认全屏或接近全屏
布局上数字键区放下面、显示屏在上面(实体计算器布局),按钮为正方形大格。
➡️ 我的推荐:B(可调窗口),默认尺寸大,最小尺寸设一个下限(保证按钮不会小到点不中)。适老应用用户可能想拖到全屏,缩放时字体同步放大是 SwiftUI 的强项。
❓ Q4 - 配色方案:
- A. 固定深色高对比:黑底、白色/亮黄色大数字、橙色运算符——对老花眼最友好,弱光下不刺眼
- B. 固定浅色:白底黑字——更接近纸质账本的观感
- C. 跟随系统深浅色 + 应用内可手动切换
➡️ 我的推荐:A(固定深色高对比),并在设置里提供切换到浅色高对比的选项。适老化不是”好看”,是”看得清”,深色底白黄字对比度最高。
❓ Q5 - 历史记录的形态:
- A. 主屏顶部滚动带:显示屏上方始终显示最近几条算式,最显眼但占屏幕空间
- B. 侧边抽屉:平时隐藏,点”历史”大按钮滑出列表
- C. 独立窗口/面板
另外:历史保留多少条?关掉应用后还保留吗?
➡️ 我的推荐:B(侧边抽屉),主屏保持极简,一个大”历史”按钮点开看。保留最近 50 条,关闭应用后清空(session 级)——持久保存会让”历史”变成又一份需要管理的旧账,反而增加困惑;如果后续真实用户想要”记下来”,再加持久化。
❓ Q6 - 键盘输入:是否支持物理键盘数字键直接输入?
- A. 支持:数字键、+-*/、回车(等于)、退格都能用
- B. 不支持:只允许点屏幕按钮
➡️ 我的推荐:A。成本很低,且照顾两类用户:会打字的老人用键盘更准,手抖的用户避免误点。
❓ Q7 - 错误与边界的处理:以下情况怎么办——
- 除以零:显示”不能除以零”中文提示 + 语音播报,自动回到可继续输入状态?
- 数字太长:超过 12 位(显示屏放得下、且老人实际用不到更大)就拒绝输入并提示?
- 结果溢出/精度:结果超过显示范围时用”亿/万”中文单位读出(语音),屏幕上显示科学计数法还是截断?
➡️ 我的推荐:除以零 → 大字中文提示”不能除以零” + 语音播报 + 按任意键恢复;输入最多 12 位;结果显示最多保留 10 位有效数字并智能去尾零,语音播报用”万/亿”中文单位读,屏幕仍显示阿拉伯数字。
请逐条回答(“都按推荐”即可,或针对某条给出不同选择)。
😀
都按推荐
🤖
第二轮也全部定案。先把新沉淀的术语更新进 CONTEXT.md(包括”砍掉记忆功能”这个重要取舍),然后进入第三轮。
Write CONTEXT.md
# CONTEXT.md — 领域术语表
本项目是一款面向老年用户的 macOS 原生计算器应用。本文件只记录领域术语与共识,不含实现细节。
## 术语
### 目标用户(老年用户)
60–80 岁、有老花眼、听力正常或轻度下降、能使用鼠标或触控板点击大按钮、习惯使用实体计算器的人群。一切界面与交互决策以该画像为基准。
### 适老化设计
围绕目标用户的一组默认体验原则:超大字体与超大按钮、高对比度配色、语音播报、完整算式显示、按键声音反馈、防误触设计。这些不是可选功能,而是产品的核心定位。
### 语音播报
在用户操作时用中文语音朗读输入与结果,让"看不清"的用户可以"听得见"。适老化设计的核心功能。
- **粒度**:每按一个键都播报(数字、运算符、结果都报)。
- **读法**:按数值念,如 17 读"十七"、12345 读"一万两千三百四十五",符合老人听账目的习惯;大数字用"万/亿"等中文单位。
- 可在设置中关闭。
### 完整算式显示
界面上始终显示当前完整算式(如"12 + 5 ="),而不只显示一个孤零零的数字,方便用户核对输入过程。
### 计算历史(历史记录)
自动记录每一次完成的计算(算式 + 结果),用户可随时回看核对。强调"自动记录、一键查看"。
- 形态:侧边抽屉,主屏上一个大"历史"按钮点开。
- 保留最近 50 条;关闭应用后清空(不持久保存)。
### 功能范围
基础四则运算(加、减、乘、除)、小数点、百分比、正负号、退格(逐位删除)、清除、等于,加上计算历史。不做科学计算,**不做记忆功能**(M+/M− 等)——历史记录已覆盖"之前算的数字想再用"的场景,记忆键的隐式状态对老人是纯负担。
### 窗口与布局
可调尺寸窗口,默认尺寸大,设最小尺寸下限保证按钮始终可点中;字体与按钮随窗口缩放同步变大。布局仿实体计算器:显示屏在上、数字键区在下。
### 配色
默认固定深色高对比:黑底、白色/亮黄色大数字、橙色运算符。设置中可切换到浅色高对比。不跟随系统深浅色自动切换。
### 键盘输入
支持物理键盘:数字键、加减乘除键、回车(等于)、退格。照顾会打字的用户,也让手抖用户避免误点。
### 错误与边界
- 除以零:大字中文提示"不能除以零" + 语音播报,按任意键恢复输入。
- 输入数字最多 12 位,超过则拒绝输入并提示。
- 结果最多保留 10 位有效数字并智能去尾零;屏幕显示阿拉伯数字,语音按数值(含万/亿单位)朗读。
### 防误触设计
对破坏性操作(如清除)采取防止误点的机制,避免用户因手抖误清掉输入内容。
术语表已更新。第二轮的答案又把设计树往下推了一层,第三轮问更深一层的分支:
❓ Q1 - 计算语义:按实体计算器的习惯还是按数学表达式的习惯?
- A. 实体计算器式(链式立即执行):按”12 + 5 =“得 17,接着按”+ 3 =“在 17 基础上继续得 20。运算符按下时立即算出中间结果
- B. 表达式式:输入”12 + 5 × 2”按等于后按运算优先级算得 22
同样地,百分比语义:
- A. 实体计算器式:“100 + 10 %” = 110(加 100 的 10%),符合老人算”加一成”的直觉
- B. 数学式:“100 + 10 %” = 100.1
➡️ 我的推荐:两者都选 A(实体计算器式)。目标用户习惯的就是实体计算器,“12 + 5 × 2 = 34”在他们看来是理所应当的;表达式优先级反而会让他们觉得”算错了”。
❓ Q2 - 清除键的防误触机制:第一轮定了要防误触,具体怎么做?
- A. 双击/长按才生效:单击无效——但双击和长按对手抖的老人其实更难
- B. 视觉隔离 + 语音确认:“清除”键用醒目红色、与数字键拉开距离,按下后立即生效并播报”已清除”
- C. 分级清除:一个”退格”键(删一位,高频、无风险)+ 一个”清除”键(清全部,红色隔离),配合 B 的播报
➡️ 我的推荐:C。防误触的关键不是让清除”难按”,而是让误按的代价低、且用户知道发生了什么。红色隔离降低误触概率,语音播报”已清除”消除”刚才发生了什么”的困惑。
❓ Q3 - 大数字的屏幕显示:显示屏上数字要不要千分位?
- A. 带千分位:显示”12,345”——便于核对位数
- B. 不带:显示”12345”——更接近实体计算器,但 7 位以上很难读
➡️ 我的推荐:A(带千分位)。老人算账常涉及成千上万,“1,234,567”比”1234567”好认太多;输入时千分位随数字增加自动重排。语音按数值念不受影响。
❓ Q4 - 设置项清单与播报语速:设置面板里放什么?
- 语音播报开关(开/关)
- 播报语速(慢 / 标准两档——老人普遍需要偏慢的语速)
- 配色(深色高对比 / 浅色高对比)
- 还要不要独立的字号档位?
➡️ 我的推荐:就放这三项,不加独立字号档位——字号随窗口缩放(第二轮已定),默认语速用”慢”。设置项每多一个,困惑就多一分。设置入口是一个齿轮大按钮,不用菜单栏。
❓ Q5 - 测试策略:哪些东西要自动化测试?
- 计算引擎(四则、百分比、正负号、退格、链式语义、除以零)——纯逻辑,可单测
- 中文数字朗读转换(12345 → “一万两千三百四十五”)——规则复杂易错,是本项目最容易出 bug 的部分
- UI 部分——SwiftUI 视图单测性价比低
➡️ 我的推荐:计算引擎 + 中文朗读转换两个模块用 TDD 开发(先写测试再实现),配 Xcode 单元测试 target;UI 和语音播报效果手工验证。
❓ Q6 - 应用名称与工程命名:
- 界面显示名(Dock、窗口标题):中文名,如”长辈计算器""暖心计算器”?
- Xcode 工程名/bundle id:英文或拼音,如
ElderCalculator/com.wangjunjian.eldercalculator?
➡️ 我的推荐:显示名”长辈计算器”(直白、一看就懂);工程名 ElderCalculator,bundle id com.wangjunjian.ElderCalculator。
❓ Q7 - 窗口关闭行为:点窗口红叉后——
- A. 直接退出应用(单窗口工具的常见行为,像系统计算器)
- B. 窗口关闭但应用留在 Dock,点 Dock 图标重开
➡️ 我的推荐:A。对老人来说”关掉就是关掉”最符合直觉,避免”窗口没了但程序还在跑”的困惑。
请逐条回答。
😀
都按推荐
🤖
第三轮也全部定案。更新 CONTEXT.md,然后进入第四轮。
Write CONTEXT.md
# CONTEXT.md — 领域术语表
本项目是一款面向老年用户的 macOS 原生计算器应用,名为"长辈计算器"。本文件只记录领域术语与共识,不含实现细节。
## 术语
### 目标用户(老年用户)
60–80 岁、有老花眼、听力正常或轻度下降、能使用鼠标或触控板点击大按钮、习惯使用实体计算器的人群。一切界面与交互决策以该画像为基准。
### 适老化设计
围绕目标用户的一组默认体验原则:超大字体与超大按钮、高对比度配色、语音播报、完整算式显示、防误触设计。这些不是可选功能,而是产品的核心定位。
### 语音播报
在用户操作时用中文语音朗读输入与结果,让"看不清"的用户可以"听得见"。适老化设计的核心功能,也是唯一的听觉反馈(不另做按键音效)。
- **粒度**:每按一个键都播报(数字、运算符、结果都报)。
- **读法**:按数值念,如 17 读"十七"、12345 读"一万两千三百四十五";大数字用"万/亿"等中文单位。
- 可在设置中关闭;语速有"慢 / 标准"两档,默认"慢"。
### 完整算式显示
界面上始终显示当前完整算式(如"12 + 5 ="),而不只显示一个孤零零的数字,方便用户核对输入过程。
### 计算语义
完全仿实体计算器:
- **链式立即执行**:按运算符即算出中间结果;"12 + 5 ="得 17 后按"+ 3 ="在 17 基础上继续得 20。不做运算符优先级,"12 + 5 × 2"按顺序得 34。
- **百分比**:按"加一成"的直觉,"100 + 10 %" = 110。
### 千分位显示
屏幕上的数字带千分位分隔符(如"1,234,567"),输入时随位数增加自动重排,便于核对大数。
### 计算历史(历史记录)
自动记录每一次完成的计算(算式 + 结果),用户可随时回看核对。强调"自动记录、一键查看"。
- 形态:侧边抽屉,主屏上一个大"历史"按钮点开。
- 保留最近 50 条;关闭应用后清空(不持久保存)。
### 功能范围
基础四则运算(加、减、乘、除)、小数点、百分比、正负号、退格(逐位删除)、清除、等于,加上计算历史。不做科学计算,**不做记忆功能**(M+/M− 等)——历史记录已覆盖"之前算的数字想再用"的场景,记忆键的隐式状态对老人是纯负担。
### 分级清除与防误触
- **退格**:删一位,高频操作,无风险。
- **清除**:清全部,用醒目红色、与数字键视觉隔离以降低误触概率;按下立即生效并语音播报"已清除",让误按的代价低且用户知道发生了什么。不用双击/长按确认——那对手抖的老人更难。
### 窗口与布局
可调尺寸窗口,默认尺寸大,设最小尺寸下限保证按钮始终可点中;字体与按钮随窗口缩放同步变大(不提供独立字号设置)。布局仿实体计算器:显示屏在上、数字键区在下。点窗口关闭按钮即退出应用("关掉就是关掉")。
### 配色
默认固定深色高对比:黑底、白色/亮黄色大数字、橙色运算符。设置中可切换到浅色高对比。不跟随系统深浅色自动切换。
### 设置项
只有三项:语音播报开关、播报语速、配色。设置入口是主屏上的齿轮大按钮。
### 键盘输入
支持物理键盘:数字键、加减乘除键、回车(等于)、退格。照顾会打字的用户,也让手抖用户避免误点。
### 错误与边界
- 除以零:大字中文提示"不能除以零" + 语音播报,按任意键恢复输入。
- 输入数字最多 12 位,超过则拒绝输入并提示。
- 结果最多保留 10 位有效数字并智能去尾零;屏幕显示阿拉伯数字(带千分位),语音按数值(含万/亿单位)朗读。
第四轮,问剩余的交互细节分支:
❓ Q1 - 支持的最低系统版本:老人的 Mac 系统版本可能比较旧。
- A. macOS 15+:覆盖 2023 年后的系统,适老功能(语音、SwiftUI)完全够用
- B. macOS 26 only:能用最新 API,但旧 Mac 跑不了
- C. macOS 13+:覆盖更广,但要绕开一些新 API
➡️ 我的推荐:A(macOS 15+)。在兼容性和开发效率之间取平衡;如果之后明确目标用户的 Mac 更新,再收紧。
❓ Q2 - 历史记录条目的交互:点开历史抽屉后,每条历史能做什么?
- A. 只读回看:纯列表,不能点——最简单,无学习成本
- B. 点条目把结果带回主屏:继续用这个数往下算——有用但可能误触后困惑”屏幕怎么变了”
- C. A + 每条一个”复制”小按钮
➡️ 我的推荐:A(只读)。回看核对是核心需求,已满足;带回主屏的交互对老人是迷惑性大于便利性。
❓ Q3 - 结果的复制:主屏上的计算结果要不要能复制到剪贴板(比如发给家人)?
- A. 不做:结果靠看、靠语音播报就够了
- B. 做一个”复制”大按钮:又多一个按钮,占地方
- C. 支持 ⌘C 复制但不放按钮:给会用键盘的人留后门
➡️ 我的推荐:A(不做)。“把结果告诉家人”已由语音播报覆盖;真有复制需求的场景极少,不值得占用界面空间。
❓ Q4 - 重复按等号:“12 + 5 =“显示 17 后,再按一次”=“会怎样?
- A. 实体计算器式:重复上次运算——再按得 22、再按得 27
- B. 无操作:结果不变,一直是 17
➡️ 我的推荐:A。这是实体计算器的标准行为,习惯用实体计算器的老人会期待它;没这个习惯的用户多按一次看到数字变化也无害(算式带会显示发生了什么)。
❓ Q5 - 输入细节打包确认(都是实体计算器的标准行为,确认即可):
- 直接按小数点输入”.5”自动显示为”0.5”
- 一个数字里禁止第二个小数点
- 正负号键作用于当前正在输入的数字
- 连续按两个运算符(如”12 + −“)后一个替换前一个
- 算完出结果后直接按数字键,开始新一笔计算(旧结果进历史)
➡️ 我的推荐:全部按上述标准行为。
❓ Q6 - 首次启动体验:第一次打开应用时——
- A. 直接可用:打开就是计算器主屏,无启动页、无教程、无欢迎语
- B. 首次打开播一句欢迎语:如”长辈计算器已打开”
➡️ 我的推荐:A。老人不需要教程——界面本身就是实体计算器的放大版;欢迎语每次听是打扰,只听一次又没意义。
❓ Q7 - 应用图标:Dock 图标怎么处理?
- A. 简单自制:大数字”123”或计算器符号的简洁图标,开发期间顺手做
- B. 先用占位图标:功能优先,图标最后再说
➡️ 我的推荐:A(简单自制),用 SF Symbols 风格的计算器图形 + 高对比配色,与适老定位一致;不追求完美,能用即可。
请逐条回答。
😀
都按推荐
🤖
第四轮定案。把最后一批术语沉淀进 CONTEXT.md:
Write CONTEXT.md
# CONTEXT.md — 领域术语表
本项目是一款面向老年用户的 macOS 原生计算器应用,名为"长辈计算器"。本文件只记录领域术语与共识,不含实现细节。
## 术语
### 目标用户(老年用户)
60–80 岁、有老花眼、听力正常或轻度下降、能使用鼠标或触控板点击大按钮、习惯使用实体计算器的人群。一切界面与交互决策以该画像为基准。
### 适老化设计
围绕目标用户的一组默认体验原则:超大字体与超大按钮、高对比度配色、语音播报、完整算式显示、防误触设计。这些不是可选功能,而是产品的核心定位。
### 语音播报
在用户操作时用中文语音朗读输入与结果,让"看不清"的用户可以"听得见"。适老化设计的核心功能,也是唯一的听觉反馈(不另做按键音效)。
- **粒度**:每按一个键都播报(数字、运算符、结果都报)。
- **读法**:按数值念,如 17 读"十七"、12345 读"一万两千三百四十五";大数字用"万/亿"等中文单位。
- 可在设置中关闭;语速有"慢 / 标准"两档,默认"慢"。
### 完整算式显示
界面上始终显示当前完整算式(如"12 + 5 ="),而不只显示一个孤零零的数字,方便用户核对输入过程。
### 计算语义
完全仿实体计算器:
- **链式立即执行**:按运算符即算出中间结果;"12 + 5 ="得 17 后按"+ 3 ="在 17 基础上继续得 20。不做运算符优先级,"12 + 5 × 2"按顺序得 34。
- **百分比**:按"加一成"的直觉,"100 + 10 %" = 110。
- **重复等号**:算出结果后再按"="重复上次运算,如"12 + 5 ="得 17,再按"="得 22、再按得 27。
- **输入规则**:直接按小数点输入".5"自动显示为"0.5";一个数字禁止第二个小数点;正负号键作用于当前输入的数字;连续按两个运算符,后一个替换前一个;出结果后直接按数字键则开始新一笔计算,旧结果自动进入历史。
### 千分位显示
屏幕上的数字带千分位分隔符(如"1,234,567"),输入时随位数增加自动重排,便于核对大数。
### 计算历史(历史记录)
自动记录每一次完成的计算(算式 + 结果),用户可随时回看核对。强调"自动记录、一键查看"。
- 形态:侧边抽屉,主屏上一个大"历史"按钮点开。
- 条目**只读回看**,不可点击带回主屏、不提供复制。
- 保留最近 50 条;关闭应用后清空(不持久保存)。
### 功能范围
基础四则运算(加、减、乘、除)、小数点、百分比、正负号、退格(逐位删除)、清除、等于,加上计算历史。不做科学计算,**不做记忆功能**(M+/M− 等)——历史记录已覆盖"之前算的数字想再用"的场景,记忆键的隐式状态对老人是纯负担。
### 分级清除与防误触
- **退格**:删一位,高频操作,无风险。
- **清除**:清全部,用醒目红色、与数字键视觉隔离以降低误触概率;按下立即生效并语音播报"已清除",让误按的代价低且用户知道发生了什么。不用双击/长按确认——那对手抖的老人更难。
### 窗口与布局
可调尺寸窗口,默认尺寸大,设最小尺寸下限保证按钮始终可点中;字体与按钮随窗口缩放同步变大(不提供独立字号设置)。布局仿实体计算器:显示屏在上、数字键区在下。点窗口关闭按钮即退出应用("关掉就是关掉")。
### 配色
默认固定深色高对比:黑底、白色/亮黄色大数字、橙色运算符。设置中可切换到浅色高对比。不跟随系统深浅色自动切换。
### 设置项
只有三项:语音播报开关、播报语速、配色。设置入口是主屏上的齿轮大按钮。
### 键盘输入
支持物理键盘:数字键、加减乘除键、回车(等于)、退格。照顾会打字的用户,也让手抖用户避免误点。
### 结果的使用
结果靠看(大屏幕、千分位)和听(语音播报)获取,**不提供复制到剪贴板**的功能。
### 首次启动
打开即是计算器主屏,直接可用。无启动页、无教程、无欢迎语——界面本身就是实体计算器的放大版。
### 错误与边界
- 除以零:大字中文提示"不能除以零" + 语音播报,按任意键恢复输入。
- 输入数字最多 12 位,超过则拒绝输入并提示。
- 结果最多保留 10 位有效数字并智能去尾零;屏幕显示阿拉伯数字(带千分位),语音按数值(含万/亿单位)朗读。
四轮下来,设计树的分支已经全部走完,frontier 已清空。以下是我们达成的完整共识,请你确认:
共识总结 —— “长辈计算器”
定位:面向 60–80 岁老花眼老年用户的 macOS 原生计算器,适老化设计(超大字体按钮、高对比、语音播报、完整算式显示、防误触)是核心定位而非附加功能。
功能:四则运算、小数、百分比、正负号、退格、清除、等于 + 自动计算历史。砍掉记忆功能(M+/M−),不做科学计算,不做结果复制。
交互语义:完全仿实体计算器——链式立即执行(无运算优先级)、“100+10%=110”、重复等号重复上次运算、千分位显示、标准输入规则。
界面:全中文;仿实体计算器布局;可调大窗口(有最小尺寸,字体随缩放);默认深色高对比(黑底白/黄字、橙色运算符),可切浅色;设置仅三项(语音开关、语速、配色),齿轮大按钮进入。
语音播报:每键播报、按数值念(“一万两千三百四十五”、万/亿单位)、默认慢速、可关闭。是唯一听觉反馈,不另做按键音效。
历史:侧边抽屉、只读、最近 50 条、关闭应用清空。
防误触:退格(删一位)+ 红色隔离的清除键(立即生效 + 播报”已清除”),不做双击/长按确认。
错误处理:除以零中文大字提示 + 播报、任意键恢复;输入最多 12 位;结果 10 位有效数字去尾零。
技术:SwiftUI、macOS 15+、工程名 ElderCalculator、bundle id com.wangjunjian.ElderCalculator、窗口关闭即退出、首次启动直接可用、简单自制图标。计算引擎与中文数字朗读转换两个核心模块用 TDD 开发并配单元测试,UI 手工验证。
文档:以上领域共识已沉淀在 CONTEXT.md 术语表中。没有需要 ADR 的决策——所有取舍(如砍记忆功能)都容易逆转且已在术语表中注明理由。
请确认:以上理解是否与你心目中的产品一致? 确认后我就开始动手搭建工程和实现。如有任何一条想改,现在说是最便宜的时机。
目录结构
.
├── CLAUDE.md
├── CONTEXT.md
└── docs
└── agents
├── domain.md
├── issue-tracker.md
└── triage-labels.md
/to-spec
description
把当前对话变成一份 spec 并发布到项目 issue tracker:不做访谈,只综合你们已经讨论过的内容。
什么是 Seam(接缝)?
Seam(接缝):Michael Feathers 的术语:一个无需在那个位置编辑代码就能改变行为的地点。它是 interface 的位置,seam 放在哪里是它自己的决策,与 seam 背后放什么相互独立。
会话
😀
/mattpocock-skills:to-spec
🤖
仓库约定已明确:spec 发布到 .scratch/<feature-slug>/spec.md,triage 状态用 Status: 行记录。无 ADR 冲突(docs/adr/ 尚为空),术语全程使用 CONTEXT.md 词汇表。
在写 spec 之前,按流程先和你确认测试接缝(seams)。这个项目是全新代码库,没有既有接缝可用,我提议在最高层设两个接缝:
接缝 1:计算引擎(计算语义 + 输入规则 + 历史记录) 一个纯逻辑状态机:输入是按键事件(数字、运算符、退格、清除、等于……),输出是界面应显示的状态(完整算式、当前数字、错误提示)和历史记录。测试从”用户按键”这个最高点进,断言”屏幕显示什么、历史里多了什么”,完全不碰内部实现。链式语义、百分比、重复等号、除以零、12 位上限、千分位前的原始数字、50 条历史上限——全部在这一个接缝上测。
接缝 2:中文数字朗读转换 纯函数:数字(含小数、负数、万/亿量级)→ 播报文本(如 12345 → “一万两千三百四十五”,0.5 → “零点五”)。这是规则最复杂、最容易出错的部分,独立成接缝。
不在接缝上的:SwiftUI 界面布局、配色、窗口行为、语音实际播放(AVSpeechSynthesizer 调用)、键盘事件捕获——这些手工验证。
理由:接缝越少越好,这两个模块是真正独立的纯逻辑,其余都是它们的薄壳。
这两个测试接缝符合你的预期吗? 确认后我就把完整 spec 写入 .scratch/elder-calculator/spec.md 并标记 ready-for-agent。
😀
确认
🤖
Write .scratch/elder-calculator/spec.md
# 长辈计算器 — 功能规格
Status: ready-for-agent
## Problem Statement(问题陈述)
老年用户(60–80 岁、老花眼、听力正常或轻度下降、习惯实体计算器)使用 macOS 自带计算器时:按钮小、字体小、对比度低,看不清;屏幕上只显示一个孤零零的数字,算完就忘了输入过程,无法核对;英文缩写键(AC、M+)看不懂。他们需要一款"看得清、听得见、错得起"的计算器。
## Solution(解决方案)
一款名为"长辈计算器"的 macOS 原生应用:仿实体计算器的大按钮大字体界面、深色高对比配色、每键中文语音播报(按数值念,含万/亿单位)、完整算式显示、自动计算历史(侧边抽屉回看)、分级清除防误触。打开即用,无需学习。
## User Stories(用户故事)
### 基本计算
1. 作为老年用户,我想点大按钮输入数字,以便不用费力瞄准就能按对。
2. 作为老年用户,我想做加减乘除四则运算,以便完成日常算账。
3. 作为老年用户,我想输入小数(含直接按小数点自动补零为"0.5"),以便算带零头的钱。
4. 作为老年用户,我想用百分比键按"加一成"的直觉计算(100 + 10 % = 110),以便算折扣和加成。
5. 作为老年用户,我想用正负号键切换当前输入数字的正负,以便记录支出。
6. 作为老年用户,我想按运算符后立即看到中间结果(链式立即执行,无运算优先级),以便和我用实体计算器的习惯一致。
7. 作为老年用户,我想算出结果后再按"="重复上次运算(12+5=17,再按=得22),以便做连加连减。
8. 作为老年用户,我想算出结果后直接按数字键开始新一笔计算,旧结果自动进入历史,以便连续算多笔账不用手动清屏。
### 看得清
9. 作为老花眼用户,我想看到超大字体的数字和超大按钮,以便不戴老花镜也能操作。
10. 作为老花眼用户,我想要黑底配白色/亮黄色大数字、橙色运算符的高对比配色,以便在弱光下也看得清。
11. 作为老年用户,我想把窗口拖大时字体和按钮跟着变大,以便充分利用屏幕。
12. 作为老年用户,我想窗口不会被我误拖得太小(有最小尺寸下限),以便按钮始终点得中。
13. 作为老年用户,我想大数字带千分位分隔符(1,234,567),以便核对位数不出错。
14. 作为老年用户,我想屏幕上始终显示完整算式("12 + 5 ="),以便随时核对输入过程。
15. 作为偏好纸质账本观感的用户,我想在设置里切换到浅色高对比配色,以便按自己的习惯看清屏幕。
### 听得见
16. 作为看不清屏幕的用户,我想每按一个键都有中文语音播报(数字、运算符、结果都报),以便不听屏幕也能操作。
17. 作为老年用户,我想数字按数值念(17 念"十七"、12345 念"一万两千三百四十五"),以便符合我听账目的习惯。
18. 作为老年用户,我想大数字用"万/亿"单位念出,以便快速理解金额量级。
19. 作为听力下降的用户,我想播报默认用慢速、可在设置里调到标准,以便听清每个字。
20. 作为不需要语音的用户,我想在设置里关掉语音播报,以便不打扰家人。
### 错得起
21. 作为手抖的用户,我想用退格键逐位删除输错的数字,以便不用从头再来。
22. 作为手抖的用户,我想"清除"键是醒目的红色、远离数字键,以便降低误按概率。
23. 作为老年用户,我想按"清除"后立即生效并听到"已清除"播报,以便知道刚才发生了什么。
24. 作为老年用户,我想除以零时看到大字中文提示"不能除以零"并听到播报、按任意键就能继续,以便不被英文错误信息吓住。
25. 作为老年用户,我想输入超过 12 位时被拒绝并提示,以便发现手抖多按了键。
26. 作为老年用户,我想一个数字里按第二个小数点时不会被接受,以便输入始终合法。
27. 作为老年用户,我想连续按两个运算符时后一个替换前一个,以便纠正按错的运算符。
### 回看核对
28. 作为老年用户,我想每次完成的计算自动记入历史,以便不用学任何操作就有账可查。
29. 作为老年用户,我想点一个大"历史"按钮滑出抽屉回看最近的算式和结果,以便核对"刚才算的什么"。
30. 作为老年用户,我想历史只读不能点,以便不会因误触让屏幕变化而困惑。
31. 作为老年用户,我想历史保留最近 50 条、关闭应用后清空,以便历史始终是"这次的账"而不是需要管理的旧账。
### 键盘与其他
32. 作为会打字的用户,我想用物理键盘的数字键、加减乘除键、回车(等于)、退格操作,以便输入更快更准。
33. 作为老年用户,我想打开应用就是计算器主屏,没有启动页、教程、欢迎语,以便立刻开始用。
34. 作为老年用户,我想点窗口关闭按钮就退出应用,以便"关掉就是关掉"不产生疑惑。
35. 作为老年用户,我想设置里只有语音开关、语速、配色三项、用齿轮大按钮进入,以便不被一堆选项吓住。
36. 作为老年用户,我想界面全是中文("加、减、乘、除、等于、清除"),以便不用猜英文缩写的意思。
## Implementation Decisions(实现决策)
- **技术栈**:SwiftUI 单 target macOS 应用;工程名 `ElderCalculator`,bundle id `com.wangjunjian.ElderCalculator`;最低系统 macOS 15;窗口关闭即退出(`NSApplication` 终止行为)。
- **模块划分**(两个深模块 + 薄壳):
- **计算引擎**:纯 Swift 值类型状态机,不依赖 SwiftUI/AppKit。公开接口约两类:输入方法(按语义命名,如"输入数字/输入运算符/退格/清除/等于/百分比/正负号")和只读的展示状态(当前完整算式文本、当前显示数字的原始值、是否处于错误态)。历史记录由引擎内部维护(完成的计算自动入历史,上限 50 条,最新的在前),对外只暴露只读列表。
- **中文数字朗读转换**:纯函数/纯类型,输入十进制数(含小数、负数、万/亿量级),输出播报文本。引擎不依赖它;UI 层用它把展示状态转成播报词。
- **UI 壳**(SwiftUI):把按键/键盘事件翻译为引擎输入,把引擎展示状态渲染为界面,并调用朗读转换 + 系统语音合成(AVSpeechSynthesizer,中文语音)完成播报。
- **计算语义**:链式立即执行;百分比按"100 + 10 % = 110"实体计算器语义;重复等号重复上次运算;出结果后按数字键开新笔计算且旧结果入历史。详细规则见 `CONTEXT.md` 的"计算语义"条目——该条目是行为的权威定义。
- **数字模型**:输入上限 12 位(拒绝并提示);结果显示最多 10 位有效数字、智能去尾零;屏幕显示带千分位;语音播报用原始数值转换(不含千分位)。
- **错误态**:除以零进入错误态——展示状态提供"不能除以零"提示,任意后续输入使引擎恢复可输入状态。
- **界面结构**:显示屏在上(完整算式带 + 当前数字大字)、数字键区在下,仿实体计算器;"清除"红色并与数字键视觉隔离;"历史"和"设置"为大按钮;历史抽屉只读;设置三项(语音开关、语速慢/标准默认慢、配色深色默认/浅色)。
- **配色**:两套固定高对比主题(深色:黑底、白/亮黄数字、橙色运算符;浅色高对比),不跟随系统自动切换;配色选择持久化到用户默认设置。
- **窗口**:默认大尺寸、设最小尺寸下限;内容随窗口缩放;单窗口,关闭即退出;首次启动无主屏以外的任何内容。
- **历史**:session 内存级,不持久化;关闭应用即清空。
- **图标**:简单自制(计算器图形 + 高对比配色),可用 Asset Catalog 生成。
- **语音播报**:默认开;每键播报(输入回显 + 结果);语速两档映射到语音合成速率;播报内容来自朗读转换模块的输出。
## Testing Decisions(测试决策)
- **什么是好测试**:只测外部行为,不测实现细节。对计算引擎,测试 = "给定一串按键事件,断言展示状态与历史内容",不得触碰内部状态字段;对朗读转换,测试 = "给定数字,断言播报文本"。
- **测试接缝**(两个,均为新接缝,已按最高可行点设置):
1. **计算引擎**:覆盖链式立即执行、百分比语义、重复等号、退格/分级清除、除以零错误态与恢复、12 位输入上限、第二个小数点拒绝、运算符替换、正负号作用域、出结果后开新笔并入历史、历史 50 条上限与顺序、去尾零与有效数字规则。
2. **中文数字朗读转换**:覆盖个十百千万亿各量级、中间带零(1001 → "一千零一")、末尾带零、小数(0.5 → "零点五")、负数、整数与小数混合。
- **开发方式**:两个模块均 TDD——先写测试再实现。
- **不在自动化测试内**:SwiftUI 界面、配色、窗口行为、语音实际播放、键盘事件捕获——手工验证。
- **既有测试先例**:无(全新代码库,本次建立先例)。
## Out of Scope(不在本次迭代范围内)
- 记忆功能(M+/M−/MR/MC)——历史记录已覆盖其场景,见 `CONTEXT.md`。
- 科学计算(三角、对数、括号等)。
- 运算优先级(表达式式计算)。
- 结果复制到剪贴板。
- 历史记录持久化与历史条目的交互(带回主屏、复制)。
- 按键音效(语音播报是唯一听觉反馈)。
- 双击/长按确认机制。
- 跟随系统自动切换深浅色、独立字号档位。
- App Store 上架、签名公证分发。
- 开机欢迎语、教程。
## Further Notes(其他说明)
- 领域术语与全部交互共识的权威定义在根目录 `CONTEXT.md`,实现中遇到歧义以该文件为准;spec 与术语表冲突时以术语表为准并在实现前澄清。
- 环境已确认:Xcode 26.6 / Swift 6.3.3 / macOS 26 开发机,可直接 `xcodebuild` 构建。
- 分发方式为本地自用:构建出 `.app` 直接运行,不签名不公证。
Spec 已发布:.scratch/elder-calculator/spec.md,状态已标记 Status: ready-for-agent。
要点回顾:
- 36 条用户故事,按”基本计算 / 看得清 / 听得见 / 错得起 / 回看核对 / 键盘与其他”六组组织,全部使用
CONTEXT.md术语表词汇 - 实现决策:两个深模块(计算引擎状态机 + 中文朗读转换纯函数)+ SwiftUI 薄壳;行为权威定义指向
CONTEXT.md - 测试决策:两个接缝、TDD、只测外部行为,已按你确认的方案写入
- Out of Scope:记忆功能、科学计算、结果复制、历史持久化、按键音效等 10 项明确排除
这个 spec 现在可以直接交给实现 agent(或用 /mattpocock-skills:tdd 技能从计算引擎开始动工)。需要我继续吗?
目录结构
.
├── .scratch
│ └── elder-calculator
│ └── spec.md
├── CLAUDE.md
├── CONTEXT.md
└── docs
└── agents
├── domain.md
├── issue-tracker.md
└── triage-labels.md
/to-tickets
description
把一份计划、spec 或当前对话拆解成一组曳光弹 ticket,每张声明自己的阻塞边,并发布到已配置的 tracker(本地是每张 ticket 一个文件、以文本表示边;真实 tracker 上则是原生的阻塞链接)。
ticket ≠ 用户故事
ticket ≠ 用户故事。 36 条用户故事是 spec 里描述”系统该有什么行为”的清单;/to-tickets 拆的是实现批次,一张 ticket 会打包多条相关的故事。按它”tracer-bullet”(曳光弹)的原则,每张 ticket 是”一次能端到端跑通的一小片”,对应你的项目大概就是之前估的 5~8 张,比如一张”计算引擎”ticket 会一次性覆盖链式语义、百分比、除以零、12 位上限等十几条故事。
所以你的实际手工介入次数 ≈ ticket 数量,大概是:拆票 1 次 + 每张票启动 1 次,总共 6~9 次”输入一行命令”的动作。每次介入之间,/implement 是全自动的:读 spec、TDD 红绿循环、跑 code review、提交,中间不需要你。
而且这个”频繁介入”恰恰是这套流程的设计意图,不是缺点:
- 每张票之间你有一个天然的检查点,可以打开应用亲手点点看,不满意马上纠偏,而不是等 36 条故事全写完才发现方向错了。
- 每张票一个全新会话,意味着智能体永远在最清醒的状态下写代码。如果让 36 条故事在一个会话里连续做完,后半程的代码是上下文被塞满的智能体写的,质量衰减比”多敲几次命令”的代价大得多。
当然,如果你真的想减少介入,也有折中:跑完 /to-tickets 后,在同一个会话里连续 /implement ticket-1、/implement ticket-2……让会话自己往下走。这在票之间依赖简单时可行,只是失去了”每票全新上下文”的保护。我的建议是:先按标准流程走一两张票感受一下,觉得介入太烦再合并也不迟。
会话
😀
/mattpocock-skills:to-tickets
🤖
基于 spec 和 CONTEXT.md,我把工作拆成 12 张 tracer-bullet 垂直切片(每张都贯穿 引擎 → UI → 测试,独立可演示/可验证,尺寸控制在单个上下文窗口内):
01 · 工程脚手架 — Blocked by: 无 交付:Xcode 工程能构建,应用启动显示空窗口,单元测试 target 能跑测试。后续所有 ticket 的地基。
02 · 数字输入与大屏显示 — Blocked by: 01 交付:点数字键,大屏实时显示输入的数字(带千分位、超大字体、黑底黄字);输入超 12 位被拒绝。第一个端到端 tracer。
03 · 四则运算与等号 — Blocked by: 02 交付:完整算账链路——加减乘除、链式立即执行、完整算式显示(“12 + 5 =”)、连续运算符后者替换前者、出结果后按数字开新笔、重复等号重复上次运算。
04 · 小数、正负号、百分比 — Blocked by: 03 交付:“.5”自动显示”0.5”、禁止第二个小数点、正负号作用于当前输入、“100 + 10 % = 110”。
05 · 退格与分级清除 — Blocked by: 02 交付:退格删一位;红色视觉隔离的”清除”键一键清全部。(可与 03 并行)
06 · 错误与边界 — Blocked by: 04 交付:除以零进入错误态(大字中文提示、任意键恢复)、结果 10 位有效数字智能去尾零。
07 · 中文数字朗读转换模块 — Blocked by: 01 交付:纯逻辑模块 TDD 完成——12345→“一万两千三百四十五”、0.5→“零点五”、负数、中间带零(1001→“一千零一”)等全规则。独立于 UI。(可与 02–06 并行)
08 · 语音播报接入 — Blocked by: 07, 03 交付:每按一键中文语音播报(默认开、慢速),结果按数值念出。
09 · 计算历史 — Blocked by: 03 交付:完成的计算自动入历史(上限 50 条、关闭应用清空);“历史”大按钮滑出只读抽屉回看。
10 · 设置面板与高对比双主题 — Blocked by: 08 交付:齿轮大按钮进设置,仅三项——语音开关、语速(慢/标准)、配色(深色/浅色高对比,选择持久化)。
11 · 键盘输入 — Blocked by: 06 交付:物理键盘数字键、+-*/、回车(等于)、退格全部可用。
12 · 窗口行为与应用收尾 — Blocked by: 09, 10, 11 交付:默认大窗口 + 最小尺寸下限 + 内容随缩放、关闭窗口即退出、中文名”长辈计算器”、自制图标。完整应用形态。
依赖图摘要:01 → 02 → 03 → 04 → 06 → 11 是主干;05 挂在 02 后、07→08→10 与主干并行;09 挂在 03 后;12 等全部就绪后收尾。
请过目:
- 粒度合适吗?(有没有太粗/太细的)
- 阻塞边对吗?(比如 05 和 07 可以并行,你接受吗)
- 有想合并或再拆的吗?
确认后我就按编号写入 .scratch/elder-calculator/issues/ 并标记 ready-for-agent。
😀
确认
🤖
Write .scratch/elder-calculator/issues/01-project-scaffolding.md
# 01: 工程脚手架
**What to build:** 用户双击应用图标,"长辈计算器"启动并显示一个空白大窗口;开发者能构建工程、运行单元测试。这是后续所有工作的地基。
**Blocked by:** None (can start immediately)
**Status:** ready-for-agent
- [ ] SwiftUI macOS 应用工程可构建运行,工程名 ElderCalculator,bundle id com.wangjunjian.ElderCalculator
- [ ] 最低系统版本 macOS 15
- [ ] 应用启动显示空白窗口(为后续大屏 UI 预留)
- [ ] 单元测试 target 已建立且能运行(空测试通过)
- [ ] 界面显示名为"长辈计算器"
Write .scratch/elder-calculator/issues/02-digit-input-display.md
# 02: 数字输入与大屏显示
**What to build:** 用户点屏幕上的数字大按钮,上方显示屏实时显示输入的数字——超大字体、黑底亮黄/白色高对比、带千分位(如 1,234,567,输入时随位数自动重排);输入超过 12 位时被拒绝并提示。第一个贯穿引擎→UI→测试的 tracer bullet。
**Blocked by:** 01
**Status:** ready-for-agent
- [ ] 计算引擎(纯 Swift 状态机,不依赖 SwiftUI/AppKit)接受数字输入并暴露只读展示状态
- [ ] 屏幕显示超大字体数字,深色高对比配色(黑底、白/亮黄数字)
- [ ] 数字带千分位分隔符,输入时自动重排
- [ ] 输入超过 12 位被拒绝并给出提示
- [ ] 引擎行为有单元测试(按键事件 → 断言展示状态,不测内部实现)
Write .scratch/elder-calculator/issues/03-arithmetic-equals.md
# 03: 四则运算与等号
**What to build:** 用户能完成完整算账链路:加减乘除、按运算符立即算出中间结果(链式立即执行,无运算优先级,"12 + 5 × 2"按顺序得 34);屏幕上始终显示完整算式("12 + 5 =");连续按两个运算符后一个替换前一个;算出结果后按数字键开新笔计算;算出结果后再按"="重复上次运算(12+5=17,再按=得22)。
**Blocked by:** 02
**Status:** ready-for-agent
- [ ] 加减乘除四则运算,橙色运算符大按钮
- [ ] 链式立即执行:按运算符即显示中间结果,无运算优先级
- [ ] 完整算式显示始终可见(如"12 + 5 =")
- [ ] 连续按两个运算符,后一个替换前一个
- [ ] 出结果后按数字键开始新一笔计算
- [ ] 重复按"="重复上次运算(17 → 22 → 27)
- [ ] 以上语义均有引擎单元测试覆盖
Write .scratch/elder-calculator/issues/04-decimal-sign-percent.md
# 04: 小数、正负号、百分比
**What to build:** 用户能算带零头的钱和折扣加成:直接按小数点输入自动补零显示"0.5";一个数字里第二个小数点不被接受;正负号键切换当前正在输入数字的正负;百分比按实体计算器"加一成"语义(100 + 10 % = 110)。
**Blocked by:** 03
**Status:** ready-for-agent
- [ ] 小数点输入,".5"自动显示为"0.5"
- [ ] 一个数字禁止第二个小数点
- [ ] 正负号键作用于当前输入的数字
- [ ] 百分比实体计算器语义:"100 + 10 %" = 110
- [ ] 以上行为均有引擎单元测试覆盖
Write .scratch/elder-calculator/issues/05-backspace-clear.md
# 05: 退格与分级清除
**What to build:** 手抖输错时,用户按"退格"逐位删除当前数字;按醒目的红色"清除"键(与数字键视觉隔离)一键清掉全部输入,立即生效。误按代价低、后果清晰。
**Blocked by:** 02
**Status:** ready-for-agent
- [ ] 退格键逐位删除当前输入的数字
- [ ] 清除键清掉全部输入与算式,立即生效
- [ ] 清除键为醒目红色、与数字键视觉隔离
- [ ] 不做双击/长按确认
- [ ] 引擎层退格与清除行为有单元测试覆盖
Write .scratch/elder-calculator/issues/06-errors-bounds.md
# 06: 错误与边界
**What to build:** 用户除以零时看到大字中文提示"不能除以零",按任意键即可恢复继续输入,不会被吓住;计算结果最多保留 10 位有效数字并智能去尾零(如 10 ÷ 4 显示 2.5 而非 2.5000000001)。
**Blocked by:** 04
**Status:** ready-for-agent
- [ ] 除以零进入错误态:展示状态提供"不能除以零"中文大字提示
- [ ] 错误态下按任意键恢复可输入状态
- [ ] 结果最多 10 位有效数字,智能去尾零
- [ ] 屏幕显示阿拉伯数字(带千分位),与语音朗读用的原始数值分离
- [ ] 以上行为均有引擎单元测试覆盖
Write .scratch/elder-calculator/issues/07-chinese-speech-formatter.md
# 07: 中文数字朗读转换模块
**What to build:** 一个纯逻辑模块(TDD 开发):输入十进制数,输出中文语音播报文本——12345 →"一万两千三百四十五"、0.5 →"零点五"、1001 →"一千零一"、负数、万/亿量级全覆盖。独立于 UI,可单独测试演示。
**Blocked by:** 01
**Status:** ready-for-agent
- [ ] 整数按数值念:个十百千万亿各量级(17→"十七",12345→"一万两千三百四十五")
- [ ] 中间带零规则正确(1001→"一千零一")
- [ ] 末尾带零规则正确(1200→"一千二百")
- [ ] 小数(0.5→"零点五")与整数小数混合
- [ ] 负数(-5→"负五")
- [ ] 全部规则 TDD:先写测试再实现
Write .scratch/elder-calculator/issues/08-voice-announcer.md
# 08: 语音播报接入
**What to build:** 用户每按一个键都听到中文语音播报——数字、运算符("加""减""乘""除")、结果(按数值念,来自朗读转换模块);默认开启、默认慢速。看不清屏幕也能操作计算器。
**Blocked by:** 07, 03
**Status:** ready-for-agent
- [ ] 每个按键都有中文语音播报(数字、运算符、退格、清除、等于)
- [ ] 结果播报使用朗读转换模块的输出(按数值念,含万/亿)
- [ ] 按"清除"播报"已清除";除以零播报错误提示
- [ ] 默认开启、默认慢速(语速档位映射到语音合成速率)
- [ ] 使用系统中文语音(AVSpeechSynthesizer 中文语音)
- [ ] 实际播报效果手工验证(语音播放不在自动化测试内)
Write .scratch/elder-calculator/issues/09-calculation-history.md
# 09: 计算历史
**What to build:** 每次按"="完成的计算自动记入历史(算式 + 结果,上限 50 条、最新的在前、关闭应用即清空,不持久化);用户点主屏大"历史"按钮滑出侧边抽屉,只读回看核对"刚才算的什么"。
**Blocked by:** 03
**Status:** ready-for-agent
- [ ] 完成的计算自动入历史(算式 + 结果),无需用户任何操作
- [ ] 上限 50 条,最新在前,关闭应用后清空
- [ ] 出结果后开新笔计算时旧结果也正确入历史
- [ ] 主屏一个大"历史"按钮,点开滑出侧边抽屉
- [ ] 历史列表只读,条目不可点、无交互
- [ ] 引擎层历史记录行为有单元测试覆盖
Write .scratch/elder-calculator/issues/10-settings-themes.md
# 10: 设置面板与高对比双主题
**What to build:** 用户点主屏齿轮大按钮进入设置,只有三项:语音播报开关、播报语速(慢/标准,默认慢)、配色(深色高对比默认 / 浅色高对比);配色选择持久化,下次启动保持。不跟随系统自动切换深浅色。
**Blocked by:** 08
**Status:** ready-for-agent
- [ ] 齿轮大按钮进入设置面板
- [ ] 语音播报开关(关闭后完全安静,无按键音效)
- [ ] 播报语速两档:慢(默认)/ 标准
- [ ] 配色两套固定高对比主题:深色(默认,黑底白/黄数字、橙色运算符)与浅色高对比
- [ ] 配色选择持久化到用户默认设置
- [ ] 不跟随系统深浅色自动切换
- [ ] 设置项仅此三项,无字号档位
Write .scratch/elder-calculator/issues/11-keyboard-input.md
# 11: 键盘输入
**What to build:** 会打字的用户可以用物理键盘操作:数字键、加减小键盘/主键盘运算符键、回车(等于)、退格键全部映射到对应计算操作,与点屏幕按钮效果完全一致。
**Blocked by:** 06
**Status:** ready-for-agent
- [ ] 数字键输入数字与小数点
- [ ] 加减乘除键输入运算符
- [ ] 回车键等同"="
- [ ] 退格键等同屏幕退格键
- [ ] 键盘输入与屏幕按钮行为一致(同一引擎入口)
- [ ] 键盘映射手工验证
Write .scratch/elder-calculator/issues/12-window-app-polish.md
# 12: 窗口行为与应用收尾
**What to build:** 完整应用形态:默认打开就是大窗口,有最小尺寸下限(按钮不会被拖到点不中),字体按钮随窗口缩放同步变大;点窗口关闭按钮即退出应用;Dock 与窗口标题显示中文名"长辈计算器";自制高对比计算器图标。首次打开即是主屏,无启动页/教程/欢迎语。
**Blocked by:** 09, 10, 11
**Status:** ready-for-agent
- [ ] 默认窗口尺寸大,设最小尺寸下限
- [ ] 字体与按钮随窗口缩放同步变大
- [ ] 关闭窗口即退出应用(单窗口,"关掉就是关掉")
- [ ] 首次启动直接显示计算器主屏,无启动页/教程/欢迎语
- [ ] 应用显示名"长辈计算器"(Dock、窗口标题)
- [ ] 简单自制应用图标(计算器图形 + 高对比配色)
- [ ] 整体手工验收:按 spec 用户故事走查一遍
12 张 ticket 已全部发布到 .scratch/elder-calculator/issues/,每张都标记了 Status: ready-for-agent:
| # | Ticket | Blocked by |
|---|---|---|
| 01 | 工程脚手架 | 无 ✅ 可立即开始 |
| 02 | 数字输入与大屏显示 | 01 |
| 03 | 四则运算与等号 | 02 |
| 04 | 小数、正负号、百分比 | 03 |
| 05 | 退格与分级清除 | 02(可与 03 并行) |
| 06 | 错误与边界 | 04 |
| 07 | 中文数字朗读转换模块 | 01(可与主干并行) |
| 08 | 语音播报接入 | 07, 03 |
| 09 | 计算历史 | 03 |
| 10 | 设置面板与高对比双主题 | 08 |
| 11 | 键盘输入 | 06 |
| 12 | 窗口行为与应用收尾 | 09, 10, 11 |
当前 frontier:01 工程脚手架(完成后 02 和 07 可并行展开)。
接下来可以从 01 开始动工——你可以直接说”开始做 01”,或用 /mattpocock-skills:tdd 进入测试先行的工作流。
目录结构
.
├── .scratch
│ └── elder-calculator
│ ├── issues
│ │ ├── 01-project-scaffolding.md
│ │ ├── 02-digit-input-display.md
│ │ ├── 03-arithmetic-equals.md
│ │ ├── 04-decimal-sign-percent.md
│ │ ├── 05-backspace-clear.md
│ │ ├── 06-errors-bounds.md
│ │ ├── 07-chinese-speech-formatter.md
│ │ ├── 08-voice-announcer.md
│ │ ├── 09-calculation-history.md
│ │ ├── 10-settings-themes.md
│ │ ├── 11-keyboard-input.md
│ │ └── 12-window-app-polish.md
│ └── spec.md
├── CLAUDE.md
├── CONTEXT.md
└── docs
└── agents
├── domain.md
├── issue-tracker.md
└── triage-labels.md
/implement
/implement 会调用 /tdd 技能 和 /code-review 技能。
description
基于 spec 或一组工单实现用户描述的工作,尽量在预先约定的接缝处使用 /tdd,期间定期运行类型检查与单个测试文件并在最后完整运行一次整个测试套件,完成后使用 /code-review 评审这项工作,并将工作提交到当前分支。
会话
😀
# /mattpocock-skills:implement #03
/mattpocock-skills:implement .scratch/elder-calculator/issues/03-arithmetic-equals.md
⚠️
/mattpocock-skills:implement不要这样写,这样会执行所有的 tickets。
🤖
Read 03-arithmetic-equals.md, CONTEXT.md
Bash 查看项目结构
Bash 列出源码与测试文件
Bash 查看 docs 与 issues 目录
Bash 查看功能规格
我先看一下 ticket 02 的 issue 文件和项目配置,了解既有约定。
Bash 查看 ticket 02 验收状态与 Xcode scheme
按照 implement 流程,我先加载 TDD 技能,然后在引擎接缝处先写测试。
mattpocock-skills:tdd skill
Edit CalculatorEngineTests.swift
Bash 运行测试确认红灯(编译失败)
Write CalculatorEngine.swift
Bash 运行测试确认切片 1 转绿
引擎全绿(22 个测试)。现在接 UI 壳:完整算式行 + 橙色运算符/等号大按钮。
Write ContentView.swift
完整套件 22 个测试全部通过(SourceKit 报错为缓存噪音,xcodebuild 编译实际成功)。接下来按流程做 code review:
mattpocock-skills:code-review skill
Bash 确认基点与 diff 范围、查找编码标准文档
Agent:Standards 轴审查 diff
Agent:Spec 轴审查 diff
两个审查子智能体已在后台并行运行,等待结果。
…
重构完成,运行最终完整测试套件:
Bash 运行最终完整测试套件
全部 23 个测试通过。提交代码并勾选工单验收项:
Bash 提交 ticket 03 实现
Edit 03-arithmetic-equals.md
Bash 提交验收项勾选
Ticket 03(四则运算与等号)完成,已分两个提交落到 main。
实现内容
- 引擎(CalculatorEngine.swift):新增
.operation/.equals按键与ArithmeticOperation类型;DisplayState新增expressionText(完整算式始终可见)。语义与 CONTEXT.md 逐条对齐:链式立即执行无优先级(12 + 5 × 2 = 34)、运算符替换、出结果后按数字开新笔、在结果上继续(17 + 3 = 20)、重复等号(17 → 22 → 27)。 - 结果格式化:最多 10 位有效数字、智能去尾零(1 ÷ 4 = 0.25)、千分位(含小数整数部分与负数:-122,456)。
- UI(ContentView.swift):显示屏上方新增完整算式行;右侧橙色运算符列(÷ × − + =)。
过程
- TDD:4 个红→绿切片(基本加法与算式 → 四则与链式 → 替换/新笔/重复等号 → 格式化),共 23 个引擎行为测试全绿。
- 双轴 code review:
- Spec 轴发现一处真实偏差——算式行中的数字未带千分位(CONTEXT 要求”屏幕上的数字带千分位”),已补测试修复;另注明”12 + =“静默无反应为 spec 未定义行为,留待 ticket 06 错误态处理。
- Standards 轴均为判断性建议,落实了两项轻量重构(
LastEvaluation消除数据泥团、橙色按钮去重);算式字符串拼搭的 Primitive Obsession 建议(token 列表化)暂不做——现有实现已被 23 个测试钉住,留待 ticket 09 历史记录时再看是否需要。
提交:cc2f124 feat: 四则运算与等号(ticket 03)、c098cda chore: 勾选 ticket 03 验收项。下一张可做的是 ticket 04(小数点/正负号/百分比)。
/code-review
description
沿两条轴评审自某个固定点(commit、分支、tag 或 merge-base)以来的变更:Standards(代码是否遵循本仓库成文的编码规范?)和 Spec(代码是否匹配原始 issue/spec 的要求?)。两条评审以并行 sub-agent 运行并排报告。当用户想评审一个分支、一个 PR、进行中的变更,或要求”review since X”时使用。
“基点”是什么
code-review 审查的不是整个代码库,而是一个 diff(差异)——“从某个时间点到现在,代码改了什么”。要算差异就需要两个端点:
基点(fixed point) ──────────► HEAD(当前最新提交)
64f0c0a c9bd0b5
64f0c0a 就是起点的那个提交哈希——你仓库里”docs: 长辈计算器领域术语表、功能规格与 12 张实现 ticket”这次提交。技能内部执行的命令是:
git diff 64f0c0a...HEAD
意思就是:把 64f0c0a 当时的代码状态和现在的代码状态对比,把中间所有改动交给两个审查子智能体看。
为什么必须指定基点
因为”从哪儿开始审”直接决定审查范围,选错了会有两种问题:
- 基点太早:diff 里混入一堆与本次 spec 无关的旧代码,审查智能体浪费时间在无关内容上,报告充满噪音
- 基点太晚:部分实现代码被排除在 diff 外,漏审
看你的提交历史就很直观:
f241e09 chore: setup 配置 ← 还没有任何 feature 内容
64f0c0a docs: spec + 12 张 ticket ← ★ 基点:spec 在此定格,实现尚未开始
bda3dd3 feat: 工程脚手架(ticket 01)← 实现从这开始
… tickets 02–12 …
3ad068f fix: 底行按钮裁切修复 ← 实现结束
c9bd0b5 chore: 脚本移动(HEAD)
选 64f0c0a 的理由:这一刻 spec 和 ticket 已经写好,但一行实现代码都还没写。从它往后算 diff,正好覆盖 ticket 01–12 的全部实现 + 后来的修复,不多不少。这正是 code-review 技能里”Pin the fixed point”那一步要你(或我)确认的:基点错了,要在源头就失败,而不是等两个子智能体跑出垃圾报告才发现。
类似的场景以后也常用:审查一个 PR 时基点是 main,审查最近 5 个提交时基点是 HEAD~5,都是同一个概念。
会话
😀
/mattpocock-skills:code-review 64f0c0a
🤖
两轴报告均已返回,汇总如下(固定点 64f0c0a → HEAD,13 个提交,ticket 01–12):
Standards
(a) 违反文档化标准(硬性)
- 设计流水线未走衔接:
docs/agents/design-pipeline.md「与 ticket 流程的衔接」要求界面类 ticket 在ready-for-agent前,spec 必须引用 DESIGN.md(及 overrides)并附 HTML 原型/Figma 链接。流水线文档(81e89c5 等)早于 ticket 05-12 的实现(e952faf),但 .scratch/elder-calculator/spec.md(Status: ready-for-agent)与 12 个 issue 全文均无 DESIGN/prototype/Figma 引用。且 prototype/index.html 是产品官网而非计算器界面原型,不能充数。 - 未发现违反 DESIGN.overrides.md 治理规则之处(未回写 DESIGN.md,偏离单条且带理由,合规)。App 配色(黑底/橙运算符)由 CONTEXT.md「配色」明文规定,仓库标准优先,不算偏离 DESIGN.md。
(b) 基线 smell(均为判断题)
- Duplicated Code:CalculatorEngine.swift 除零判断
pending == .divide && entryValue == 0及错误分支在pressOperation/pressEquals重复;trimmedExpression与syncExpression内联去尾空格循环重复;“放弃换算 clearEntry” 在三个 press 方法重复。ChineseSpeechFormatter.swift 的numericText复制了CalculatorEngine.formatted的"%.10g"/rounded口径——同一规则两处,改 10 位规则需散点改(兼 Shotgun Surgery 隐患),应提取共享。 - Data Clumps:
leftOperand + pendingOperation + expressionPrefix总是结伴被清/被设(reset、beginNewCalculation、enterDivideByZeroError、pressOperation),一个”待结算式”类型呼之欲出。 - Primitive Obsession:
entry用裸 String 表达”输入中的数”,衍生 “-0”、”-” 等特判,值得一个小类型。 - Divergent Change:
CalculatorEngine一身三职(按键状态机、显示格式化、历史记录),历史(recordHistory/maxHistoryItems)可拆出。 - Feature Envy(轻):ContentView.swift 大量
settings.theme.*链式取色,可考虑注入 theme。
命名普遍诚实,无 Mysterious Name;测试只走公开按键接口,结构良好。docs/guides 与官网原型文档类变更过目无异常。
Spec
(a) 缺失/部分实现
- 结果有效数字上限有口径偏差:spec 实现决策与工单 06 均要求”结果最多保留 10 位有效数字”,但
CalculatorEngine.formatted对 |v|<1e13 的整数直接全量显示——如 999,999,999,999 × 9 = 8,999,999,999,991 显示 13 位有效数字,仅小数路径用%.10g。全量显示精确整数对老人更友好,但属未经 spec/工单记录的偏差,需澄清或补记。
(b) Scope creep
- prototype/index.html(492 行 HTML 原型)与 docs/guides/ai-native-design-pipeline.html:spec 36 条用户故事与实现决策均无此产物,属规格外交付(非 Out of Scope 明列项)。
- 正负号键在出结果时对结果取负并转为新输入,超出 CONTEXT”正负号键作用于当前输入的数字”;但工单 04 评论区已记录为评审确认的扩展,可接受。
(c) 可疑实现
未发现逻辑性错误。链式执行、百分比”加一成”、重复等号、错误态恢复、运算符替换、12 位上限、历史 50 条均与 CONTEXT「计算语义」一致。播报词(“已清除""不能除以零""加/减/乘/除”)符合工单 08 记录。
测试约定:CalculatorEngineTests / ChineseSpeechFormatterTests 只经 press / displayState / history / spoken 断言外部行为,未触碰内部字段(虽用 @testable import 但无实质依赖),符合”只测外部行为”。但 ticket 05-12 的实现与测试同在 e952faf 一个提交,“两个模块 TDD 先测后实现”从提交历史无法验证。
小结:Standards 轴 1 项硬性违规(界面 ticket 缺设计流水线引用衔接)+ 5 条判断题 smell,最严重为设计流水线衔接缺失;Spec 轴 1 项口径偏差 + 2 项 scope creep、无可疑实现错误,最严重为”10 位有效数字”整数路径的未记录偏差。
/improve-codebase-architecture
description
扫描代码库寻找深化机会,以可视化 HTML 报告呈现,然后对你选中的那一个进行拷问式推进。
这条命令以项目的领域模型为依据,并建立在共享的设计词汇之上:
- 调用 Skill 工具传入 “codebase-design”,获取架构词汇(module、interface、depth、seam、adapter、leverage、locality)及其原则(删除测试、“接口就是测试面”、“一个 adapter = 假想接缝,两个 = 真实接缝”)。在每条建议中精确使用这些术语,不要漂移到”component""service""API""boundary”。
CONTEXT.md中的领域语言为好的接缝提供名字;docs/adr/中的 ADR 记录了本命令不应重新翻案的决定。
会话
😀
/improve-codebase-architecture
🤖
我已完整阅读全部 Swift 源码、测试、spec 与 CONTEXT.md,并核对了 git 历史。以下为架构走查报告。
# 长辈计算器架构走查报告
## 总体印象
这个代码库的架构纪律显著好于一般 SwiftUI 项目。spec 里"两个深模块 + 薄壳"的设计被忠实执行了:**CalculatorEngine 和 ChineseSpeechFormatter 都是真正的深模块**,测试只打接口不戳内部。任务书中的五个疑似摩擦点,经核实有两个基本不成立(见下),真正的问题集中在语音播报一侧。
## 对五个疑似摩擦点的验证结论
1. **"ContentView 承担太多职责"——基本推翻。** 状态机、算式文本拼装(`syncExpression`,CalculatorEngine.swift:326-331)、历史追加决策(`recordHistory` 只在 `pressEquals` 内部调用,CalculatorEngine.swift:298,316)全部在引擎里。ContentView 剩下的是:布局/缩放、键盘字符→`CalculatorKey` 的翻译(ContentView.swift:60-88)、以及两行编排 `engine.press(key); announcer?.announce(...)`(ContentView.swift:52-57)。git churn 高是因为每个 ticket 都加新键、新 UI,属于正常的 UI 局部性,不是职责超载。引擎与 View 之间没有手工状态搬运——`displayState` 是唯一同步通道。
2. **"引擎暴露过多内部细节"——推翻。** 接口只有 `press(_:)`、`displayState`、`history` 三个成员(约 400 行实现对 3 个入口,很深)。`entry`、`leftOperand`、`pendingOperation`、`lastEvaluation`、`isShowingResult` 等全部 private。
3. **"播报触发逻辑散落"——部分成立。** "何时播报"集中在 ContentView 的唯一入口 `press`(好),但"念什么"的规则所在模块与硬件耦合、完全无测试(见候选 1)。
4. **"格式化逻辑重复"——部分成立。** 千分位 `grouped` 只有一处(好),但"10 位有效数字"的数值规范化口径在两处独立实现(见候选 2)。
5. **"谁在决定一笔计算完成"——集中,无问题。** 只有 `pressEquals` 调用 `recordHistory`;重复等号也走同一处。HistoryDrawer 纯接收数据。
## 候选深化机会
### 候选 1:播报词决策与语音合成器硬耦合,核心产品规则零测试覆盖 — Strong
- **涉及文件**:`ElderCalculator/Speech/VoiceAnnouncer.swift`(全文)、`ElderCalculator/ContentView.swift:52-57`
- **问题描述**:`VoiceAnnouncer.announce(_:state:)`(VoiceAnnouncer.swift:26-54)包含一组真正的产品规则——错误态优先念错误、"="念结果数值、每个功能键的中文播报词("点"/"正负号"/"已清除")。语音播报是 CONTEXT.md 钦定的"适老化核心功能",但这些规则**没有任何测试**:`AVSpeechSynthesizer` 在 VoiceAnnouncer.swift:7 被硬编码为私有属性、不可注入,`speak` 一调就真出声,现有测试接缝根本够不到 `announce`。另外,非法数字键的防御判断 `(0...9).contains(digit)` 在引擎(CalculatorEngine.swift:174)和 announcer(VoiceAnnouncer.swift:32-35)各写了一份。
- **删除测试结论**:删掉 VoiceAnnouncer,播报词规则只能散进 ContentView.press 这一个调用点——复杂度不消失反而埋进 View,所以模块边界本身是对的;问题是**模块内部把纯决策和硬件适配器揉在了一层**。
- **可测性/局部性影响**:这是当前唯一一块"有产品规则、无测试路径"的逻辑。播报词决策是(key, DisplayState) → String? 的纯函数,完全可以像引擎一样 TDD。
- **深化方向**:把"按键+状态→播报词"的纯决策与 AVSpeechSynthesizer 适配器分层,合成器改为可注入的接缝。
### 候选 2:数值规范化口径(10 位有效数字)双处实现,靠注释对齐 — Worth exploring
- **涉及文件**:`ElderCalculator/Engine/CalculatorEngine.swift:334-343`(`formatted`)、`ElderCalculator/Speech/ChineseSpeechFormatter.swift:27-32`(`numericText`)
- **问题描述**:`%.10g` 这一"结果最多 10 位有效数字"的口径在两个模块各实现一遍,ChineseSpeechFormatter.swift:26 的注释"与屏幕显示口径一致"是靠注释维持的跨模块约定。屏幕念"0.3333333333"、语音念"零点三三…",两者必须共用同一舍入口径,否则显示与播报会对不上。
- **删除测试结论**:删掉其中一处,另一处仍在运行,口径漂移的复杂度不会消失——它现在是隐式的。
- **可测性/局部性影响**:改口径(例如改成 12 位有效数字)必须同步改两个文件、两套测试,改一处漏一处就是显示与播报不一致的 bug。
- **深化方向**:提取一个共享的"数值规范文本"概念,屏幕格式化与语音转换都从它出发。
### 候选 3:一个运算符的表述分散在两个文件 — Worth exploring
- **涉及文件**:`ElderCalculator/Engine/CalculatorEngine.swift:366-395`(`symbol`/`apply`/`percentValue`)、`ElderCalculator/Speech/VoiceAnnouncer.swift:67-77`(`spokenWord` 扩展)
- **问题描述**:`ArithmeticOperation` 的屏幕符号、运算语义、百分比规则在引擎文件里,播报用词却在 Speech 目录的扩展里。新增第五种运算(或改"除"的念法)要跨目录找两个 switch。
- **删除测试结论**:删掉 spokenWord 扩展,播报规则散进 announce 的 switch——更糟;当前写法只是放错了文件。
- **可测性/局部性影响**:纯局部性问题,体量很小(4 行),但"一个运算的所有表述"这个概念被切开了。
- **深化方向**:把运算符的播报用词并回运算符定义所在处(或候选 1 落地后并入播报词决策层),让一个概念住一个文件。
### 候选 4:错误态的双通道冗余(mainText 与 errorMessage 同值) — Speculative
- **涉及文件**:`ElderCalculator/Engine/CalculatorEngine.swift:15-16, 162-170`;消费方 `ContentView.swift:103`、`VoiceAnnouncer.swift:27`
- **问题描述**:错误态下 `mainText` 和 `errorMessage` 都被设为"不能除以零",`enterDivideByZeroError` 要分别写两个字段;DisplayState 里两个字段表达同一事实,靠人肉保持同步。
- **删除测试结论**:删掉 errorMessage,ContentView 的红色样式和 announcer 的错误优先判断就得靠字符串比较或重新解析 mainText——更糟。冗余是真实的,但现有消费方式都合理。
- **深化方向**:若要动,可让错误态成为 DisplayState 的一个显式 case 而非两个并行字段;收益小,可不动。
## 已经足够深、不建议动的模块
- **CalculatorEngine**:深模块范本。接口 3 个成员、实现约 400 行,历史记录、算式拼装、格式化全部内聚;测试(CalculatorEngineTests.swift,40+ 例)只通过 `press` + `displayState` + `history` 断言,完全符合 spec 的测试决策。`mainValue` 看似为语音开的后门,但删掉它 announcer 就得把"1,234,567"解析回 Double——传原始值是正确取舍,不算泄漏。
- **ChineseSpeechFormatter**:一个静态方法的接口对约 90 行实现,纯函数、测试充分。唯一的瑕疵是候选 2 的口径重复。
- **HistoryDrawer / SettingsView / ElderCalculatorApp**:都是称职的薄壳,数据单向流入,无逻辑埋藏。
- **AppSettings / CalculatorTheme**:三项设置 + 两套主题的简单模型,theme 为派生属性,无假想接缝。
- **屏幕按钮与物理键盘两个适配器共用 `press` 入口**(ContentView.swift:52-57):这是真实接缝(两个适配器、一条路径),行为一致性从构造上得到保证,不要抽象掉。
- **测试接缝本身**:引擎测试与 formatter 测试都是"给定输入断言输出"的行为测试,没有绕过接口戳内部状态的案例。
## 推荐强度汇总
| 候选 | 强度 | 理由 |
|---|---|---|
| 1. 播报词决策与合成器解耦 | **Strong** | 产品核心规则(每键念什么、错误优先)完全不可测;分层是低成本高收益 |
| 2. 数值规范化口径收敛 | **Worth exploring** | 真实的跨模块隐式耦合,但每处只有几行,收益是消除口径漂移风险 |
| 3. 运算符表述集中 | **Worth exploring** | 纯局部性小修,4 行代码搬家级别,顺手做即可 |
| 4. 错误态双通道 | **Speculative** | 冗余真实存在但消费方都合理,改动收益不明显 |
最大的结论:这个项目的计算与朗读转换两侧已经够深,**唯一的结构性短板在语音播报的"决策/硬件"不分层**——它恰好是产品的核心卖点,却是唯一没有测试接缝的行为规则集中地。
报告已在浏览器中打开,文件路径:
/var/folders/gm/qdt4nkv91g3g3vb5kjtfqk2h0000gn/T/architecture-review-20260921-211736.html
走查结论速览
好消息是项目整体架构纪律很好:CalculatorEngine 和 ChineseSpeechFormatter 都是深模块范本(接口 3 个成员对约 400 行实现,测试只打接口),屏幕按钮与物理键盘共用 press 入口是真实接缝。ContentView 的高改动频率经核实属于正常的 UI 局部性,不是职责超载。
四个候选深化机会:
| # | 候选 | 强度 |
|---|---|---|
| 1 | 播报词决策与语音合成器分层——错误优先、每键播报词这些适老化核心规则锁在与 AVSpeechSynthesizer 硬耦合的模块里,零测试覆盖 | Strong |
| 2 | 数值规范口径收敛——“10 位有效数字”在引擎和语音格式化器各实现一遍,靠注释对齐 | Worth exploring |
| 3 | 运算符表述集中——符号/语义在 Engine 目录,播报用词在 Speech 目录,一个概念被切开 | Worth exploring |
| 4 | 错误态改为 DisplayState 显式 case——mainText 与 errorMessage 两个字段说同一事实 | Speculative |
最高推荐是候选 1:它是产品核心卖点上唯一没有测试接缝的行为规则集中地,分层后播报词决策变成纯函数,可以像引擎一样 TDD,成本低收益直接。
想深入探讨哪一个?选定后我会用 grilling 流程和你逐项过决策(约束、依赖、深化后模块的形态、接缝后面放什么、哪些测试能保留)。候选 2 和 3 体量很小,也可以考虑和候选 1 一并落地。

😀
深入探讨:候选 1、2、3
🤖
mattpocock-skills:grilling skill
进入追问环节。我先把三个候选涉及的关键文件读一遍,确保每个问题都基于代码事实而不是猜测。
Read VoiceAnnouncer.swift
Read ChineseSpeechFormatter.swift
Read CalculatorEngine.swift
三个文件都读完了,发现一个在走查报告里没点名的重要事实,先看它,再开始第一轮提问。
新发现(候选 2 的实锤):CalculatorEngine.swift:335 的整数快路径阈值是 1e13,而 ChineseSpeechFormatter.swift:28 是 1e16。结果在 1e13 ~ 1e16 之间的整数(比如 999,999,999,999 × 999,999,999,999),屏幕按 %.10g 显示成科学计数法,语音却把完整精确值念出来——显示与播报的发散不是理论风险,是现存行为。这把候选 2 从”防漂移”升级成了”修已有不一致”。
第一轮
❓ Q1 - 播报词决策模块的形态:announce 里”错误优先 / 数字念数值 / 功能键念固定词”这组规则(VoiceAnnouncer.swift:26-54)以什么形态独立出来?
- A. 纯函数模块:新建
Speech/SpeechScript.swift,static func script(for key: CalculatorKey, state: DisplayState) -> String?。VoiceAnnouncer 退成薄协调器:问 SpeechScript 要词 → 交给合成器。 - B. 决策并入引擎:
DisplayState增加spokenText字段,引擎按键时顺带产出播报词。接口更少,但引擎开始承担播报职责,两个领域的规则搅在一个状态机里。 - C. 不拆决策,只注入合成器:
announce留在 VoiceAnnouncer,仅把AVSpeechSynthesizer换成协议。测试要连带构造 AppSettings 和打断逻辑,决策规则仍不是独立测试面。
➡️ 推荐 A。播报词决策是 (key, DisplayState) → String? 的纯函数,和 ChineseSpeechFormatter 同款形态,测试就是一张决策表。B 违反”一个模块一个概念”,C 只解决了一半问题。
❓ Q2 - 合成器接缝的形态:AVSpeechSynthesizer 退到接口之后,这个接口长什么样?
- A. 协议 + 两个适配器:
protocol SpeechSynthesizing { func speak(_ text: String) },生产适配器包装 AVSpeechSynthesizer(含打断逻辑与SpeechRate → avRate映射,VoiceAnnouncer.swift:57-65 移进适配器),测试适配器记录收到的文本数组。 - B. 闭包注入:VoiceAnnouncer 收一个
speak: (String) -> Void。更轻,但”打断上一条”和语速映射这些适配器职责没了家,容易散回调用点。
➡️ 推荐 A。两个适配器 = 真实接缝;语速映射是 AV 框架的细节,正该锁在 AV 适配器里。
❓ Q3 - 开关判断与编排位置:speechEnabled 的静默判断(现在在 speak 里)和 ContentView 的编排(engine.press(key) 后调 announcer?.announce,ContentView.swift:52-57)维持现状吗?
- A. 都维持:SpeechScript 是不知道开关存在的纯函数;开关判断留在 VoiceAnnouncer 协调器;ContentView 的两行编排不动。改动面最小。
- B. 开关上移到 ContentView:
if settings.speechEnabled { ... }包住播报调用。VoiceAnnouncer 更纯,但 View 多了一条产品规则。
➡️ 推荐 A。开关是播报模块自己的规则,不该漏进 View;ContentView 现有的”引擎按一下、播报跟一句”编排已经是正确厚度。
❓ Q4 - 共享口径的归属与对齐方向(候选 2):规范化文本提取到哪里,以及上面发现的 1e13/1e16 发散往哪边对齐?
- A. 独立小模块:新建
NumericText.swift(放Engine/或根目录),暴露static func canonical(_ value: Double) -> String(≤10 位有效数字、去尾零、不带千分位)。引擎的formatted=grouped(canonical(...)),语音的numericText=canonical(...)。双方都依赖它。 - B. 引擎持有,语音依赖:
CalculatorEngine.canonicalText公开,ChineseSpeechFormatter 调用。少一个文件,但 Speech → Engine 多了单向依赖。
对齐方向子问题:以 spec 的”结果最多 10 位有效数字”为准,语音跟着屏幕走(大整数结果不再念完整精确值,改念与屏幕一致的口径)——你接受这个行为变化吗?还是认为语音念精确值反而是特性,要把屏幕阈值放宽到 1e16?
➡️ 推荐 A + 语音对齐屏幕。独立模块没有依赖方向问题;spec 白纸黑字写的是 10 位有效数字,屏幕是对的,语音念出屏幕上不存在的位数对老花眼用户是”对不上账”的困惑源。
请按编号回答(可以只回”都按推荐”)。Q5(spokenWord 搬到哪)依赖 Q1 的结论,放到下一轮。
😀
都按推荐
🤖
第一轮四项决策全部落定:SpeechScript 纯函数模块、协议 + 双适配器、开关留在协调器、独立 NumericText 模块且语音对齐屏幕。前沿向外推进,第二轮如下。
第二轮
❓ Q5 - spokenWord 搬去哪(候选 3,现在依赖已解锁):运算符播报用词(VoiceAnnouncer.swift:67-77)的新家。
- A. 并入 SpeechScript:
spokenWord扩展删除,“加/减/乘/除”成为 SpeechScript 里.operation分支的内部细节。播报相关规则全部住在Speech/。 - B. 并回 ArithmeticOperation(CalculatorEngine.swift:366):运算符的符号、语义、百分比、播报用词四种表述住一处,但引擎文件开始认识”播报”这个概念。
➡️ 推荐 A。播报用词是播报规则,不是运算语义;SpeechScript 的 switch 本来就有 .operation 分支,顺手吸收。走查报告里也注明了这条路径。
❓ Q6 - 非法数字键的防御判断保留几份:引擎在 CalculatorEngine.swift:174 有 (0...9).contains(digit) 守卫,播报侧在 VoiceAnnouncer.swift:32 也有一份。SpeechScript 落地后这份判断会移进它。
- A. 两侧都保留:引擎守卫保护位数上限不变式,SpeechScript 守卫保护”不念出非法数字”——各保各的不变式,成本各一行。
- B. SpeechScript 信任引擎:无条件念
Double(digit)。少一份重复,但 SpeechScript 的正确性开始依赖”调用方必先过引擎”这个隐式约定。
➡️ 推荐 A。两个守卫保护的是不同不变式,不是真重复;B 省一行代码买来一条跨模块隐式约定,不划算。
❓ Q7 - 新模块的领域命名:两个新模块要进 CONTEXT.md 术语表(它们是新领域概念)。名字定哪个?
- A. 播报词决策(SpeechScript)/ 数值规范文本(NumericText):中文术语贴 CONTEXT.md 现有风格(“语音播报""完整算式显示”),英文类型名直白。
- B. 你另外起名。
➡️ 推荐 **A”。落地时我会把两个术语补进 CONTEXT.md(domain-modeling 的常规副作用)。
❓ Q8 - 落地顺序与 ticket 组织:三个候选怎么排、怎么记账?(仓库惯例:issue 以 markdown 跟踪在 .scratch/elder-calculator/issues/)
- A. 一张 issue,一次提交:候选 2 → 候选 1(Q5 的候选 3 顺手并入),一张
13-speech-architecture.md。三者在代码上交织(SpeechScript 的=分支会经过 NumericText),分开提交反而要中间态。 - B. 两张 issue 两次提交:先
13-numeric-text.md(候选 2,含行为对齐),再14-speech-script.md(候选 1+3)。历史更清晰,但第二次提交要基于第一次的中间态。
➡️ 推荐 B。候选 2 带行为变化(大整数播报对齐屏幕),值得独立一张 issue 和独立提交说明;候选 1+3 是纯重构无行为变化,适合紧随其后单独验收。
❓ Q9 - 记录型假合成器住哪:测试用的 recording 适配器(实现 SpeechSynthesizing,把收到的文本存数组供断言)放在哪个 target?
- A. 测试 target(
ElderCalculatorTests/):假适配器只为测试存在,不进 App 二进制。 - B. App target:万一以后要做”播报历史”之类的调试 UI 可以复用。
➡️ 推荐 A。一个适配器是假想接缝、两个才是真实接缝——第二个适配器为测试而生,就住在测试里;将来真出现第三个消费者再搬不迟(YAGNI)。
请按编号回答(“都按推荐”即可)。这轮答完后,设计树的前沿还剩测试策略细节,应该一轮就能收尾。
😀
都按推荐
🤖
第二轮全部落定。动手前我自查一个事实:候选 2 的行为对齐会牵动哪些既有测试。
关键事实:现有语音测试最大用例只到 123,456,789(约 1e8),远在 1e13 之下——口径对齐不会破坏任何现有测试。但顺藤摸瓜发现一个走查没抓到的问题,必须在动手前请你拍板。
新发现(e-notation 漏进语音):结果 ≥ 1e16 时(如 999,999,999,999 × 999,999,999,999 ≈ 1e24),%.10g 产出 "9.999999999e+23" 这种科学计数法文本,而 ChineseSpeechFormatter.swift:15 逐字符映射数字时,e、+ 会被 ?? 0 吞成”零”——今天就会念出”九点九九九…零二三”之类的错误播报。候选 2 提取 NumericText 时绕不开它。
第三轮
❓ Q10 - 科学计数法结果的播报策略:结果大到 %.10g 输出 e-notation(≥ 1e16,日常几乎不可达,但存在)时,语音念什么?
- A. 念固定提示语:如”结果过大,请核对屏幕”。屏幕维持科学计数法显示,语音诚实地说”念不了”。新增一句播报词,逻辑一个分支。
- B. 逐位念尾数 + 量级:“九点九九九乘以十的二十三次方”。最忠实,但”十的二十三次方”对目标用户是没有意义的声音串,实现也最复杂。
- C. 维持现状:念错文本。不可接受,列出仅为完整性。
➡️ 推荐 **A”。对 60–80 岁用户,承认”这个数字太大念不了,看屏幕”比念一串无法理解的天文数字更适老;屏幕大字显示正好补上。
❓ Q11 - 新测试打到哪一层:
- A. 两层都测:
SpeechScriptTests用决策表覆盖(按键 × 状态 → 播报词),VoiceAnnouncerTests用记录型假合成器测协调器职责(开关关闭时静默、文本透传)。 - B. 只测 SpeechScript:协调器只剩三行编排,肉眼验收即可,少一个测试文件。
➡️ 推荐 **A”。协调器里仍有真规则(speechEnabled 静默),而假适配器在 Q9 已决定要写——写了不用是浪费,用它测协调器正好让”两个适配器”的接缝成立。
❓ Q12 - 够不着的死代码删不删:NumericText 统一口径后,整数值最多 10 位有效数字、手动输入最多 12 位——ChineseSpeechFormatter.swift:22 的”万亿”节单位(需 13 位以上)和 第 38 行 的 16 位以上逐位兜底都变成不可达代码。
- A. 删掉:实现吸收不了的死路径就删,模块变浅变干净;测试里对应用例一并删。
- B. 保留:
spoken是 public 静态方法,防御”未来有别的调用方传天文数字”。
➡️ 推荐 **A”。删除测试:这两条路径删掉,复杂度真的消失(没有任何调用方能喂给它们)。“未来调用方”是假想接缝,YAGNI。
❓ Q13 - NumericText 住哪个目录:
- A.
Engine/:它是”结果数值的规范文本”,数值的家在引擎侧;Speech 和 Engine 都 import 它。 - B. 项目根目录:标榜”共享”,但项目一共三个目录,单独放反而显眼得过了头。
➡️ 推荐 **A”。规范文本描述的是计算结果的数值口径,本质属于计算语义;语音只是它的第二个消费者。
这四题答完,设计树的前沿就清空了,我会把整棵树汇总给你确认后再动手。
😀
都按推荐
🤖
第三轮全部落定,设计树前沿已清空。汇总整棵树,确认后开工。
决策树汇总
-
Issue 13 · 数值规范文本(候选 2,含行为对齐)
- 新建 Engine/NumericText.swift:
static func canonical(_ value: Double) -> String,规则取引擎现行口径——整数且abs < 1e13精确展开,否则%.10g(Q4、Q13) CalculatorEngine.formatted改为grouped(canonical(...));ChineseSpeechFormatter.numericText删除,改用同一入口- 行为对齐:1e13–1e16 的整数结果,语音从念精确值改为与屏幕一致(修掉现存发散)
- e-notation 结果(≥1e16):语音念固定提示语”结果过大,请核对屏幕”(Q10,措辞实现时可微调)
- 删除不可达代码:“万亿”节单位、16 位以上逐位兜底(Q12)
- 测试:新增 NumericText 测试;现有 40+ 语音测试全部不受影响(最大用例 1.2e8);补 ≥1e13 的对齐用例
- 新建 Engine/NumericText.swift:
-
Issue 14 · 播报词决策分层(候选 1 + 候选 3)
- 新建
Speech/SpeechScript.swift:static func script(for key: CalculatorKey, state: DisplayState) -> String?纯函数(Q1) - 规则:错误态优先念错误、数字念数值、
=念mainValue、功能键念固定词;非法数字守卫保留(Q6) spokenWord扩展删除,“加/减/乘/除”并入 SpeechScript 内部(Q5,候选 3 顺带落地)- 新接缝
protocol SpeechSynthesizing { func speak(_ text: String) }:生产适配器包装 AVSpeechSynthesizer(含打断逻辑与语速映射),记录型假适配器住测试 target(Q2、Q9) - VoiceAnnouncer 退为薄协调器:开关判断 + SpeechScript + 合成器;ContentView 一行不动(Q3)
- 测试两层:SpeechScriptTests 决策表 + VoiceAnnouncerTests 用假合成器测开关静默与文本透传(Q11)
- 新建
-
领域模型与记账
- CONTEXT.md 新增两个术语:播报词决策(SpeechScript)、数值规范文本(NumericText)(Q7)
- 两张 issue 落
.scratch/elder-calculator/issues/,两次提交:13 先(行为变化,独立验收),14 后(纯重构)(Q8)
理解一致的话我就按 TDD 开工:先 issue 13(红 → 绿 → 重构),再 issue 14。有要调整的地方现在说。
😀
有需要记录的 ADR?
🤖
按项目规范,ADR 由 domain-modeling 流程惰性创建。先加载该技能。
mattpocock-skills:domain-modeling skill
Read CONTEXT-FORMAT.md
Read ADR-FORMAT.md
格式清楚了。按”难逆转 × 缺背景会感到意外 × 真实权衡”三条检验本次决策树,两个决策够格记 ADR:
| 决策 | 检验 |
|---|---|
| Q4+Q10 语音数值口径(对齐屏幕;超范围念提示语) | ✅ 三条全中:未来读者会疑惑”为什么语音不念精确大数”,并可能把它”修”回发散状态;逐位念、屏幕放宽都是真实备选 |
| 候选 1 分层(决策纯函数 + 合成器可注入) | ✅ 三条全中:未来会有人图省事把 AVSpeechSynthesizer 直接内联回去,ADR 记录”播报规则必须可测”这条产品级理由 |
| Q6 双守卫、Q9 假适配器位置、Q13 目录 | ❌ 易逆转、不意外,跳过 |
| 候选 4 未实施 | ❌ 你没有给出承载性理由,只是没选它,跳过 |
现在落盘:两个 ADR + CONTEXT.md 补两个术语(Q7 已确认)。
Edit CONTEXT.md
Write 0001-speech-numeric-policy.md
# 语音播报的数值口径以屏幕为准,超范围念固定提示语
屏幕显示与语音播报共用同一数值规范文本口径:结果最多 10 位有效数字(spec 既定)。此前语音对 1e13–1e16 的整数结果念完整精确值,而屏幕按 `%.10g` 显示成科学计数法——对老花眼用户,"听到的与看到的对不上"比"没念精确值"更困惑,故语音对齐屏幕。结果大到屏幕只能以科学计数法显示时,语音念固定提示语"结果过大,请核对屏幕",不逐位念尾数与量级。
## Considered Options(考虑的方案)
- **语音保留精确念值、屏幕阈值放宽到 1e16**:违背 spec"结果最多保留 10 位有效数字"的既定口径。
- **超大结果逐位念"尾数 + 十的 N 次方"**:最忠实,但"十的二十三次方"对目标用户是无意义的声音串,实现也最复杂。
- **维持现状**:e-notation 文本的 `e`、`+` 字符在逐字映射时被吞成"零",会念出错误播报,不可接受。
## Consequences(决策)
16 位以上逐位兜底在统一口径后不可达(整数结果 <1e13 按精确值播报,最多 13 位;其余 ≤10 位有效数字;手动输入 ≤12 位),已随之删除。"万亿"节单位保留:1e12–1e13 的整数结果(万亿级)在应用内真实可达。
Write 0002-speech-script-layering.md
# 播报词决策与语音合成分层
语音播报是适老化核心功能,其规则(每个按键念什么、错误态优先)必须可测试。因此播报模块拆为两层:播报词决策是纯函数(按键 + 展示状态 → 播报词),与硬件无关;语音合成器待在接缝之后、可注入替换——生产用系统合成器,测试用记录型假适配器。决策模块不认识开关与合成技术;开关判断留在协调层。
## Considered Options(考虑的方案)
- **播报词并入计算引擎**(展示状态附带播报文本):接口更少,但计算语义与播报规则搅进同一个状态机,两个领域的规则失去各自的局部性。
- **不拆决策、仅把合成器改为可注入**:播报词规则仍没有独立测试面,测试要连带构造设置与打断逻辑。
## Consequences(决策)
运算符的播报用词("加/减/乘/除")作为播报规则住进决策模块,而非运算符定义处——运算符的屏幕符号与运算语义留在引擎,播报表述归播报。
领域模型已落盘:
- docs/adr/0001-speech-numeric-policy.md — 语音数值口径对齐屏幕,超范围念固定提示语(含三个被否决的备选,防未来重新提议)
- docs/adr/0002-speech-script-layering.md — 播报词决策与语音合成分层(记录”为什么不能让合成器直连决策”)
- CONTEXT.md 新增两个术语:播报词决策、数值规范文本(纯领域描述,不含实现细节)
这两个 ADR 会在下次架构走查时被自动读到,候选 2 的”要不要让语音念精确值”和候选 1 的”要不要把合成器内联回去”都不会再被重新提议。
决策树至此完整:13 项决策全部落定、两份 ADR 记录、两个术语入库。确认无误的话我就按 TDD 开工——先 issue 13(数值规范文本,含行为对齐),再 issue 14(播报词决策分层,纯重构)。
😀
OK
🤖
Write 13-numeric-text.md
# 13: 数值规范文本与播报口径对齐
**What to build:** 提取共享的数值规范文本口径(≤10 位有效数字、智能去尾零、不带千分位),屏幕格式化与语音播报都从它出发,消除两处独立实现靠注释对齐的隐式耦合;屏幕按科学计数法显示的结果,语音念固定提示语"结果过大,请核对屏幕",不再因 e-notation 字符被吞而念出错误文本(ADR-0001)。
**Blocked by:** 无
**Status:** ready-for-human
- [x] NumericText 模块:canonical 口径实现 + 单元测试
- [x] 引擎 formatted 改为在规范文本上加千分位,屏幕显示行为不变
- [x] 语音 numericText 删除,改用同一口径;1e13–1e16 整数结果播报与屏幕对齐(行为变化)
- [x] 科学计数法结果念固定提示语
- [x] 删除 16 位以上逐位兜底(不可达);"万亿"节单位保留(万亿级整数结果可达)
- [x] 全部测试通过
## Comments
实现说明(2026-09-22):
- 新增 Engine/NumericText.swift:canonical 口径(整数 <1e13 精确展开,否则 %.10g),屏幕 formatted 与语音播报共用。
- 行为变化:结果大到屏幕按科学计数法显示时(整数 ≥1e13、非整数 ≥1e10),语音念"结果过大,请核对屏幕";此前这类值会被念成错误文本(e-notation 的 e/+ 被吞成"零")或与屏幕不一致的精确值。
- 走查阶段曾判断"万亿"节单位可删,实现前复核发现 1e12–1e13 整数结果(万亿级,如 999,999,999,999 × 10)真实可达,予以保留;仅删除 16 位以上逐位兜底。
- 66 个测试全部通过(新增 NumericTextTests 5 例 + 大数对齐 1 例)。
- 待人工验收:App 内算一笔大数乘法(如 999,999,999,999 × 999,999,999,999),确认屏幕科学计数法显示 + 语音念提示语。
红阶段:测试引用尚不存在的 NumericText,预期编译失败。
Bash 运行全部测试验证绿阶段
绿阶段完成:66 个测试全部通过,包括新增的对齐用例。更新 issue 13 状态并提交(先文档决策,后代码,CLAUDE.md 的既有改动不卷入)。
Bash 提交 ADR 与术语表更新
Bash 提交 issue 13 实现
Write 14-speech-script.md
# 14: 播报词决策与语音合成分层
**What to build:** 播报模块按 ADR-0002 拆层:播报词决策(SpeechScript)为纯函数模块——按键 + 展示状态 → 播报词;语音合成器退到 SpeechSynthesizing 接缝之后,生产用系统适配器、测试用记录型假适配器;VoiceAnnouncer 退为薄协调器(开关判断 + 编排);运算符播报用词并入决策模块。ContentView 编排不变。纯重构,无行为变化。
**Blocked by:** 13
**Status:** ready-for-human
- [x] SpeechScript 纯函数模块 + 决策表测试(错误优先、数字、非法数字、运算符、等号、功能键)
- [x] SpeechSynthesizing 协议 + 系统合成适配器(打断、语速映射)+ 记录型假适配器(测试 target)
- [x] VoiceAnnouncer 薄协调器 + 协调器测试(开关静默、文本透传)
- [x] spokenWord 扩展从 VoiceAnnouncer 搬入 SpeechScript(私有)
- [x] ContentView 仅改构造一行,行为不变
- [x] 全部测试通过
## Comments
实现说明(2026-09-22):
- 新增 Speech/SpeechScript.swift:播报词决策纯函数,含运算符播报用词(原 VoiceAnnouncer 的 spokenWord 扩展,转为私有)。
- 新增 SpeechSynthesizing 协议(在 VoiceAnnouncer.swift);新增 Speech/AVSpeechSynthesizerAdapter.swift 承接打断逻辑与 SpeechRate→avRate 映射。
- VoiceAnnouncer 退为薄协调器:开关 → 决策 → 合成器,三行编排。
- ContentView 仅构造处一行变化(注入系统合成适配器)。
- 76 个测试全部通过(新增 SpeechScriptTests 7 例、VoiceAnnouncerTests 3 例)。
- 待人工验收:实际播放一遍播报(数字、运算符、等于、错误态、设置里关闭语音),确认听感与重构前一致。
Bash 运行测试确认红阶段(SpeechScript 等未实现)
Bash 运行全部测试验证 issue 14 绿阶段
绿阶段完成:76 个测试、5 个套件全部通过(新增 SpeechScriptTests 7 例 + VoiceAnnouncerTests 3 例)。更新 issue 14 并提交。
Bash 提交 issue 14 实现
三个提交全部落地。存一条项目测试命令到记忆,然后收尾。
全部完成
三个候选全部落地,三次提交,76 个测试全绿:
| 提交 | 内容 |
|---|---|
c8fb22b docs | ADR-0001(语音口径对齐屏幕)、ADR-0002(播报分层)、CONTEXT.md 新增两个术语 |
a076f7f feat (ticket 13) | 候选 2:NumericText 统一数值口径;修掉两处存量 bug——e-notation 被念成错误文本、1e13–1e16 语音与屏幕发散;删 16 位兜底死代码 |
32e375b refactor (ticket 14) | 候选 1+3:SpeechScript 纯函数决策模块、SpeechSynthesizing 接缝 + 双适配器、VoiceAnnouncer 退为薄协调器、spokenWord 并入决策层 |
过程中的两处诚实更正也已记录在案:Q12 的”万亿可删”分析有误(万亿级整数结果真实可达,已保留,ADR 已修正);提示语触发条件以”屏幕显示科学计数法”为准而非死板的 ≥1e16。
留给你的人工验收(两张 issue 都已标记 ready-for-human):
- 13:算
999,999,999,999 × 999,999,999,999,确认屏幕科学计数法显示 + 语音念”结果过大,请核对屏幕” - 14:完整听一遍播报(数字、运算符、等于、除以零、设置里关语音),确认听感与重构前一致
候选 4(错误态双字段)按 Speculative 留在报告里未动——将来如果错误态种类变多(不止除以零),那时再重开它会更划算。
目录结构(完整)
.
├── .scratch
│ └── elder-calculator
│ ├── issues
│ │ ├── 01-project-scaffolding.md
│ │ ├── 02-digit-input-display.md
│ │ ├── 03-arithmetic-equals.md
│ │ ├── 04-decimal-sign-percent.md
│ │ ├── 05-backspace-clear.md
│ │ ├── 06-errors-bounds.md
│ │ ├── 07-chinese-speech-formatter.md
│ │ ├── 08-voice-announcer.md
│ │ ├── 09-calculation-history.md
│ │ ├── 10-settings-themes.md
│ │ ├── 11-keyboard-input.md
│ │ ├── 12-window-app-polish.md
│ │ ├── 13-numeric-text.md
│ │ └── 14-speech-script.md
│ └── spec.md
├── CLAUDE.md
├── CONTEXT.md
├── DESIGN.md
├── DESIGN.overrides.md
├── docs
│ ├── adr
│ │ ├── 0001-speech-numeric-policy.md
│ │ └── 0002-speech-script-layering.md
│ ├── agents
│ │ ├── design-pipeline.md
│ │ ├── domain.md
│ │ ├── issue-tracker.md
│ │ └── triage-labels.md
│ └── guides
│ ├── ai-native-design-pipeline.html
│ └── ai-native-design-pipeline.md
├── ElderCalculator
│ ├── AppSettings.swift
│ ├── Assets.xcassets
│ │ └── AppIcon.appiconset
│ │ ├── Contents.json
│ │ ├── icon-1024.png
│ │ ├── icon-128.png
│ │ ├── icon-16.png
│ │ ├── icon-256.png
│ │ ├── icon-32.png
│ │ ├── icon-512.png
│ │ └── icon-64.png
│ ├── ContentView.swift
│ ├── ElderCalculatorApp.swift
│ ├── Engine
│ │ ├── CalculatorEngine.swift
│ │ └── NumericText.swift
│ ├── HistoryDrawer.swift
│ ├── SettingsView.swift
│ └── Speech
│ ├── AVSpeechSynthesizerAdapter.swift
│ ├── ChineseSpeechFormatter.swift
│ ├── SpeechScript.swift
│ └── VoiceAnnouncer.swift
├── ElderCalculator.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ └── xcshareddata
│ │ └── swiftpm
│ │ └── configuration
│ └── xcshareddata
│ └── xcschemes
│ └── ElderCalculator.xcscheme
├── ElderCalculatorTests
│ ├── CalculatorEngineTests.swift
│ ├── ChineseSpeechFormatterTests.swift
│ ├── NumericTextTests.swift
│ ├── ScaffoldingTests.swift
│ ├── SpeechScriptTests.swift
│ └── VoiceAnnouncerTests.swift
├── prototype
│ └── index.html
└── tools
└── generate-icon.swift