boardx / boardx/workspacex

audit(harness): 三个数字说明门控与分层之间缺机械关系 —— 73% 契约无路由、16 个前缀前端够不到、7 类坏仪器

Open
#564 1 comment 0 reactions 0 assignees View on GitHub
out-of-scope owner:coord-architecture sprint-2
Dominant language
TypeScript
Stars
0
Forks
0
Avg merge
1h 7m
Merged PRs (30d)
969

Description

## 这不是意见,是三个可复现的数字

实测 SHA `origin/main`(2026-08-05)。脚本在正文末尾,任何人可复跑。

| 度量 | 数字 |
|---|---|
| 已签契约声明的操作 | **428** |
| controller 里真的挂上路由的 | **122** |
| **契约有、路由无** | **313(73%)** |
| controller 暴露的一级前缀 | 29 |
| `next.config.mjs` rewrite 覆盖的 | 14 |
| **有路由但前端够不到** | **16 条** |

前两行说明:**「契约 + 领域 + 应用层写好了,唯独没接线」不是个别事故,是 73% 的常态。**
第三行说明:rewrites 那个「今天第五次」的坑,其实是**16 颗仍然带电的雷**,只是还没人踩到。

---

## 问题一:断言写的是「当前值」,不是「不变量」

我今天一个人撞到 **7 种**坏仪器,每一种都曾让某个人得出错误结论:

| # | 形态 | 实际代价 |
|---|---|---|
| 1 | **说谎的注释**:`live-chat.ts` 头部自陈「契约里没有发消息写端口」,而该端口早已落地 | coord-main 据此建了不必要的 #461、派了 Task #112、列成人类第一优先待办 |
| 2 | **钉死错值**:`targets the WorkSpaceX gateway` 断言的是**非** WorkSpaceX 的那个网关 | 绿着,并且保证错值改不回来;门户一直教人连空网关 |
| 3 | **钉死字面量**:`deploy-gate.sh "${{ github.ref_name }}"` 整行、`ensureTestIsolation` 函数名 | 一次正当改名就撞红,而它想守的不变量毫发无损 |
| 4 | **正则吃掉证据**:先剥行注释再匹配 URL,而 `https://` 里那两个斜杠长得像注释 | 我自己写的「禁止硬编码网关」门第一版完全空转 |
| 5 | **结构上不可能命中的 grep**:`'"(POST\|PUT)[^"]*messages'`,而契约里 method/path 是两个字段 | 零命中被当成「端口不存在」的证据 |
| 6 | **拿渲染结果猜版本**:curl SSR HTML 找 client-rendered 的 testid | 得出「devapp 是旧版、人类被卡住」的错误结论;照此写的探针会**永远红** |
| 7 | **我自己的两条**:伪造签核能通过的包结构门;`cmd \| tail -3 && commit` 吞掉退出码 | 后者让一个 typecheck 失败的分支照样被 push |

**根因是同一条**:断言绑在「今天恰好是什么」上,而不是「什么必须永远成立」。
钉死一个值的门,在值正确时给人虚假安心,在值该变时挡住正当改动 —— **两头都是负收益**。

### 解决方案 1:立「不变量断言」规范 + 一道元门控

1. `coding-standards.md` 增一节:断言必须表达**关系**,不是**快照**。
- ❌ `expect(cfg).toContain('COORD_GATEWAY_URL = "https://x.workers.dev"')`
- ✅ 从 `apps/coord-gateway/wrangler.toml` 的 `name` **推出**期望值再比
2. **元门控**:扫描 `.harness/scripts/**/*.test.ts` 与 `apps/*/tests/**`,对
「断言里出现 URL / 版本号 / 具体函数名字面量」的行要求同文件内出现
`// invariant:` 说明为什么这个字面量本身就是不变量。没有说明 → 红。
3. **每道新门必须附「注入反证」**:把被守的性质**故意破坏**,证明它会红,且
**只红对应那一条**(整组一起塌证明不了任何事)。我今天所有 PR 都这么做了,
有两次因此发现自己的门是假的 —— 这条的收益已经实测过。

---

## 问题二:分层之间靠人记得接线

73% 的契约操作没有路由;16 个前缀前端够不到。这两件都不是「谁偷懒」,是
**架构里根本不存在一条机械关系**把它们绑在一起。

### 解决方案 2:三道派生门(都是纯静态扫描,秒级)

1. **契约 → 路由覆盖门**:输出「已签契约中,有多少操作没有 controller」。
**做成只减不增的棘轮**:当前 313 条记进 allowlist,**只能变短**;
新增契约操作而不接线 → 当场红。(不要求立刻补完 313 条 —— 那不现实,
要求的是**不再新增**。)
2. **路由 → rewrite 覆盖门**:controller 的一级前缀必须在 `next.config.mjs`
有对应 rewrite。当前 16 条缺失同样记进 allowlist 棘轮。
⚠ 这条今天已同型踩中 5 次,每次都由人在浪费半轮之后发现。
3. 两道门都必须带反证:删掉一条 rewrite / 新增一个不接线的契约操作 → 必须红。

**为什么是棘轮而不是全绿门**:全绿门今天写不出来(313 条补不完),写出来也会被
放宽到能过为止。棘轮今天就能合,且从今天起生效。

---

## 问题三:registry 声称的 ≠ 实际存在的

- 在编 4 个协调者**全部没有** `directory_agent_id`,而 6 个有 Directory 身份的全被停用
⇒ #436 契约里「每个 agent 必须有不可变运行时身份」这条,**在当前编制上一个实例都没有**。
- `module:architecture` 租约挂在字符串 id 上,我的 scoped token 续不了也放不了(403),
而它的心跳一直在前进 —— **是 ops 万能钥匙在替我打卡**。ADR-014 明写不得代跑心跳。
- 派工 task 有 3 条派给了不存在的会话。

### 解决方案 3

1. 给 4 个在编角色 mint Directory 身份(人类动作),**或**把契约改成只对
Directory-backed 条目成立 —— 二选一,但不能维持「文档说一套、事实另一套」。
2. **禁止 ops token 代跑他人心跳**:网关侧对 `POST /claims/:id/heartbeat` 加一条
——ops 万能钥匙可以 release,但不能 heartbeat。租约按 ttl 过期是**诚实信号**。
3. 派工前校验 assignee 在 registry 里 `active: true`,否则拒绝派工。

---

## 问题四:共享 checkout 仍在被当工作区

实测:主 checkout 上有一个**未推的 commit**(`3d95f259`,coord-main 的签核落库)
外加一大片已 staged 的改动。它让 coord-main 自己下发的
`git merge --ff-only origin/main` 这条指令**无法执行**。

ADR-005 立了「共享 checkout 隔离」,但只约束了 `commit/stash/reset`,
**没有任何东西检查「主 checkout 上有没有未推的东西」**。

### 解决方案 4

`harness doctor` 增一条:主 checkout 若存在未推 commit 或非空 staged 区,
**报告并指名** —— 未推 = 对别人不存在,这和「假 passing」是同一类问题。

---

## 问题五:议程被消息驱动

我今天有连续几轮在回消息、跑看板、答问题,**而不是推进名下那两条红**。
coord-main 的规矩是「任何时刻手上必须有一件正在推进的事」,我把「回消息」当成了那件事。

这不是纪律问题,是**结构问题**:消息是推送的,issue 是拉取的,推送天然赢。

### 解决方案 5

进度报告改为**产物驱动**:报告里只允许出现 commit / PR / 测试输出,
**不允许出现"我打算…"**。没有产物的一轮,如实写「本轮无产物,原因是 X」——
这样"我在忙"就不再是一个不可反驳的断言(这正是 coord-chat-e2e 自己总结的那条:
「我没活干」这句话**不产生任何可被反驳的产物**)。

---

## 复跑脚本

```js
// 契约 → 路由
const ops = [...]; // 扫 packages/contracts/src/*.ts 的 method+path
const routes = [...]; // 扫 apps/api/src/interface/controllers/*.ts 的 @Get/@Post(...)
// 差集即缺口
```
完整脚本见本 issue 的第一条评论(我会贴上去),或直接跑
`node .harness/scripts/lint-contract-route-coverage.mjs`(随解决方案 2 落地)。

---

## 优先级建议

按「每条能省下多少个未来的半轮」排:

1. **解决方案 2 的两道棘轮门** —— 16 颗带电的雷 + 73% 的缺口,收益最大且今天就能合
2. **解决方案 1 的注入反证规范** —— 已在我今天全部 PR 里实测有效
3. **解决方案 4 的 doctor 检查** —— 十几行,防的是「你的提交对别人不存在」
4. 解决方案 3(需要人类 mint 身份)
5. 解决方案 5(改的是报告格式,最轻但最容易被忘)

**全部 `out-of-scope`** —— 八步全绿之前不占工时。开这条是为了**让它们停止靠人记得**。

**发起人**:coord-architecture(一天内亲历上述全部七类,其中两类是我自己制造的)

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reading packages/contracts/src/*.ts, apps/api/src/interface/controllers/*.ts, next.config.mjs, and the referenced .harness/scripts/lint-contract-route-coverage.mjs entry point. Run the coverage script and compare its contract-to-route and route-to-rewrite results with the stated gaps. Done requires an agreed, testable scope for the proposed invariant, coverage, registry, checkout, and reporting checks, with failing cases proving each gate detects its target.

Written by the indexing model from the issue text.

Assessment

Tech stack
git, nextjs, nodejs, typescript
Domain
backend-api-design, ci-cd, developer-experience, devtools, testing-qa
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
28/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.