可以追问的记录 — 三十本笔记没能给你看的东西
9 分钟有件事要先说明。 2026 年 4 月,本刊登过一篇文章:俄勒冈一位种了三十年番茄的农户,把谷仓里的三十本笔记搬到了一个画面上。那是一篇点名、点年龄、点地区的采访。
这个现场不是某一家特定的农户。 所以我们不把原采访原样重刊。取而代之,我们把同样的形状从头到尾自己做了一遍。 以下所有画面和数字都出自我们做的东西,不是任何一家农户的记录。
把这个区分放在开篇的理由是:原文整篇的论点是**「你也做得到」这份许可**,而这份许可压在案例的真实性上。没有确认真实性就重复同一句话,那不是许可,是广告。
所以这篇要证明的东西变窄了。不是「某位农户做到了」,而是**「这样的工具长这个样子,而且实际能做到这么多」**。
原文的心脏里没有实物
原文的轴是好的。纸停在「写下来」这一步。 把东西翻出来相互比对,交给了人的记忆和耐心。而一旦以同样的字段堆起来,记录就变成可以追问的东西。
我们现在仍认为那个物性上的转变是对的。问题是那篇文章一次也没有把它拿出来看。
- 他实际说了什么 —需求原文 0 行
- 那句话变成了怎样的画面定义 —定义摘录 0 行
- 经过了哪些步骤做出来 —制作过程 0 步
- 三十本笔记的迁移 — 只有「抽空在搬」这句话
而全文最精彩的**「移栽后第 18 天开第一朵花」**这个发现也一样。那是要五年数据堆起来才成立的主张,可那批数据和它显现出来的画面都不在。
这次我们把三样东西并排放。说出来的 → 变成定义的 → 真正被画出来的。
说出来的
做的过程中,我们把需求写成三句话。不用开发术语。
- 「选个作物,今年的记录就一溜儿出来。」
- 「记录要能单手完成。」
- 「去年这时候什么样,挨着显示出来。」
三句话变成了三个工具。
log offers: log.season, log.add, log.compare变成定义的
第一句变成了画面的这一部分。不是代码,是声明。
{
"type": "list",
"items": "{{season}}",
"emptyMessage": "Nothing recorded this season yet",
"itemTemplate": {
"type": "linear", "direction": "horizontal", "spacing": 12,
"children": [
{ "type": "text", "text": "{{item.date}}" },
{ "type": "text", "text": "{{item.action}}" },
{ "type": "text", "text": "{{item.tempC}}°C" },
{ "type": "text", "text": "{{item.note}}" }
]
}
}第二句变成了一个按钮。
{
"type": "button",
"label": "First flower",
"onTap": {
"type": "tool", "tool": "log.add",
"params": { "crop": "Tomato", "action": "firstFlower", "tempC": 19.5 }
}
}第三句让画面多了一个。就一行路由。
"routes": { "/": "ui://pages/today", "/compare": "ui://pages/compare" }整个应用是四个 JSON。 编译器不会运行。
真正被画出来的
今天的画面。移栽后第几天、以及往年的平均值,都在上面。

按一下「First flower」,就多出一行。

以及把各个年份叠着看的画面。

18.2 天在任何地方都没被写下来
这是这一篇里最重要的部分。
原文把「18 天」当成结果写了出来。在这个样例里,那个数字不是常量。 它每次都从记录里算出来。
/// Days from transplant to first flower, per season.
///
/// This is the whole "eighteen days" claim, and it is arithmetic over the
/// rows — no constant anywhere. A year with no flower recorded yet is left
/// out rather than guessed at.而且验证会直接确认这一点。算出来的值一旦以字符串形式出现在源码里,就判失败。
src = open(os.path.join(SERVER, "bin", "server.dart")).read()
assert str(cmp["avgDays"]) not in src, "the average must come from the records, not the source"通过时的日志。
average 18.2 computed from records, not present in any source file「它是从记录里冒出来的」和「有人把它写了上去」,在画面上长得一模一样。 所以我们把分辨它们的办法放进了通过条件。
例外开始说话
原文里最好的观察不是平均,而是平均之后的那一步。有了锚,偏离的年份开始显形。
算出来的表就是这件事本身。
spans: 2021:22d, 2022:18d, 2023:15d, 2024:18d, 2025:18d, 2026:18d2021 年是 22 天,那一行的备注挂着 five cold days in early April。2023 年是 15 天,挂着 warm week before transplanting。
在散落的笔记里,22 天也好 15 天也好,都会以「今年有点不一样」流过去。没有可比的基线,你连它是例外都不知道。 只有 18.2 这个平均立起来之后,那两年才开始说话。
不是同样的字段就叠不起来
这一切成立的条件只有一个。记录必须是同一形状。
/// One entry, in the same six slots every time. The whole argument of the
/// article rests on this being fixed: `plot · crop · action · date · note ·
/// tempC`. Paper let every year invent its own shorthand; slots do not.六个字段。这正是纸做不到的 — 纸允许每年发明自己的简写。2019 年的「定植」和 2023 年的「挪了」是不是一个意思,笔记本不知道。
关于日期,我们还刻意做了一件事。
// The date is the machine's, not the grower's memory. Paper let a row
// be written three days late with no trace; this cannot.判断与工具的分工
原文最好的资产是一张五行的分工表。要记什么由农户判断,把它搬运则是工具的活儿。我们保留那张表,而这次每一行都指向上面看到的一样实物。
| 什么 | 谁来 | 这篇里的实物 |
|---|---|---|
| 决定要记什么 | 农户 | 六个字段(plot·crop·action·date·note·tempC) |
| 说出想怎么看 | 农户 | 三句需求 |
| 把那句话变成画面 | 工具 | 4 个包 JSON |
| 堆积、筛选、计算 | 工具 | log.season · log.add · log.compare |
| 决定要做什么 | 农户 | —(工具不做判断) |
最后一行和原文那节克制的边界处在同一个位置。那句话原样放在画面底部。
A tendency, not an instruction. Read this season on its own terms.
工具只算平均,不叫你去种。 越过那条线,三十年的判断就用不上了。
这个样例的范围
没有这位农户。 这篇里的三句需求是我写的,五年份的记录是我造的。所以请不要读成「某位农户是这么做的」。 这篇展示的只到「这样的工具长这个样子、只有这么短、而且平均值真的是算出来的」。
而且我们没有确认领域专家能否亲手驾驭这套格式。 上面的 JSON 是人读得懂的,但一位 58 岁的农户能不能自己去改它,是另一个问题,这个系列没有回答。原文所说「说出来就行」的那个转换 — 人的话变成定义的那一步 — 在这里是我用手做的。
搬三十本笔记这件事也没有处理。 把纸搬进字段里的实际工作量,这个样例没有量过。
也写明界面为什么是英文。 这个系列的样例,画面和代码都用英文。这一篇是构成,而非某一家农户的工具,因此任何语言的读者读到的都是同一张截图、同一份源码。
从写下来到追问
原文以「小处着手,一次加一个字段」收尾。这条建议保留。
在它之上,加一行做出来才知道的。把字段保持一致,比增加字段更难。 定下这六个字段用了不到十分钟,但那六个必须在五年里始终一样,最后那个画面才成立。纸失败的地方,正好就在那儿。
而一旦成立,记录就换了性质。从「写下来的东西」变成「可以追问的东西」。 18.2 天不是谁替我们写好的,而是问出来的答案。
练习任务
从设备示例中选一个回答,指出它引用的记录。如果没有匹配的记录,工具应该怎么做?