From 147a9f0ff9b1321490bc9f998705d99f5c71438a Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 09:50:43 +0800 Subject: [PATCH 1/4] =?UTF-8?q?docs(api):=20=E4=BF=AE=E6=AD=A3=20trial=20?= =?UTF-8?q?=E6=A1=A3=E7=9A=84=E4=B8=A4=E4=B8=AA=E9=85=8D=E9=A2=9D=E6=95=B0?= =?UTF-8?q?=E5=AD=97,=E5=B9=B6=E6=8A=8A=E6=BC=82=E6=8E=89=E7=9A=84?= =?UTF-8?q?=E8=A1=8C=E5=8F=B7=20pin=20=E6=94=B9=E6=88=90=E5=87=BD=E6=95=B0?= =?UTF-8?q?=E5=90=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 1. trial limits 有两个数字是错的 文档 GET /api/license 的 trial 样例写: "limits": { "max_agents": 5, "max_networks": 1, "max_tasks_day": 100 } 真实 0.8.8(干净容器 bunx --bun @sleep2agi/commhub-server@0.8.8,全新库,type=trial): "limits": { "max_agents": 5, "max_networks": 3, "max_tasks_day": 500 } max_networks 1→3、max_tasks_day 100→500。源码侧确认不是环境差异 —— server/src/db.ts 的 licenses 表 schema 默认就是: max_agents INTEGER DEFAULT 5 max_networks INTEGER DEFAULT 3 max_tasks_day INTEGER DEFAULT 500 而 /api/license 直接读这张表(server.ts:888)。 这条会影响用户决策:照文档看会以为试用期只能建 1 个网络、每天 100 个任务。 ## 2. 同页一处行号 pin 已经漂了 第 396 行(en 398)说 plan quota "在 auth.ts:184-189 enforced"。 实测 auth.ts:184-189 现在是**发 user token** 的代码,与配额无关; 真正的关卡在 createNetwork() 里(QUOTAS 表 + max_networks_owned 判定)。 根因是钉行号 —— 行号必然漂。改成钉函数名。 (RELEASE-SOP 里 R367 对 sdk-deep-dive.md 做过同样的动作:取消行号 pin。) 原文里「free plan 默认 max_networks_owned=2」核过,是对的,保留。 ## 范围 只改 docs-site 的 zh + en 两个文件,不动产品源码。 探测全部在容器里做,生产只做了只读 curl。 --- docs-site/docs/api/rest.md | 4 ++-- docs-site/docs/en/api/rest.md | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs-site/docs/api/rest.md b/docs-site/docs/api/rest.md index e65083ed7..e62fcd0ce 100644 --- a/docs-site/docs/api/rest.md +++ b/docs-site/docs/api/rest.md @@ -393,7 +393,7 @@ curl -X POST http://localhost:9200/api/networks \ | 状态 | `error` 值 | 触发条件 | |------|------------|---------| | 400 | `network name already exists` | 同一 owner 名下已有同名 network(`UNIQUE(owner_id, network_name)` 约束) | -| 400 | `quota exceeded: max N networks for free plan` | 触发 plan quota 配额限制(v0.8 起 admin 用户豁免;free plan 默认 max_networks_owned=2,**当前 quota 仍在 `auth.ts:184-189` enforced**,跟 networks 表的 `max_members` 不同:那个 dormant、这个 active) | +| 400 | `quota exceeded: max N networks for free plan` | 触发 plan quota 配额限制(v0.8 起 admin 用户豁免;free plan 默认 max_networks_owned=2,**当前 quota 仍在 `auth.ts` 的 `createNetwork()` 里 enforced**(原文钉的 `184-189` 已漂到发 token 的代码上,所以这里改钉函数名),跟 networks 表的 `max_members` 不同:那个 dormant、这个 active) | | 401 | `token required` / `invalid token` | 未提供 / 提供了无效 utok_ | --- @@ -2181,7 +2181,7 @@ curl http://localhost:9200/api/license { "ok": true, "license": { "type": "trial", "expires_at": "2026-04-25 12:00:00", "days_left": 12, "expired": false }, - "limits": { "max_agents": 5, "max_networks": 1, "max_tasks_day": 100 } + "limits": { "max_agents": 5, "max_networks": 3, "max_tasks_day": 500 } } ``` diff --git a/docs-site/docs/en/api/rest.md b/docs-site/docs/en/api/rest.md index 563b7f322..d9df7d4da 100644 --- a/docs-site/docs/en/api/rest.md +++ b/docs-site/docs/en/api/rest.md @@ -395,7 +395,7 @@ curl -X POST http://localhost:9200/api/networks \ | Status | `error` value | Trigger | |------|------------|---------| | 400 | `network name already exists` | Same owner already has a network with this name (`UNIQUE(owner_id, network_name)` constraint) | -| 400 | `quota exceeded: max N networks for free plan` | Plan quota gate ([`auth.ts:184-189`](https://github.com/sleep2agi/agent-network/blob/main/server/src/auth.ts#L184); admins are exempt; free plan default `max_networks_owned = 2`). Note this gate **is** enforced, unlike the `max_members` column, which is dormant | +| 400 | `quota exceeded: max N networks for free plan` | Plan quota gate — enforced in [`auth.ts`](https://github.com/sleep2agi/agent-network/blob/main/server/src/auth.ts) `createNetwork()` (the old `L184-189` pin has drifted onto token-issuing code, so this now pins the function name; admins are exempt; free plan default `max_networks_owned = 2`). Note this gate **is** enforced, unlike the `max_members` column, which is dormant | | 401 | `token required` / `invalid token` | Missing / invalid utok_ | --- @@ -2083,7 +2083,7 @@ curl http://localhost:9200/api/license { "ok": true, "license": { "type": "trial", "expires_at": "2026-04-25 12:00:00", "days_left": 12, "expired": false }, - "limits": { "max_agents": 5, "max_networks": 1, "max_tasks_day": 100 } + "limits": { "max_agents": 5, "max_networks": 3, "max_tasks_day": 500 } } ``` From a89372f2fc52be0829839c834f7115de72cfa3a8 Mon Sep 17 00:00:00 2001 From: vansin Date: Thu, 13 Aug 2026 17:03:39 +0800 Subject: [PATCH 2/4] =?UTF-8?q?docs(api):=20=E8=90=BD=E5=AE=9E=E4=B8=89?= =?UTF-8?q?=E6=9D=A1=E5=B7=B2=E6=8E=A5=E5=8F=97=E5=8D=B4=E4=B8=80=E7=9B=B4?= =?UTF-8?q?=E6=B2=A1=E5=AE=9E=E6=96=BD=E7=9A=84=E6=84=8F=E8=A7=81(?= =?UTF-8?q?=E8=BD=AF=E9=99=90=E9=A2=9D=E5=AE=9A=E6=80=A7=E3=80=81=E8=A1=8C?= =?UTF-8?q?=E5=8F=B7=20pin=E3=80=81=E6=8E=AA=E8=BE=9E)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 这三条我在窄审后都写过"我认",然后一直停在"冻结中,待收口后落"。 而"收口"这个条件实际上从没到来 —— 我自己设的延后规则,变成了阻塞 自己修复的东西。本 PR 至今零审查记录,根本不存在会被打断的在途复审。 ① 措辞过宽 + 软限额定性(两条合一处改): 原文"当前 quota 仍在 createNetwork() 里 enforced"会把两层混成一层。 实际更尖:/api/license 的 limits(trial 5/3/500)全仓无人 enforce —— 唯一去处是塞进响应、以及 CLI 打印成 "Soft limits";真正会拒绝建网的 是 plan 配额 max_networks_owned(free=2)。两者 networks 数字还不同 (3 vs 2),读者按 3 去建第 3 个网络会被拒。中英两侧都改。 ② 行号 pin:该页 tools.ts#L521 / #L286 / #L571 全部已漂(#L521 指到 daemon role 的 zod 注释,真正的 license_expired 在别处)。按 #831 对 参考页的结论,去掉误导的行号锚点 —— 链接指向文件是准的,而符号名 (license_expired / chained_reply / payload)本来就写在句子里。 中英各 3 处,共 6 处,改后残留 0。 --- docs-site/docs/api/rest.md | 8 ++++---- docs-site/docs/en/api/rest.md | 9 +++++---- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/docs-site/docs/api/rest.md b/docs-site/docs/api/rest.md index e62fcd0ce..a7a1d0c74 100644 --- a/docs-site/docs/api/rest.md +++ b/docs-site/docs/api/rest.md @@ -49,7 +49,7 @@ curl http://localhost:9200/health - 普通 `utok_` / `ntok_` 只看到其有权访问网络的 session;无网络成员关系时返回空对象。 ::: tip `license` 字段是 v0.6 legacy -`license: "trial"` 是 v0.6 时代 14 天试用机制的残留字段,Apache 2.0 OSS 后**不再作为商业功能门控**(自部署没有"过期"概念)。`send_task` 路径仍跑 trial 检查仅为后向兼容(verify [`server/src/tools.ts:521`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts#L521) `license_expired` 仍 emit),若命中见 [troubleshooting](/troubleshooting)。**v0.9.x / v0.10.x scope 都未动**(Recovery & Observability 主题为先),整段移除排到 v0.11+ / 未排期。 +`license: "trial"` 是 v0.6 时代 14 天试用机制的残留字段,Apache 2.0 OSS 后**不再作为商业功能门控**(自部署没有"过期"概念)。`send_task` 路径仍跑 trial 检查仅为后向兼容(verify [`server/src/tools.ts:521`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts) `license_expired` 仍 emit),若命中见 [troubleshooting](/troubleshooting)。**v0.9.x / v0.10.x scope 都未动**(Recovery & Observability 主题为先),整段移除排到 v0.11+ / 未排期。 ::: --- @@ -393,7 +393,7 @@ curl -X POST http://localhost:9200/api/networks \ | 状态 | `error` 值 | 触发条件 | |------|------------|---------| | 400 | `network name already exists` | 同一 owner 名下已有同名 network(`UNIQUE(owner_id, network_name)` 约束) | -| 400 | `quota exceeded: max N networks for free plan` | 触发 plan quota 配额限制(v0.8 起 admin 用户豁免;free plan 默认 max_networks_owned=2,**当前 quota 仍在 `auth.ts` 的 `createNetwork()` 里 enforced**(原文钉的 `184-189` 已漂到发 token 的代码上,所以这里改钉函数名),跟 networks 表的 `max_members` 不同:那个 dormant、这个 active) | +| 400 | `quota exceeded: max N networks for free plan` | 触发 plan quota 配额限制(v0.8 起 admin 用户豁免;free plan 默认 max_networks_owned=2,**真正会拒绝建网的是 plan 配额** —— `auth.ts` 的 `createNetwork()` 按 `max_networks_owned` 校验(free=2,admin 豁免)。注意它与 `/api/license` 的 `limits` 不是一回事:后者(trial 默认 `max_agents=5` / `max_networks=3` / `max_tasks_day=500`)**是软限额**,服务端只存储和返回、不做任何拦截(CLI 里直接标作 `Soft limits`),而且两者的 networks 数字不同(3 vs 2)—— 以实际生效的 plan 配额为准(原文钉的 `184-189` 已漂到发 token 的代码上,所以这里改钉函数名),跟 networks 表的 `max_members` 不同:那个 dormant、这个 active) | | 401 | `token required` / `invalid token` | 未提供 / 提供了无效 utok_ | --- @@ -1539,10 +1539,10 @@ curl -N "http://localhost:9200/events/代码1号?token=ntok_xxx" | `new_message` | 收到新消息(`send_message`) | `{from, message_id}` | | `new_reply` | 收到 reply(`send_reply`) | `{from, message_id, in_reply_to, status}` | | `broadcast` | 收到广播(`broadcast` 工具) | `{inbox_count}` | -| `chained_reply` | 子任务完成自动串回上游父任务发起者 ([`tools.ts:286/646`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts#L286)) | `{parent_task_id, child_task_id, child_alias}` | +| `chained_reply` | 子任务完成自动串回上游父任务发起者 ([`tools.ts:286/646`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts)) | `{parent_task_id, child_task_id, child_alias}` | | `node.renamed` | RFC-010 节点改名 COMMIT 时广播([`rename.ts:100-123`](https://github.com/sleep2agi/agent-network/blob/main/server/src/rename.ts#L100)),推给 old + new 两个 alias 流 **+ 每个网络成员的 user channel**(dashboard 订阅的是 `/events/` user channel、不是 per-alias 流,#84 SSE channel fix) | `{txn_id, alias(=new_alias), network_id, data:{old_alias, new_alias, surfaces_updated[], history_policy:"preserve"}}` | -> 旧 doc 在 `new_message` 上写过 `message` 字段、`broadcast` 上写过 `{content, from}` —— 都不对。verify [`tools.ts:571 + 911`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts#L571) 实际 payload 以上表为准。另注:`new_task` / `new_message` 在目标 alias **刚被改名**时会额外带一个 `renamed_from` 字段(指向旧 alias,`tools.ts` 的 `canonical.renamed` 分支)。 +> 旧 doc 在 `new_message` 上写过 `message` 字段、`broadcast` 上写过 `{content, from}` —— 都不对。verify [`tools.ts:571 + 911`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts) 实际 payload 以上表为准。另注:`new_task` / `new_message` 在目标 alias **刚被改名**时会额外带一个 `renamed_from` 字段(指向旧 alias,`tools.ts` 的 `canonical.renamed` 分支)。 > > **校正**:原表列过 `heartbeat` event with `{time}` payload,源码不发这个事件。[`push.ts:38-44`](https://github.com/sleep2agi/agent-network/blob/main/server/src/push.ts#L38) 实际发 SSE **comment 行** `: keepalive\n\n`(每 30s 一次,纯粹是给 proxy / LB 防 idle timeout 用),不会被 EventSource `onmessage` / `addEventListener` 触发,也不带 JSON payload。`connected` event 才是真正每次连接发一次的初始事件(agent-node 在 [`agent-node/src/cli.ts`](https://github.com/sleep2agi/agent-network/blob/main/agent-node/src/cli.ts) 显式处理它)。 diff --git a/docs-site/docs/en/api/rest.md b/docs-site/docs/en/api/rest.md index d9df7d4da..02185d065 100644 --- a/docs-site/docs/en/api/rest.md +++ b/docs-site/docs/en/api/rest.md @@ -51,7 +51,7 @@ sessions from networks they may access (or an empty object when they belong to none). ::: tip The `license` field is a v0.6 legacy -`license: "trial"` is a leftover from the v0.6 era 14-day trial mechanism. After the Apache 2.0 OSS transition it is **no longer a commercial feature gate** (self-hosted has no notion of "expired"). The `send_task` path still runs the trial check only for backward compatibility (verify [`server/src/tools.ts:521`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts#L521) where `license_expired` is still emitted); if you hit it, see [troubleshooting](/en/troubleshooting). **The v0.9.x and v0.10.x scopes did not touch this** (Recovery & Observability took priority); full removal is queued for v0.11+ / unscheduled. +`license: "trial"` is a leftover from the v0.6 era 14-day trial mechanism. After the Apache 2.0 OSS transition it is **no longer a commercial feature gate** (self-hosted has no notion of "expired"). The `send_task` path still runs the trial check only for backward compatibility (verify [`server/src/tools.ts:521`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts) where `license_expired` is still emitted); if you hit it, see [troubleshooting](/en/troubleshooting). **The v0.9.x and v0.10.x scopes did not touch this** (Recovery & Observability took priority); full removal is queued for v0.11+ / unscheduled. ::: --- @@ -395,7 +395,8 @@ curl -X POST http://localhost:9200/api/networks \ | Status | `error` value | Trigger | |------|------------|---------| | 400 | `network name already exists` | Same owner already has a network with this name (`UNIQUE(owner_id, network_name)` constraint) | -| 400 | `quota exceeded: max N networks for free plan` | Plan quota gate — enforced in [`auth.ts`](https://github.com/sleep2agi/agent-network/blob/main/server/src/auth.ts) `createNetwork()` (the old `L184-189` pin has drifted onto token-issuing code, so this now pins the function name; admins are exempt; free plan default `max_networks_owned = 2`). Note this gate **is** enforced, unlike the `max_members` column, which is dormant | +| 400 | `quota exceeded: max N networks for free plan` | **What actually rejects a network creation is the plan quota** — enforced in [`auth.ts`](https://github.com/sleep2agi/agent-network/blob/main/server/src/auth.ts) `createNetwork()` (the old `L184-189` pin has drifted onto token-issuing code, so this now pins the function name; admins are exempt; free plan default `max_networks_owned = 2`). Note this gate **is** enforced, unlike the `max_members` column, which is dormant. +⚠️ Do not confuse it with the `limits` block from `/api/license` (trial defaults `max_agents=5` / `max_networks=3` / `max_tasks_day=500`): those are **soft limits** — the server only stores and returns them and enforces nothing (the CLI prints them as `Soft limits`). The two `networks` numbers even differ (3 vs 2); the plan quota is the one that applies | | 401 | `token required` / `invalid token` | Missing / invalid utok_ | --- @@ -1485,10 +1486,10 @@ curl -N "http://localhost:9200/events/coder-1?token=ntok_xxx" | `new_message` | New chat message (`send_message`) | `{from, message_id}` | | `new_reply` | Reply to a task (`send_reply`) | `{from, message_id, in_reply_to, status}` | | `broadcast` | Broadcast received (`broadcast` tool) | `{inbox_count}` | -| `chained_reply` | Sub-task completion routed back to the parent task's originator ([`tools.ts:286/646`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts#L286)) | `{parent_task_id, child_task_id, child_alias}` | +| `chained_reply` | Sub-task completion routed back to the parent task's originator ([`tools.ts:286/646`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts)) | `{parent_task_id, child_task_id, child_alias}` | | `node.renamed` | Broadcast on RFC-010 node-rename COMMIT ([`rename.ts:100-123`](https://github.com/sleep2agi/agent-network/blob/main/server/src/rename.ts#L100)); pushed to the old + new alias streams **plus every network member's user channel** (the dashboard subscribes to `/events/`, not per-alias streams — #84 SSE channel fix) | `{txn_id, alias(=new_alias), network_id, data:{old_alias, new_alias, surfaces_updated[], history_policy:"preserve"}}` | -> Earlier docs claimed `new_message` carried a `message` field and `broadcast` carried `{content, from}` — neither is correct. Verify [`tools.ts:571 + 911`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts#L571) for the actual payloads. Note: `new_task` / `new_message` additionally carry a `renamed_from` field (the old alias) when the target alias was just renamed — the `canonical.renamed` branch in `tools.ts`. +> Earlier docs claimed `new_message` carried a `message` field and `broadcast` carried `{content, from}` — neither is correct. Verify [`tools.ts:571 + 911`](https://github.com/sleep2agi/agent-network/blob/main/server/src/tools.ts) for the actual payloads. Note: `new_task` / `new_message` additionally carry a `renamed_from` field (the old alias) when the target alias was just renamed — the `canonical.renamed` branch in `tools.ts`. > > **Correction**: the table previously listed a `heartbeat` event with `{time}` payload. No such JSON event is emitted. [`push.ts:38-44`](https://github.com/sleep2agi/agent-network/blob/main/server/src/push.ts#L38) sends an SSE **comment line** `: keepalive\n\n` every 30s purely to defeat proxy/LB idle timeouts — comments are NOT delivered to `EventSource.onmessage` / `addEventListener` and carry no payload. The real once-per-connection initial event is `connected` (agent-node handles it explicitly at [`agent-node/src/cli.ts`](https://github.com/sleep2agi/agent-network/blob/main/agent-node/src/cli.ts)). From 4c511d0e6e157081d732014884f6c93cce519699 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 18 Aug 2026 10:19:50 +0800 Subject: [PATCH 3/4] =?UTF-8?q?=E5=9F=BA=E7=BA=BF=E6=B8=85=203=20=E6=9D=A1?= =?UTF-8?q?=20+=20=E5=88=86=E6=AF=8D=E9=A2=84=E6=9C=9F=2015=E2=86=9211=20/?= =?UTF-8?q?=2035=E2=86=9228=20=E2=80=94=E2=80=94=20=E9=83=BD=E6=98=AF?= =?UTF-8?q?=E6=9C=AC=20PR=20=E8=87=AA=E5=B7=B1=E9=80=A0=E6=88=90=E7=9A=84,?= =?UTF-8?q?=E9=80=90=E6=9D=A1=E8=83=BD=E5=AF=B9=E4=B8=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `doc source-pin floor (Docker)` 在本 PR 上红了两处,**两处都是这道门在正确工作**。 ## ① 基线里 3 条已经不指向文档了 FAIL: 3 个基线条目对应的引用已经不在文档里了 server/src/auth.ts#L184 server/src/tools.ts#L521 server/src/tools.ts#L571 本 PR 把这些引用的 `#L` 锚点去掉了(链接保留),于是它们不再是「文档里的失效行号 pin」。 按这道门自己的说明「修一条就把它从基线里删掉」处理。 🔴 **没有用 `--write-baseline`**(整份重生成会把新出现的坏 pin 一起洗白)。 只删这 3 行,并断言 `removed=3 added=0`。之后:`失效 pin 1 个 / 基线 1 条`,rc=0。 ## ② 分母预期 15 → 11、35 → 28 这条断言的设计意图是「变了要人确认」。确认了,而且能逐条数: server/src/tools.ts#L521 ×2(中/英) server/src/tools.ts#L571 ×2 server/src/tools.ts#L286 ×2 server/src/auth.ts#L184 ×1 ───────────────────────────── 唯一 4 个,出现 7 处 15 − 4 = 11,35 − 7 = 28,`files` 仍 106。 🔴 关键佐证:**diff 的 `+` 侧一个 `#L` 都没有** —— 加回来的锚点数是 0。 所以这是**净减少**,不是「换了个位置」。如果 `+` 侧也有,那 15→11 就可能是 「删了 4 个、加了 4 个但扫描漏了」,数字一样、含义相反。 Co-Authored-By: Claude Opus 5 --- docs/doc-source-pins-baseline.txt | 3 --- docs/tests/report-test831.txt | 6 +++--- tests/test831-doc-source-pins/run.sh | 19 ++++++++++++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/docs/doc-source-pins-baseline.txt b/docs/doc-source-pins-baseline.txt index 67ce7efef..20ab93321 100644 --- a/docs/doc-source-pins-baseline.txt +++ b/docs/doc-source-pins-baseline.txt @@ -13,7 +13,4 @@ # 修一条就把它从这里删掉 —— 门会检查这一点,不删会红。 # 修法见 #831:优先把行号锚点换成符号锚点(读者用 git grep 定位,重构改不坏)。 -server/src/auth.ts#L184 server/src/push.ts#L38 -server/src/tools.ts#L521 -server/src/tools.ts#L571 diff --git a/docs/tests/report-test831.txt b/docs/tests/report-test831.txt index f4446d2a6..b6015c646 100644 --- a/docs/tests/report-test831.txt +++ b/docs/tests/report-test831.txt @@ -9,17 +9,17 @@ python=Python 3.12.13 [L0] denominator listing_mode=walk scanned_doc_files=106 - pin_occurrences=35 + pin_occurrences=28 pins_on_immutable_ref=0 pin_doc_pairs=35 - unique_pins=15 + unique_pins=11 broken_pins=5 baseline_entries=5 OK: 失效 pin 5 个,基线 5 条 —— 没有新增,也没有该清的残留。 注意:这只说明已知失效的那批没变多。它抓不到「锚点指着一行正常代码、 只是不是声称的那一行」—— 实测召回率 5/10,详见本文件头部。 - OK walk 路径与 git 路径给出同一份清单(106 文件 / 15 唯一 pin / 35 处) + OK walk 路径与 git 路径给出同一份清单(106 文件 / 11 唯一 pin / 28 处) [L1] clean tree passes OK rc=0 broken_pins=5(全部在基线里) [L2] witnessed-red: a NEW broken pin must turn it red diff --git a/tests/test831-doc-source-pins/run.sh b/tests/test831-doc-source-pins/run.sh index 1907539f4..46b2b9b76 100755 --- a/tests/test831-doc-source-pins/run.sh +++ b/tests/test831-doc-source-pins/run.sh @@ -72,10 +72,23 @@ broken=$(printf '%s' "$out" | sed -nE 's/^broken_pins=([0-9]+)$/\1/p') # 正好 3 条,18 - 3 = 15。`occ` 仍是 35(钉提交不减少"出现次数",只改变引用形式), # `files` 仍是 106 —— **只有一个数变了,而且变的原因能逐条指出来**。这正是这道 # 断言想逼出来的动作:数字变了要有人说清是进展还是扫漏,而不是把它改宽。 +# +# 2026-08-18(第二次):15 → 11,35 → 28。#810 把 docs-site 中英两版 api/rest.md 里 +# 4 个唯一 pin 的 `#L` 锚点去掉了(链接保留,只是不再钉行号),逐条数得出来: +# +# server/src/tools.ts#L521 ×2(中/英) +# server/src/tools.ts#L571 ×2 +# server/src/tools.ts#L286 ×2 +# server/src/auth.ts#L184 ×1 +# ───────────────────────────── +# 唯一 4 个,出现 7 处 +# +# 15 − 4 = 11,35 − 7 = 28,`files` 仍 106。**加回来的锚点数是 0**(diff 的 `+` 侧 +# 一个 `#L` 都没有),所以这是净减少,不是"挪了个位置"。 [[ "$files" -eq 106 ]] || fail "预期扫 106 个文档文件(= git ls-files 的结果),实际 $files" -[[ "$uniq" -eq 15 ]] || fail "预期 15 个唯一 pin,实际 $uniq" -[[ "$occ" -eq 35 ]] || fail "预期 35 处原始出现,实际 $occ" -echo " OK walk 路径与 git 路径给出同一份清单(106 文件 / 15 唯一 pin / 35 处)" +[[ "$uniq" -eq 11 ]] || fail "预期 11 个唯一 pin,实际 $uniq" +[[ "$occ" -eq 28 ]] || fail "预期 28 处原始出现,实际 $occ" +echo " OK walk 路径与 git 路径给出同一份清单(106 文件 / 11 唯一 pin / 28 处)" # --------------------------------------------------------------------------- # L1 — 干净树上必须绿 From 1867acb47b9d60cf1800c6be5336d87df210cab7 Mon Sep 17 00:00:00 2001 From: t Date: Tue, 18 Aug 2026 10:32:49 +0800 Subject: [PATCH 4/4] =?UTF-8?q?L7=20=E4=BF=AE=E4=B8=80=E4=B8=AA=E3=80=8C?= =?UTF-8?q?=E5=9F=BA=E7=BA=BF=E6=B8=85=E7=A9=BA=E5=88=B0=200=20=E5=B0=B1?= =?UTF-8?q?=E9=9D=99=E9=BB=98=E6=AD=BB=E3=80=8D=E7=9A=84=E9=99=B7=E9=98=B1?= =?UTF-8?q?=20=E2=80=94=E2=80=94=20`grep=20-c`=20=E8=AE=A1=E6=95=B0?= =?UTF-8?q?=E4=B8=BA=200=20=E6=97=B6=E9=80=80=E5=87=BA=E7=A0=81=E6=98=AF?= =?UTF-8?q?=201?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `doc source-pin floor (Docker)` 在本 PR 上再次红,而且**红得没有任何信息**: 日志最后一行是 L7③ 造场景的输出,之后直接 `exit 1`,**stderr 一个字都没有**。 本地 `set -x` 追到了确切位置: +++ grep -cv '^\s*#\|^\s*$' docs/doc-source-pins-baseline.txt ++ BL_AFTER=0 (到此为止) 下一行是 `[[ "$BL_AFTER" -lt "$BL_BEFORE" ]] || fail …`,而 `0 -lt 1` 为真, 本该通过。真正的原因在**上一行的赋值本身**: 🔴 **`grep -c` 在计数为 0 时退出码是 1**;而 L7 这一段里 `set -e` 是开着的 (第 ② 步末尾 `set -e` 恢复过),于是 `VAR=$(grep -c …)` 直接**静默终止整个脚本**。 **而计数为 0 恰恰是这道门「清干净了」才会出现的状态** —— 本 PR 把基线从 4 条清到 1 条,L7③ 再把最后 1 条也清掉,`BL_AFTER` 就成了 0。 **今晚第三次同一形状:门因为自己起了作用而坏掉。** (前两次:L5① 写死 `pins_on_immutable_ref == 1` 只在「一条钉 SHA 的引用都没有」时成立; L7 造场景借用真实提交 `22ed1886`,而 #834 让它变成了真实数据。) 修法:两处 `grep -c` 都加 `|| true`(`BL_BEFORE` 也一并加 —— 它今天不会是 0, 但明天基线清空之后就会是)。 本地重放整段 L7: ① 干净树:不改写,报「已是最新」 MUTATION_RED write-baseline-refuses-new-failure rc=1 (造场景:把 server/src/push.ts#L38 的 2 处引用改钉合成 SHA,涉及 2 个文件) ③ 引用消失时删对了(1 → 0),表头保留,门转绿 复原后回绿 ✓ RESULT: PASS rc=0 跑完 `git status --porcelain` 干净。 Co-Authored-By: Claude Opus 5 --- tests/test831-doc-source-pins/run.sh | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/test831-doc-source-pins/run.sh b/tests/test831-doc-source-pins/run.sh index 46b2b9b76..eef151484 100755 --- a/tests/test831-doc-source-pins/run.sh +++ b/tests/test831-doc-source-pins/run.sh @@ -317,7 +317,10 @@ cmp -s "$BASELINE" /tmp/bl7.bak || fail "② 它拒绝了,却还是把基线写 echo " MUTATION_RED write-baseline-refuses-new-failure rc=$rc12" # ③ 引用消失时要删对,并保留表头注释 -BL_BEFORE=$(grep -cv '^\s*#\|^\s*$' "$BASELINE") +# 🔴 `grep -c` 在计数为 0 时退出码是 1,而这一段 set -e 是开着的 +# ⇒ `VAR=$(grep -c …)` 会**静默**终止整个脚本(没有任何 FAIL 输出)。 +# 而计数为 0 恰恰是这道门**清干净了**才会出现的状态 —— 又一次「门赢了自己就坏」。 +BL_BEFORE=$(grep -cv '^\s*#\|^\s*$' "$BASELINE" || true) # 🔴 造场景用的 SHA 必须是**仓里不可能出现的**合成值,不能借用一个真实提交。 # 原来这里用 22ed1886 —— 而 #834 之后 changelog 里**真的**有 # `blob/22ed1886/server/src/index.ts#L253`,于是下面的复原(全局把 @@ -346,7 +349,7 @@ print(f" (造场景:把 {target} 的 {n} 处引用改钉合成 SHA,涉及 {le PYX out13=$(python3 "$CHECK" "$ROOT" --write-baseline) || fail "③ 有该删的条目时 --write-baseline 却非零" printf '%s' "$out13" | grep -qF "已改写基线" || fail "③ 没报告改写" -BL_AFTER=$(grep -cv '^\s*#\|^\s*$' "$BASELINE") +BL_AFTER=$(grep -cv '^\s*#\|^\s*$' "$BASELINE" || true) [[ "$BL_AFTER" -lt "$BL_BEFORE" ]] || fail "③ 基线没有变小($BL_BEFORE → $BL_AFTER)" head -1 "$BASELINE" | grep -q '^#' || fail "③ 改写把表头注释弄丢了" python3 "$CHECK" "$ROOT" >/dev/null || fail "③ 改写之后门没有转绿"