# 交接说明 — 2026-08-06 > **⚠️ 这不是最新的一份。** 本文写的是 v3.9.20,**当前一份是 > [HANDOFF-2026-10-15.md](HANDOFF-2026-10-15.md)**。状态表与"仍未做"请以最新那份为准。 > 索引在 [README.md](README.md)。交接是 append-only,旧结论不删。 **上一份**:[HANDOFF-2026-08-04-E.md](HANDOFF-2026-08-04-E.md)(它自己在 §8 收口了)。 那一份从 v3.9.17 一直写到 v3.9.19,末尾越写越长 —— 这一份重新开始,只带走**跨版本仍然有效** 的东西,不复述已经有结论的历史。方法论沿用 [AUDIT-LEDGER.md](AUDIT-LEDGER.md) 第十三轮。 --- ## 0. 当前状态 ``` 版本 / tag 3.9.20 / v3.9.20 —— **已发版**,产物已核(§9.6) master == tag == origin/master,**无未发版 commit** 门禁 3473 pass / 0 fail(266 个文件,exit 0) 突变 七个 spec 共 74 条:全部如声明,**零声明漏网**(原六个 spec 有 4 条) secret-scan EXIT=0 ← 只扫**被跟踪**文件且**跳过 test/**,见 §3.1 工作树 干净 开着的 issue #244(模型目录 + 视觉,已回并要数据 —— §9.3) · #236(等报告者数据) · #239(等付费号) · #208(作者公告帖) 开着的 PR #242 · #243(同一贡献者 warelik,**均已评审发出**,等他改 —— §9.4) ``` **本节最容易过期。** 两个实例:上一版写"开着的 PR:无",而 #242/#243 在写下它之后 47 分钟就开了;上一版写"未发版且该发",而发版发生在同一个会话稍后 —— **同一会话内会过期的 句子,发版后要回来改**,这条第十三轮记过一次,这是第二次。 ### 门禁数字权威表 | ref | pass | 文件数 | 新增 | |---|---|---|---| | `v3.9.17` | 3433 | 263 | | | `v3.9.18` | 3447 | 264 | +14(#241 的 11 + 复核补的 3) | | `v3.9.19` | **3449** | **264** | +2(小数上限那两条) | | (master 文档轮,未单独发版) | 3462 | 265 | +13 —— 文档一致性守卫;行为零变更 | | `v3.9.20` | **3473** | **266** | +24 相对 v3.9.19 —— gemini 出口 parity(§9.1)+ 上一行那 13 条 | 测量口径 `npm run test:release`(逐文件进程隔离)。每一行都是那个 ref 上真跑出来的 —— 不要用"上一行 + 本轮新增"推算,那正是第十三轮 guard 2 抓到的错法。 --- ## 1. 接手第一步 ```bash cd /Users/dwgx/Documents/Project/WindsurfAPI git status --short # 期望:干净 git rev-parse master origin/master # 期望:相同 git describe --tags master # 期望:v3.9.19-N-g…(带后缀是正常的,见下) npm run test:release # 期望:3462 / 0(265 个文件) for s in test/mutations/*.json; do npm run mutate -- "$s"; done # 期望:六个全绿 ``` **不要用 `git describe --exact-match`。** tag 之后一旦落了文档 commit,它就 `fatal`,而那是 正常状态。带 `-N-g…` 后缀说明 master 领先 tag N 个 commit;只要那 N 个不含行为变更,不需要发版。 ### 三条会立刻绊住你的操作纪律 1. **突变循环启动后就不要碰仓库。** 它每一步都在 `写入 → 跑测试 → git checkout HEAD -- 还原`, 期间任何别的编辑都会被 guard 6 判脏并中止整轮。要并行干活就去 worktree。 2. **commit 前先切分支。** `.githooks/pre-commit` 拒绝在 master 上 commit,而它拒绝时你那次 `git add` 已经做了 —— 工作树仍是脏的,紧接着跑突变会被 guard 1 再拒一次,看起来像两个 毫不相干的失败。 3. **别跑 `test/_research/responses-cache-hit-seq.test.js`。** 它 2 条失败(**在 v3.9.18 之前 就红**),而且**会写自己的 fixture** —— 误跑一次工作树就脏。`run-test-shard.mjs` 只取 `test/*.test.js` 顶层,所以 `_research/` 从来不在门禁口径内,这两条不是回归。 --- ## 2. 仍未做的 ### 2.1 等外部,不是待办 - **#236** GLM 工具调用停滞 —— 已向报告者列出**四项能相互区分候选机制**的信息(代理 debug 日志里 tool call 的原始形态、绕开 subapi 直连、换模型对照、确认 `GLM-5-2-NONCE` 那个拼法 是不是笔误)。标题说"浏览器操作报错"是误导,正文其实是工具调用停滞,与 #237/#238 同族。 **拿到前两项基本就能定位 —— 别在拿到之前从代码倒推着改。** - **#239** 「支持获取真实的 ACUs 计费」**功能请求**。需付费号跑 `DEVIN_CONNECT_DEBUG_META=1 npm run calibrate:devin`。**注意范围**:billing tag 校准是它的 前置,不是它本身 —— 上一份交接曾把两者记成同一件事。 - **#208** 作者自己的公告帖,没人在等回复。 ### 2.2 可离线做 - **`rateTable` 真实响应**:`GetUserStatus` 403,从未见过。 - **散射的确切成因**已隔离(台账第十轮),但 RPM 剩余比例与 LRU **各自独立充分**,不存在 单一成因 —— 如果有人再提"修散射",先读那一轮。 ### 2.3 我判断不该做的(留着理由,别当遗漏重新捡起来) - **`clearCallerBindings` 不接线** —— 没有正确的触发事件。断连是**主动错误**的触发点; session reset 这个事件不存在;账号维度的清理不值得加(死绑定单调老化到 LRU 前端、最先被 驱逐,实测存活集与提前回收完全相同)。 - **首轮 thundering-herd 不是缺陷** —— 它是 issue #37 的明确意图(并发突发要散开)。 - **RPM 满即清绑定:显然的修法是净负面** —— 实测保留绑定散射 4/3 个冷账号 vs 清绑定 2, 已 revert。只有抖动短于相邻两轮间隔时保留才划算,而 RPM 窗口 60 秒、agentic 循环隔几秒, 所以受阻 caller 通常连续多轮受阻 —— 正是保留会输的 regime。 --- ## 3. 已知的记账口径问题 ### 3.1 `secret-scan` 的范围 只扫**被跟踪**文件,且 `IGNORED_PREFIXES` 跳过 `test/`。说"secret-scan 干净"要带这个范围。 它有正对照(`test/secret-scan.test.js` 断言植入的 fixture 会以非零退出),所以 EXIT=0 且零 输出是真的,不是坏探针。 ### 3.2 四条有据可查的突变漏网 `lastUsed` 项 / `WAITABLE` 白名单 / backoff 倍数 / `known()` existence 项。**每条的理由写在 对应 spec 的 `name` 字段里**,而不是这里 —— 读那里,别读这里的摘要。 ### 3.3 未 exhaustive 扫描的面 `Ctrl-F` 搜 `仍未 exhaustive 扫描`,**取最后一个命中**。别信任何写在文档里的行号(见 §4.2)。 --- ## 4. 最该记住的几条 台账第十三轮有完整版。这里只留判别动作。 ### 4.1 钳住取值范围不等于钳住语义 v3.9.18 给 digest 上限加了 `Math.min(x, 32000)`。对 `1e9` 有效,对 `0.5` **完全失效** —— `Math.min` 忠实地保留了 0.5,而语义反转发生在下游:消费者是 `slice(-n)`,而 `slice(-0.5)` === `slice(0)` === 整个字符串。实测 `MAX_CHARS=0.5` 送出全部 50000 字符,**正是那道钳要挡的 后果**。v3.9.19 加 `Math.floor` 补上。 **这道钳失败不是因为它写错了,而是因为它对的。** **动作**:给数值旋钮加钳之后,接着问"这个数最终被谁消费,那个消费者对非整数、对 0、对边界值 的行为是什么"。 ### 4.2 文档里写行号 = 写一个会被下一次编辑推走的引用 同一个会话里踩两次: - 台账导航写"实测:第 1150 行",**在写下的瞬间就是错的** —— 我量的是编辑前的文件,而把它推到 1154 的正是同一个 commit 的那处编辑。 - 交接 §6.6 写 `rescueThinkingOnly` 在 119/228/334/559,一天后实际是 119/234/340/565 —— 我加 `Math.floor` 的注释把它们推下去了。 **动作**:要么别写行号,要么在**最终状态**上重测。稳定的指路方式是 `Ctrl-F` 一个字符串。 ### 4.3 突变 anchor 按整行匹配,所以任何包裹式改写都会打断它 一天被打断三次:#241 合并往判据里插 `hasTools`(两条)、我自己的 `Math.floor` 包住表达式 (一条)。第三次发生在我把前两条写进台账**之后**。 **动作**:anchor 锚**表达式**,不锚整行(去掉行首缩进与 `if (`)。表达式比整行耐改写。 例外是那种 replacement 要删掉条件本身的突变,它必须锚含 `if (` 的整行。 改完 `src/` 一定跑一遍完整突变循环 —— **测试套件不跑 spec,门禁全绿看不见 anchor 失配。** ### 4.4 "共 N 处"里的 N 也是需要测量的数字 交接与 merge message 都写了"三处重新 spread(3295/**3437**/3454)",实际**四处**(漏了 3229), 而 3437 这行不存在(是 3435)。结论没错(3229 同样保留 `.tools`),枚举错了。`grep -c` 一次 就能避免。 ### 4.5 一个碰巧正确的未测量数字,比一个错的更难发现 交接曾写"六个 spec 共 60 条:56 CAUGHT + 4 漏网",而其中一个 spec 的 baseline 已过期、 guard 2 在第一条突变之前就中止它 —— 那 60 条里最多跑了 49。修完重跑,分布**恰好就是 56 + 4**。 **数字是对的,它作为"我跑过"的证据是假的。** 而它旁边的数字全都真实,所以它借到了那些数字的 可信度;接手的人按清单跑出红,第一反应是怀疑自己的环境。 **动作**:每个数字要能回答"这一次是谁跑出来的"。**存量数字不能当基数** —— 我曾把 baseline 从"记录里的 71"加"新增的 3"算成 74,真值 79。要一个计数就去测它。 ### 4.6 说"这个 PR/这轮引入了 X"之前,先看邻居有没有同一个 X 我一度把"空串静默关掉 digest"记成 #241 的缺陷,驱动核实后不是:`Number('') === 0` 是那个文件 里**四个旋钮共有的既存约定**。处置因此完全相反 —— 不是改代码,而是改那句说谎的注释。按"PR 缺陷"处理会去动既存部署的行为,为一个不存在的缺陷。 反方向同样成立:**新增旋钮时去读邻居的伤口。** #241 真正的缺陷是新旋钮缺上限,而同一个函数 里的同类旋钮早就因为同一个洞加过钳,教训就写在它的注释里。**新代码不会自动继承邻居学到的。** ### 4.7 一个只有一个可达取值的旋钮,比没有旋钮更糟 `rescueThinkingOnly` 在 #241 之后成了死配置(默认 `true`、两个调用点都传字面 `true`、无任何 测试传 `false`)。已删(`866da63`,未发版 —— 零行为变更)。 它读起来像一个变化点,评审者必须为它推理;而任何冲着它去的突变守卫,守的是到不了线上的代码 —— 正是 v3.9.13 那条 medium 缺陷的形状。 **动作**:加一个 opt-out 参数时,同时写一条传 `false` 的测试。**那条测试才是让它成为旋钮而 不是装饰的东西。** 没有它,下一轮就该把这个参数删掉。 --- ## 5. fan-out 复核这套流程本身的两条 v3.9.19 那轮用了 15 个 lens + 每条发现一个独立对抗验证器(81 条候选 → 40 条唯一 → **35 CONFIRMED / 5 REFUTED**)。两条关于流程的: - **一次 40 个验证器并发会被网关限流。** `pipeline()` 是全部并发:前 16 个返回、17–39 全部 stall 且每个重试 6 次都 stall。改成 4 个一批顺序跑,58 条全部返回。台账第五轮记过"单个 agent 跑太久会丢全部结果",**扇出太宽是同一条的另一个形态**。 - **验证器必须有 `UNVERIFIED` 第三态。** 第一版脚本把"没返回"和"没问题"混进同一个 `filter(Boolean)` —— 那正是 `confirmed: 0` 被当成"通过"的形状。而 synthesizer 最后也 stall 了,裁决却都在 `journal.jsonl` 里:**agent 挂了不等于结果没了,先去 journal 捞。** **5 条 REFUTED 也是产出**:验证器用 `merge-base --is-ancestor` 证明一条不在审查范围内、拿 邻居惯例反驳两条"broken ref"、还有一条指出报告者只读了 native 那一支而漏了第二个无门的发射点 ——**并在反驳过程中发现了另一条未记录的缺陷**。 --- ## 6. 那几个曾经"未发版"的 commit —— 已随 v3.9.20 发出 **这一节现在是历史。** 下面四个 commit 当时判断为"零行为变更所以刻意不打 tag",判断在当时 是对的;它们随 v3.9.20 一起发出去了,因为那一版带了一条真的行为变更(§9.1)。**判据本身 没变** —— 变的是有了一个需要用户去装的东西可以搭车,正是本节末尾预留的那条路。 | 项 | commit | |---|---| | 删掉死配置 `rescueThinkingOnly` + 三条 anchor 改锚表达式 | `866da63` | | 台账六条旧记录补齐 `mergedAt`/`title`;`mergedAt` 口径定在 UTC | `12eaac7` | | 首页 `PR #54 · PR #54` 重复标签(渲染侧去重,不动数据) | `12eaac7` | | 两个 README 说面板"显示同样的信息"→ 如实说它是超集 | `12eaac7` | ### 为什么不打 tag 按仓库自己的先例(v3.9.12 那次的原始判断):`v*` tag 会触发四个产物构建,而这些产物在行为上 与 v3.9.19 **完全相同** —— 没有任何东西需要用户去装。删死配置是零行为变更;首页去重走 GitHub Pages(从 `docs/` 部署,不需要 tag);其余是文档与台账数据。 **所以下一次带真实行为变更的发布顺带带上这几个 commit 即可。** 想单独发也行: `package.json` + `package-lock.json`(**两处**)bump,照 [releases/README.md](releases/README.md) 的发版顺序走 —— 那份此前只有四步且其中一步会被 pre-commit 钩子拒绝,本轮已按实际做法补全。 一个反面判据也记下:v3.9.19 是**该**单独发的 —— 那次 v3.9.18 的 release notes 在讨论一条 落在 tag 之后的修复,notes 描述了一个不在该版本里的东西。改措辞会把用户留在有洞的版本上, 发一版让 notes 变成真的更对。**判据是"有没有东西需要用户去装",不是"改了多少行"。** `mergedAt` 那条值得一句:既有惯例**本身就不一致**(#230 等于 UTC,#232/#233 既不是 UTC 也 不是 JST),而 `contributors.json` 的 schema 从来没写时区。定在 GitHub 的 `merged_at`(UTC) 并写进记忆文件。**没有回改 #232/#233** —— 那两条的来源无法核实,改一个不知出处的记录不如 留着并写明。 --- ## 7. 文档梳理(2026-08-06) 这一轮问的是"文档集该是什么形状",不是"哪句话错了"。测出来的形状:手写文档 6452 行,**交接十份 占 40%、台账占 22% —— 合起来 62%**,而 **55 条可复用的「动作」散在 10 个文件里**(台账 32, 交接 23)。 ### 7.1 最有价值的发现不是"组织得不好" **仓库里唯一那份发版说明(`docs/releases/README.md`),照着做会在第 3 步被自己的 git 钩子拒绝。** 它原本四步,四处都错或危险地不全:只 bump `package.json`(锁文件带**两处** version)、 叫你在 `master` 上 commit(`.githooks/pre-commit` 明确拒绝)、`git tag` 造 lightweight (三个已发 tag 全是 annotated 对象)、**零门禁步骤**。 而完整顺序此前**只散在归档交接里**:`annotated tag` 只在 08-03、`package-lock` 散在三份、 "tag 必须落在 master" **哪里都没有**。已改成八步、每步写清为什么,并补两节("这到底该不该 发版"的判据、notes 的体例)。 **判别动作**:一份"怎么做 X"的文档,要**照着做一遍**才算读过。逐句读它会显得合理 —— 第 3 步读起来完全正常,只有真去执行才会撞上钩子。 ### 7.2 归宿早就存在,而约定不指向它 台账 `## 方法论笔记` 那一节(在逐轮小节**之前**)只有 49 行,**最后更新是 2026-07-30** —— 第三到十三轮全部把教训写进了自己那一轮。容器有,约定不指向它。 这解释了为什么 55 条会散开:**不是没人想收拢,是每轮的自然落点是"当时那个活跃文件"。** 所以光新建一份 playbook 不解决问题 —— 它会变成第 56 个副本。**这条仍未做**,见 §8。 ### 7.3 所以这轮的产出是守卫,不是新文档 `test/docs-consistency-guard.test.js`(13 条)。选它而不是"再写一份文档"的理由: **`scripts/run-test-shard.mjs` 用 `readdirSync` glob `test/*.test.js`,新守卫自动进门禁** —— 没有需要记得的登记步骤。这就是"什么让它活着"的答案:机制,不是纪律。 钉住的四组: - 全部 md 相对链接可解析 - 交接链**恰好一个活头**(横幅含"当前一份是"的是归档);每份归档必须链到活头且链回索引; 索引第一行必须点名活头 —— 这轮我**手工**改了九个横幅,那正是该被机械化的 - 发版说明与它描述的工具**对账**:不得叫人在 master 上 commit、必须提 `package-lock`、 必须 `git tag -a`、必须列出三条门禁命令、必须写明套件不跑突变 spec - **全部突变 anchor 唯一性** —— 把三个绿灯都漏掉的那条不变式搬进了门禁 突变验证 8 条全 CAUGHT,含两种**真实发生过**的 anchor 破坏形态(改 anchor 内部 = #241 干的; 包裹被锚表达式 = 我自己的 `Math.floor` 干的)。 **一条突变漏网是我的突变选错了,不是守卫有洞**:在 anchor 那行**后面**追加注释,anchor 作为 子串仍然匹配 —— 而 `mutate-verify` 用同一套子串逻辑,所以那条 anchor 其实没坏。 *判别动作*:突变漏网时先问"这条突变真的注入了它声称的缺陷吗",再问"守卫有没有洞"。 ### 7.4 第一版守卫两条红,都是守卫自己错 - head 检测把横幅里的**后向**指针("上一份:[X]")也算成"指向当前",于是 8 个文件都像是头 - runbook 那条匹配到了**我自己在勘误块里引述的那句旧错误指令** —— 扫原始 prose 分不清"指令" 与"对指令的引用",改成先剥 `>` 引用块再检查 台账那条"修复本身也需要一轮复核"在守卫上同样成立。 ### 7.5 顺带修掉的 - 本文 §6 曾把 "release-process" 当成一份文档来引用(还用反引号排版成了标识符)—— **仓库里没有这个东西**,是我上一轮留下的悬空引用。 新守卫第二条断言专门钉这个形状(反引号包起来、读着像文件名、而既无文件也无链接) - `CONTRIBUTING.md` 钉着 v3.9.14 的 "3418 / 261",而那之后发了五个版本。**结构修法是不引用 数字、只指路** —— 更新它只是把过期推迟。同时补上"门禁全绿不等于没弄坏东西"(PR #241 那次 三个绿灯都没看见两条 anchor 断了) - notes 体例此前写在索引里,而写 notes 的人会去 `releases/README.md` 找。搬到用它的地方, 索引改成指路 ### 7.6 这轮 workflow 失败了一次,教训值得留 8 个 lens 全部 stall(每个被重试 3–4 次,零 result),而四份 transcript 已有 200–300KB —— **它们做了真实调查然后在产出结构化输出之前挂掉**。成因是 lens 太宽:我给 S1 的任务是 "枚举 55 条 + 聚类 + 提案",一个 turn 装不下。 **没有当成"没发现"**:停掉 workflow,从 transcript 里捞出六条具体线索,其中三条自己核实成立 (含本文那个悬空引用)。§5 那条"agent 挂了不等于结果没了,先去 journal 捞"这轮又用上了一次, 而这次要捞的不是 journal 而是 agent transcript。 **动作**:给 lens 的任务要能在一个 turn 里收敛。要"枚举 + 分析 + 提案"就拆成两阶段,别指望 一个 agent 一次做完。 ## 8. 仍未做:方法论的归宿 55 条动作散在 10 个文件里,而台账的 `## 方法论笔记` 自 2026-07-30 起没被喂过。**这轮没做**, 因为它需要先回答"什么让新文档不变成第 56 个副本",而我只答出了一半: - 已答:**机制而非纪律** —— 守卫自动进门禁那条路走通了(§7.3) - 未答:方法论是**判断**,不是可断言的不变式。没法写一条测试断言"这份 playbook 是最新的" 一个可能的形状:让每轮的"动作"**只写在台账那一节**,逐轮小节里只留指针。但那要改的是写作习惯, 而习惯性约定在这个仓库已经失败过(一份交接对应一个版本 → 一天写了五份)。 **下一轮开工前先决定这一条**,别再往任何文件里追加第 56 条动作。 --- ## 9. 出口 parity 复核(2026-08-06 第二轮) 这一轮问的是"**同一条不变式在四条出口协议上是否都成立**" —— 挑这个面是因为它是本仓库 复发最多的形状(记过 6 次),而台账的方法论笔记说得很直接:被审过的地方是硬的 (鉴权 30+ 次、协议层 fuzz 190+ 次,零缺陷),**没审过的地方才有货**。 四条出口:`chat.js`(OpenAI)· `messages.js`(Anthropic)· `responses.js` · `gemini.js`。 ### 9.1 查出一条真缺陷:Gemini 静默丢掉全部工具参数 `gemini.js` 文件头第 9 行自己写着 "Mirrors src/handlers/messages.js"。而 messages.js 的 **B4** 修复没镜像过来:两处 `catch {}` 把无法解析的 `arguments` 塌成 `{}`。同一份上游 payload 实测: ``` Anthropic 出口 input = {"__raw_arguments":"{\"file_path\": \"/etc/passwd\", …"} + log.warn Gemini 出口 args = {} 零日志 ``` 已修(`aeb18dd`):抽 `parseToolArgs()`,非流式与 `flushToolCalls()` 两个站点都调它, 原串放在 `__raw_arguments`(与 Anthropic 同键名)。**这是第七次。** **判别动作**:一个文件声称"mirrors X",就把 X 里带编号的修复逐条对过去。这条声明本身 是最好的检查清单,而它在这里躺了一个月没人当清单用。 ### 9.2 一条既有测试把缺陷钉成了期望行为 `test/gemini.test.js` 有 `it('survives malformed tool-call argument JSON (args -> {})')`, 断言 `deepEqual(args, {})`。它**真正测的是"不抛异常"**,但把空对象写进期望之后,数据丢失 在后来每个读者眼里都像是刻意设计 —— 我改完代码正是它先红的。 **判别动作**:写"survives / 不崩"这类测试时,断言要**只钉不崩那一条**。把当前的返回值 顺手写进 `deepEqual`,等于把当时的缺陷升级成契约。测试名里带括号解释返回值(`(args -> {})`) 是这个毛病的显式征兆。 ### 9.3 `gemini.js` 此前零突变 spec —— 这就是它没被发现的原因 71 条测试从未被突变验证过。新增 `test/mutations/gemini-egress-parity.json`(7 条,全 CAUGHT)。 仓库突变总数 **67 → 74**,声明漏网 **4 → 4**(新 spec 零漏网)。 **两条关于突变本身的**: 1. **第一次跑出一条漏网,是我的突变锚错了,不是守卫有洞。** 我锚在 `error()` 的早退卫兵 (`if (this.finished) return`)上,而真正的 latch 是它末尾的 `this.finished = true` —— 改卫兵不改 latch,`finish()` 照样早退,**这条突变压根没注入它声称的缺陷**。理由已写进 spec 的 `name` 字段,免得下一轮有人把锚改回去。 2. **我把"日志行被删"记成声明漏网,理由是"断言日志输出需要 harness" —— 不成立。** `test/retry-rescue-budget-split.test.js:198` 从第十一轮起就在替换 `log.warn` 收集输出。 **这和 PR #242 那条 settle-flush survivor 是同一个毛病:"工具还没建"而工具就在隔壁文件里。** 已补两条断言(含一条负对照:合法 payload 零日志,否则断言在"无条件记日志"的版本上也绿)。 ### 9.4 查证但**未修**的三条(留着理由,别当遗漏重新捡起来) - **`mapFinishReason` 的 `|| 'STOP'` 兜底对 `finish_reason:'error'` 会翻成正常结束** —— 正是 special-agent.js 的 "H2" 注释要防的后果。但**不可达**:那条路径先发 error 帧, `error()` 会 latch `finished`,finish chunk 在翻译前就被丢掉。按 §4.7,给到不了的分支加 映射等于加一个评审者必须为之推理的假变化点,所以改成把**让它无害的前提**写进注释 + 用 测试钉住。我一度加了 `error: 'OTHER'` 又撤掉。 *判别动作*:确认一条缺陷"可达"要走完整条链,不能只驱动出口函数 —— 我第一次的探针直接 调 `openAIToGemini`,而生产上那个值到不了它。 - **`buildGetChatMessageRequest` 有四处 `env` 穿线缺口**(`getImageFieldTag()` / `getImageToolDefEnabled()` 无参调用、`expandVisionMessage` 与 `getToolDefTags` 硬写 `process.env`)。**生产行为不受影响**(`process.env` 就是真源),但**零测试传假 env**, 所以它是个陷阱而非活缺陷:哪天有人写一条传假 env 的断言,它会静默拿到真环境 —— 台账记过 四次的坏探针形状,这次在源码侧。未修,因为它需要动 vision 那条路径而收益是纯测试面。 - **`intent-extractor.js:179` 与 `tool-emulation.js:787` 的 `{}` 兜底是合理的** —— 前者从模型文本里刮取、带 confidence 分,后者已有宽松回退路径。B4 这条 parity 缺口 **只在 `gemini.js`**,如实收范围。 ### 9.5 判据翻转,已发 v3.9.20 §6 原本的判断是"零行为变更所以不该打 tag"。**gemini 那条是行为变更**(带工具的 Gemini 客户端,上游给出畸形 arguments 时行为不同),所以判据翻转:按 [releases/README.md](releases/README.md) 的"这到底该不该发版"—— **有东西需要用户去装**。 §6 那四个文档 commit 一并带上了。 ### 9.6 发版失败了一次,而失败的是我这一轮加的守卫 **第一次打的 `v3.9.20` 指向 `3cb8f3a`,Test job 红,六个 job 零产出**(零 release、零镜像、 零二进制)。红的是 §7.3 那个文档守卫里我本轮新增的版本断言,而本地三道门禁全绿。两个原因叠加: 1. **它读 `.git/refs/tags` + `packed-refs`,而 GitHub 的 checkout 是浅克隆,只带正在构建的 那个 tag。** 所以每个更早的 tag 在 CI 里都"不存在"。我为这种情况写了探针检查 (`known.size > 0`),**它没救场**:正在构建的 tag 在场,所以断言从不触发。 **一个只在"完全读不到"时才跳过的探针,挡不住"读到一半"。** 2. **它扫全部 md,而台账是 append-only。** 抓到的是台账第十三轮里一句把 master 等同于 **当时那个 tag** 的记录 —— 那句话**写下时为真**,在一份只追加的历史记录里,过期的旧声明 正是它该有的样子。 *这里刻意不复述那句原话。* 第一版写法照抄了进来,**新守卫立刻抓到本文** —— 这是同一个 毛病在一天内的第四次,而这次是守卫自己在两分钟内拦下的,不是我读出来的。**这就是把纪律 换成机制的全部意义。** 这是同一个毛病的**第三次**:守卫分不清"声明"与"看起来像声明的东西"。第一次是 §7.4 那条 (勘误块里引述的旧指令),第二次是我自己在台账里重贴错误字符串,这次是历史记录里当时为真的声明。 已改(`4f05a71`):**不再读 git**,比对 `package.json`(每个 checkout 都有,且是 `src/version.js` 实际吐出的值);**只查会做当前声明的两个文件**(索引 + 索引第一行指向的活头 交接),归档交接与台账不在范围内。 **动作(这一轮最该带走的一条)**:一条新断言只要读了**仓库元数据**(git 状态、文件系统布局、 环境变量),就必须问"CI 的 checkout 和我的工作副本差在哪",并**在那个条件下跑一次**。 本地跑三遍不等于跑过。复验方式:`git clone --depth 1 --no-tags file://$PWD` 然后在里面跑 —— 零 tag 的浅克隆下 15/15 通过,而旧版在那里必红。 删 tag 重打之前核过三样:零 GitHub Release、零成功 job、GHCR 里没有那个包 —— **所以没人可能 下载到东西**。理由写进了新 tag 的 annotation,免得以后有人看到"tag 被移动过"却查不到原因。 ### 9.7 产物已核(第 8 步,核东西不核 job 状态) | 产物 | 大小 | 判据 | |---|---|---| | `windsurfapi-macos.zip` | **45.54 MB** | 判据"带 x64 约 45MB / 不带约 22MB" → **x64 在里面** | | `windsurfapi.exe` | 58.97 MB | 与 v3.9.19 同量级 | | `windsurfapi-macos-arm64` | 61.68 MB | 同上 | | `windsurfapi-windows.zip` | 22.71 MB | 同上 | 镜像 `ghcr.io/dwgx/windsurf-api` 的 `3.9.20` / `3.9` / `latest` 三个 tag 都 HTTP 200 且 **digest 相同**(`sha256:c209600…`)。日志里 x64 二进制被真实下载(25,026,646 字节)—— #233 修的那条持续有效,这是第六次。 *注意镜像名是 `windsurf-api` 而不是 `windsurfapi`* —— 我第一次查 manifest 用错了名字,拿到 403 差点当成"没推上去"。**403 和 404 在 GHCR 上都可能只是名字写错。** ---