方法 · 端到端实践

三板斧。

一个开源项目,如何从一句业务上的话走到需求、设计、真实用户和支持,再回到新需求。以 MRRC Modern 为完整样本,方法是 SDD、Harness、FDE,每一站都有证据。

75 天 · 67 行 SDD 版本记录 22 条机器可读约束 测试 262 → 1,299 v1.0.0 → v1.18.1 · 三平台

BG1SB  ·   ·  约 22 分钟

大多数工程文章都从半截讲起:需求来了,照着做设计,测试通过,收工。这篇想从项目真正开始的地方讲起:一个人开口说出的那句业务上的话。然后跟着 MRRC Modern 走完它的前 75 天(2026-07-06 → 2026-09-19):一台业余无线电的远程控制服务器,从单电台的小实验,长成覆盖三个平台、十个机型的产品,还带着一条对外的支持链。用的办法,就是这套生态里反复打磨的三板斧:持续更新的 SDD(契约)、工程 Harness(把契约变成机器执行力的装置)、FDE(让现场不断回头修正前两者的闭环)。「三板斧」这个说法,出自民间传说里的程咬金:梦中得授斧法,醒来只记得三招半,却靠这三招半闯出了名头。借用到工程上,取的就是这个意思——路数不在多,在每一斧都落得到实处。下面每一站,都能落到某个提交、某行 SDD、某个测试、某份发布产物,或者某张公开答复卡上。

本文要回答的六个问题

  • 事实一句话是怎么变成契约的?
  • 事实路线图是从哪儿来的?
  • 事实知识怎么变成了机器能读的东西?
  • 推断支持为什么变成了一张传感网?
  • 推断新需求和旧需求走同一扇门吗?
  • 论点什么可以搬走,什么搬不走?

同一条链,两种断法

业务意图 → 需求 → 设计 → 现场 → 支持 → 新需求。每个项目都在走这条链,区别只在断在哪里。

这条链在幻灯片上是直线,在现实里是个环:需求要等现场信号确认它重要,才算真的成立;现场信号要能改变设计,才算没有白费。环闭上了,项目就开始复利;断了,通常是两种样子。

愿景落不了地

业务上的那句话在路演里反复出现,需求却慢慢漂成一张功能清单。没人说得清「这句话在哪个产物里变成了可度量的义务」,于是每次争论都从观点重新开始。

现场回不来

迭代很快,但现场教会的东西只留在聊天记录和维护者的记忆里。同一个事故,每个季度都被重新发现一次;版本号能告诉你发布了什么,却说不出它学到了什么。

MRRC Modern 的设计,就是要让这两种失败变得很贵。它始于 2026-07-06——一个单电台实验(FT-710 走自己的 USB 线)的初始提交;到 2026-09-19,已经攒下 435 个提交、67 行 SDD 版本记录、22 条架构决策、22 条机器可读的约束、37 条 changelog 记录,版本从 v1.0.0 走到横跨 Windows、macOS 和树莓派镜像的 v1.18.1,外加一条公开的支持链,带答复页、进度板和无人值守的落实环节。数字不是重点,重点是产出这些数字的这套装置。

作者论点

三板斧是一个系统:SDD 把判断沉淀成契约,Harness 让「忘记」在结构上不可能发生,FDE 让现场持续修正前两者。抽掉任何一根,你立刻就能说出对应的失败:一份没人遵守的漂亮文档、一个没有记忆的快速迭代,或者一份不再听现场说话的契约。这也是总纲页的论点——执行可以委托,判断不能——在整条生命周期上的展开;那一页承诺的可证伪链条(现场事故 → SDD 裁决 → 约束 → 阻止重演),正是 §02–§05 要走的路(总纲)。

活得比对话更久的契约

Harness 三层结构里,那份随代码一起演进的设计文档:业务(为什么)、技术(怎么做)、产品(交付什么)。

2.1 · 从一句话到可度量的需求

项目的 Echo 阶段先于一切架构,只产出了一段话:

事实 · 催生了 SDD V1.0 的 Demo 定义

「用浏览器遥控 FT-710,用一根 USB 线替代 SCU-LAN10,不加任何硬件。实时瀑布(FT4222 SPI)。双向 48 kHz Opus 音频。移动优先 UI。四条 WebSocket 通道(控制、音频 RX、音频 TX、频谱)。」

Echo 的输入都很具体:作者本人在多台设备上的日常操作;对已有开源 FT-710 遥控项目(F4HTB)的评估——没有 SPI 频谱、没有双向 Opus、没有移动 UI;对照 Hamlib 记下的差距;还有那个不公平的优势:从更早的 MRRC 项目继承来的可复用模式(PTT 安全、多实例架构,以及 SDD 模板本身)。这段话变成了基线:全部 15 章一次写完,其中有一张编号的 NFR 表,每条都带目标值和验证方法。在这个项目里,需求要写成「现场测试能裁定的义务」:

需求目标验证方法
NFR-001 · RX 音频延迟(电台喇叭 → 浏览器喇叭)端到端 < 500 ms听感测试
NFR-002 · 控制响应局域网内 UI 指令确认 < 200 msWebSocket 往返观测
NFR-008 · PTT 响应触摸到 TX 指令 < 100 ms时序日志
SC8 · 成功标准PTT 不能卡死安全架构 + 看门狗测试

这个模式很小,也很严格:没有目标值和验证方法的需求,只能算意见。「PTT 不能卡死」之所以成立,是因为背后有一整章(SDD 15,PTT 安全架构)和一只看门狗撑着——它不是口号。《礼记·中庸》讲「凡事豫则立,不豫则废」,紧跟着的是「言前定则不跲,事前定则不困」——话先说定,才不会临事磕绊;事先谋定,才不会当场受困。需求带上目标值和验证方法,就是「事前定」在工程里的样子。

2.2 · 从需求到设计决策

设计决策在这里是编号的、带日期的、可追溯的:AD-001 … AD-022,一张决策登记表。每一条背后都有 Echo,有几条背后是真实的事故:

决策定了什么被什么逼出来的
AD-002FT-710 这条路直接走 pyserial CAT,不用 HamlibHamlib 差距分析;直连协议更简单,还能拿到真频谱
AD-01148 kHz 编解码域 + 帧对齐的 44.1 kHz 桥现场信号:「16 kHz TX 爆裂声」
AD-014永不轮询 DN;——每调一次,VFO 就向下走约 20 Hz一次真实的频率漂移事故
AD-017录音写入线程按会话幂等重建;队列满时丢最旧的块2026-09-12 那份录音无声的 MP3 现场报告
AD-019未验证机型拒绝发射,除非明确放开新机型来得比硬件验收更快
AD-021 / AD-022支持诊断包、初步研判,以及能自证的更新通道支持这件事本身:第一轮邮件永远在要证据

2.3 · 版本行是学习的最小单位

SDD 的版本历史不是 changelog 的副本,它是项目的学习记录。一次变更一段:现场信号或需求、证据、决策、还没闭合的边界。75 天 67 行,有的只是一行修复,有的是一整条支持链。V2.45 那行读起来像一份带结论的现场报告:录音无声的日志特征(开始 6 ms 后就出现 Recording writer is falling behind — dropping audio,22 秒里丢弃 950+ 次)、用 ffmpeg 测出的产物(-91 dB,即数字静音,时长却是对的)、根因、修复、三个回归测试,外加边界:「长时间连续录制和树莓派 SD 卡写入仍未验证」。

事实 · 没有自动升级

生命周期状态是显式的——planned、implemented、tested、bench-verified、field-verified、released、deferred、known-issue——没有任何一个状态会自己往前走。一个已发布的子系统,不能把成熟度借给别的客户端或机型;未验证的 Icom 和 Yaesu 机型带着发射限制发布,并且如实写明。

不需要被记住的约束

一条教训如果只活在文档里,迟早会被跳过。Harness 的职责,是让「跳过」当场失败。

3.1 · 两层,一个目标:让失败可见

Harness 分外层和内层:外层是业务、技术、产品三组约束(即 SDD 的三组章节),内层是执行层(人 + 智能体、仓库、工具、测试 + 审查、部署、现场遥测)。本文聚焦内层最锋利的一件工具:约束清单。

3.2 · 约束清单:22 条教训变成机器

.agents/skills/sdd-guardian/harness/constraints.json 里存着 22 条规则,从 SDD 和真实事故中提炼,每条带严重级、理由和匹配模式。智能体(或人)在会话一开始就拿到它们;编辑前的检查会以退出码 2 拦下违规。重点是这份清单的来历——它们不是风格偏好:

约束严重级固化的是什么
cat-no-dnblockDN; 每调一次,活动 VFO 就向下走约 20 Hz——一次真实的频率漂移事故,如今不可能再被无意间写回来
cat-direct-serial-ioblock串口 I/O 只允许出现在电台控制器内部,防止 asyncio 传输层被到处直接触碰
audio-16k-rateblock音频速率是 44.1 kHz 设备 / 48 kHz 编解码,音频路径上永不出现 16 kHz(爆裂声事故);16 kHz 只属于存储
poll-stale-guardinfo轮询循环丢弃过期的在途读数(「滤波器切换只有 50% 生效」那次竞态)
ptt-release-no-verifyinfoTX0 是发完即忘;松开时的校验循环正是 PTT 卡死的根源
password-constant-timewarn口令比较必须使用 hmac.compare_digest

换算规则只有一条:事故变成守卫,才算修完。修复是第一环;SDD 里的裁决是第二环;能在下一次动手前拦住的机器可读约束是第三环。这份清单之所以又小又单调,原因就在这里:每一行都付过真实的代价。《战国策·赵策一》里有一句话:「前事之不忘,后事之师。」前面的事不忘掉,就是后事的老师。约束清单就是这句话的机器形态——这里的「不忘」,靠的不是谁的记性,而是编辑路径上的那道关卡。

3.3 · 把关与唯一事实来源

清单周围还有一整套装置:unittest 套件(7 月第 12 个循环时 262 项 → 9 月 FDE 进度板那次提交时 1,299 项)、离线可跑的 release_check 规则(发布记录里 29 项通过 / 0 失败)、记录每份产物版本/大小/SHA-256 的发布产物清单,以及强制文档一致的测试——版本历史首行的 SDD 版本,必须和 README、生成页、落地页一致。给人看的入口是 docs/PROJECT_MAP.md:它只回答一个问题,「我改了 X,必须同步哪些东西?」每个主题一行:

主题唯一事实来源靠什么强制
应用版本CHANGELOG.md 顶部条目构建脚本解析它;release_check
SDD 版本SDD/14-version-history.md 首行test_sdd_docs_consistency.py
设计决策SDD/08-architecture-decisions.md(AD-001…AD-022)release_check 的 AD 索引 + 守护清单
工程约束constraints.jsonsdd_context.py check——block 级违规退出码 2
发布产物release-artifacts.json(版本 / 大小 / SHA-256)release_check.py + 产物测试
推断 · Harness 让委托这件事变得安全

只有仓库自己会拒绝,你才能放心把仓库交给智能体。约束清单把项目的伤疤变成编辑前的拦截,这和产品里的发射限制是同一个设计观:权限有边界、拒绝要显式,每次拒绝都注明来历。

Echo → Delta → Product,以及第四项实践

刻在血统里的现场闭环:到现实里去、证明风险最高的假设、把活下来的东西抽象出来,再让支持链对产品自己跑同一个循环。

4.1 · 十二个循环,每个只由一个现场信号触发

陆游教儿子读书时写过一句:「纸上得来终觉浅,绝知此事要躬行」(《冬夜读书示子聿》)——纸上得来的终是浅的,要真懂,得亲自到现场去做。MRRC Modern 的第一个时代,就是把这句诗立成了纪律:它留下了一张 git 可查的循环表。每一行三样:一个 Echo(现场信号)、一个 Delta(对最高风险的证明)、一个 Product 产出(活下来的抽象)。节选:

循环现场信号(Echo)造出来的东西(Delta → Product)
V1.0 · 7 月 6 日替代 SCU-LAN10:一根 USB 线、不加硬件一整天写完 15 章 SDD 基线 + 7 个核心模块
V1.1 · 7 月 6 日「16 kHz TX 爆裂声」AD-011:48 kHz 编解码域 + 44.1 kHz 帧对齐桥
V1.2 · 7 月 8 日「频率漂移约 20 Hz;TX 表头空白」活动 VFO 跟踪、表头校准,以及后来成为 cat-no-dn 的那条裁决
V1.7 · 7 月 19 日「滤波器切换只有 50% 生效」每次轮询查询后丢弃过期读数 + 3 个回归测试
V1.9 · 7 月 21 日「USB 重连后瀑布永远冻结」on_reconnected 钩子在重连后重跑频谱初始化
V2.1 · 7 月 22 日「PTT 键控了但没有声音 / 没有功率」TX 上行归因改进 + 每会话 TX 计数 + 5 个回归测试

四个模式撑起了这个时代,而且四个都可以复制:一个循环 = 一个真实信号(不做投机性的功能);文档同步写,不是事后补(版本行随修复一起落地);风险最高的假设先做原型(先直连 CAT,再 48 kHz 管线,最后多客户端归属);从既有杠杆出发(从更早的项目继承了六项资产,第一个循环做的是「集成」,不是「发明」)。

4.2 · Product 意味着抽象,然后成为平台

Product 阶段,是把现场的土办法变成资产的阶段:独立频谱子进程走带长度前缀的管道(FT4222 的逐字节重同步已被证明走不通)、帧对齐的 44.1↔48 kHz 重采样、带命令跳过和过期读丢弃的自适应轮询、带标签的双编解码音频帧、按次记录的 PTT TX 会话日志。决定性的抽象出现在八月:可插拔电台后端。服务端、轮询和状态层不再关心是哪台电台,机型差异全部收进 profile。这一步把 FT-710 的解法变成了平台:共享 CI-V 核心的五个 Icom 机型、共享 ASCII-CAT 核心的四个 Yaesu 机型,全部跑在已经现场验证过的 FT-710 传输层上,并且在硬件验收追上之前,带着发射限制发布。

4.3 · 第四项实践:支持闭环

第四项实践把同一套 Echo→Delta→Product 的纪律,对准产品自己的支持链。它分四块、三天上线:(1)脱敏诊断包;(2)自动预检和公开答复页;(3)带生成式清单的只读更新检查;(4)公开的现场进度板和无人值守的落实者。组织它的规则是:分类就是决策——noise 以「已答复」关闭,bug 自动排进落实队列,feature 等操作员拍板,没答复的上报永远不关闭。落实者从干净的 main 工作区独立运行:只读模型按 unified-diff JSON 契约提补丁;护栏拒绝受保护路径和超大补丁;补丁只落在隔离的 fde/<id> 分支上,整套测试全绿才提交。main 永远不让无人值守的进程碰,合入始终是人来做。机制写在机制分册 · 现场闭环;第一周的日志在《闻过则喜》。

两条线索,一站不漏

现场事故,只有能看着它走完全程才有意思。两个真实案例:从症状到已发布的修复,再到公开答复。

5.1 · 线索 A ——「录音没有声音,但文件看上去完全正常」

现场(2026-09-12 22:26)。一次真实会话录出的 MP3,时长和大小都对,就是没有声音。日志里有明显的特征:开始仅 6 ms 就出现 Recording writer is falling behind — dropping audio,21.9 秒里丢弃 950+ 次,最后以 Recording queue full on stop — finishing directly 收尾。ffmpeg 测得产物为 -91 dB:数字静音。

设计(V2.44/V2.45)。按设计,写入线程是「每会话跑一次」的:消费到 stop 哨兵就退出,可它只在进程启动时创建了一次。第一次录音之后,后面每个会话都没有消费者;有界队列毫秒级就灌满,stop() 再按时间戳把整条时间轴补成静音。修复:幂等的 _ensure_rec_writer(),每次音频块入队、stop 入队和每次 REC 时都重建;写入线程异常死亡就打 WARNING(这正是那次事故最缺的可观测性);队列满时丢最旧的块,不再同步收尾。三个回归测试,随 v1.15.0 发布。

支持(2026-09-18)。一份新的诊断包带着同样的特征到了,但所有命中的日志行都停在 9 月 12 日——修复之前。预检模型被要求:下结论之前,先读项目自己的事故史。于是它交叉对照了 CHANGELOG 和 SDD 版本行,定位到当前代码里的 server.py:348,并且顶住了两个顺手的答案(「早就修好了」/「回归了!」),改为执行一套流程:录一段新的 QSO 再回报。后续诊断包带着 9 月 19 日的健康日志,关掉了这一单。公开答复卡上写明了推理过程和原始证据行。

推断 · 版本行是线索沉淀的地方

没有这一行,第二份上报就得从零开始,维护者要把两周前的诊断重新推导一遍。有了它,预检就是一次查找:特征 → 历史 → 结论 → 流程。SDD 版本行不是文档债,它是这个项目的免疫记忆。

5.2 · 线索 B ——「应用已损坏,无法打开」

现场(发布周,v1.18.0)。第一个 macOS 安装包发布,用户先是撞上了一个连右键都绕不过去的 Gatekeeper 对话框,接着又遇到第二个更安静的失败——RX 没有声音,日志却让他们「检查电台」。

设计(V2.54,随 v1.18.1 发布)。两个根因,都是静默失败。签名步骤从第一个 macOS 构建起就一直在悄悄失败——构建脚本只把它当非致命警告,所以每个包其实都没签上名;修复按 codesign 的要求重排了资源树布局,并让签名失败直接中止构建。RX 无声则是一个缺失的权限键:bundle 从未申请过麦克风权限,macOS 便照常打开采集流、往里填零,一个错误都不报;修复补上这个键(构建期断言),并把警告指向系统设置,而不是电台。验证做到产物级:codesign --verify 通过、指定要求满足、spctl 只剩缺 Developer ID 的抱怨,系统权限日志从 Refusing… 变成 AUTHREQ_PROMPTING。文档替下一个用户先回答了问题:macOS 指南解释两种对话框的区别,落地页直说 v1.18.1 之前的下载必须换包。

5.3 · 线索 C ——「PTT 松开后的体感延迟」(一段话讲完的案例)

一个现场体感——「松开 PTT 后 RX 回来得太慢」——被日志拆成了约 470–870 ms:采集重开 262–300 ms、排空约 70–130 ms、轮询次序,再加上客户端抖动缓冲里积压的约 120 ms 静音 Opus 帧。V2.55 发了三个独立的修复:释放排空和 unkey 之前跳过 TX 状态轮询、键控期间不再广播静音 RX 帧、记住上次成功的采集设备以跳过重开时的全量枚举——重开延迟后来实测从约 270 ms 降到约 200 ms。边界照旧写清楚:端到端的改善仍需真机验收。

事实 · 三条线索共同的部分

同样六站:现场信号 → 日志证据 → SDD 行(根因 + 决策 + 边界)→ 回归测试 → 发布 → 公开答复卡。差别只在节拍:录音这条线,从信号到修复用了一天,到后续闭环用了一周;macOS 这条线用了一个晚上;延迟这条线用了两天。节拍由最慢的一站决定,而那一站通常是证据,不是代码。

这些数字能支撑什么、不能支撑什么

计数只是过程证据:它们能说明装置在运转,但单凭它们,撑不起任何产品主张。

观察数值(截至 2026-09-19)能支撑什么
仓库跨度2026-07-06 → 2026-09-19,435 个提交同一条产品线上的持续交付
SDD15 章;67 行版本记录;AD-001…AD-022;36 条 NFR;SC1–SC9需求和决策是可追溯的工件,不是谁的记忆
Harness22 条约束(block/warn/info);编辑前的关卡,违规退出码 2;release_check 29 项;产物清单带 SHA-256教训被强制执行,而不是归档了事
测试第 12 个循环 262 项(7 月 22 日)→ 1,299 项(9 月 19 日)每次事故都买回了回归覆盖
发布37 条 changelog 记录;v1.0.0 → v1.18.1;Windows、macOS、rpi64发布是常规动作;回退目标真实存在
电台面3 个协议族、10 个机型键平台抽象经受住了和其他硬件的接触
支持链3 天 4 块;Modern 已发布 2 条答复;进度板在线现场反馈有一条流水线,而不是一个邮箱
事实 · 诚实的边界(写下来,不抹平)

未验证的 Icom 和 Yaesu 机型,在硬件验收前一律拒绝发射;Yaesu 系列现阶段只收不发;更新通道目前是只读的(下载/安装是后续分块);FDE 进度板上的第一条真实 bug 还在等它的第一次真实排期,第一份无人值守补丁还在等第一次人工合入;基于摘要的预检读到的是摘要,不是完整环境。跨项目可比的数字,包括当前维护中的测试计数,只放在证据记录一处。

什么可以迁移,按什么顺序建

从实践里提炼的八条规则,然后是三级采纳阶梯,以及每一级的陷阱。

  1. 契约与第一个提交一起写

    一段愿景文字,然后是全部章节,包括带目标值和验证方法的 NFR 表。第一个提交,是拥有契约最便宜的时刻;往后每拖一刻,都是一次返工。

  2. 给每条需求配一个目标和一个方法

    「< 500 ms,听感测试」才叫需求;「应该快一点」只是偏好。像「PTT 不能卡死」这样的成功标准,背后要有一章安全架构和一只看门狗。

  3. 让现场选路线图

    一个循环 = 一个真实信号,不做投机性的功能。版本行——信号、证据、决策、边界——就是路线图的日志;67 行构成的论证,任何功能清单都给不出来。

  4. 把每次事故变成规则,而不是记忆

    修复是第一环,裁决是第二环,下一次动手编辑前就会被检查的机器可读约束是第三环——到这一步,事故才算彻底修好。清单要小,每一行都该付过真实的代价。

  5. 先给现场装上传感器,再扩功能

    脱敏诊断包把访谈变成了测量。传感器要在需要数据之前就造好;这条支持链分四块上线,它排第一块。

  6. 让分类就是决策

    分类这个问题只回答一次,之后就让它自动调度:环境噪声直接关闭、缺陷排期、feature 等人来拍板。没答复的上报永远不关闭——「闭环」在这里是一个承诺。

  7. 把「做」自动化,永不自动化「裁决」

    无人值守的落实,只有被围起来才安全:只读提议、护栏、隔离分支、全量测试把关、main 不许碰、人工合入。两本账分开记,过程证据永远不能升级成产品主张。总纲把留给人的那一半命名为:意图、边界、裁决;理由是问责,不是任务大小(分工)。

  8. 把边界写下来

    生命周期状态之间没有自动升级;未验证的硬件拒绝发射;更新通道自己写明只读;已知问题保持可见。写下来的边界是信任资产,被用户撞见的边界是事故。

采纳阶梯

阶段建什么完成判据陷阱
1 · 文档时代SDD 基线(业务/技术/产品章节)+ 每次变更一行版本记录 + changelog随便挑一个已发布的行为,你都能说出它背后的需求和决策文档写完就被悄悄放弃——版本行必须随修复一起落地,否则永远是「回头补」
2 · 契约时代从真实事故提炼的约束清单 + 编辑前检查 + 测试/发布规则 + 事实来源地图已知的失败不可能再被写进来而不亮红灯收集一堆没人运行的规则——强制执行(编辑路径上的退出码)才是全部意义
3 · 现场闭环时代诊断包 → 预检 → 答复 → 进度板 → 无人值守落实,合入归人没人在线的时候,用户报告的症状也能变成分支上经过测试的补丁,而裁决仍然归人把手伸过护栏——闭环必须失败即关闭,否则它就变成一段没人审查的 main 历史
作者论点 · 该复制什么

复制纪律,别复制数字。没有第二个项目需要这个项目的测试数或发布节拍;能迁移的是形状:一份沉淀判断的契约、一套拒绝已知错误的装置、一个被允许修正前两者的现场闭环,以及一个始终握着「什么算完成」裁决权的人。

一句话,三板斧

一句愿景变成契约,契约变成带目标的需求,需求变成决策,决策撞上现实然后被修正,修正变成守卫,守卫让下一次委托变得更安全;支持链把环闭上,让用户把现实报回来——附带答复、一块进度板,以及一份测过了、等着人点头的补丁。这就是整段旅程。「豫则立」管契约,「前事不忘」管守卫,「躬行」管现场——三句老话,各押一斧。三板斧,说到底是一句话:

一句话的纪律

让判断沉淀成契约,让机器拒绝那些你已经付过代价的错误,让现场经由用户修正前两者。裁决,始终归人。

深入机制:Harness、Loop 与 Living SDD · 现场闭环 · 智能体工程。本案例背后的日志:七十亿 token 的全程拆解 · 闻过则喜。线上入口:MRRC Modern 站点、答复页、FDE 进度板。