Commit ab43ffce5d96d26bf636f99b80c6df69be78348a
1 parent
14d83bc2
feat(goals): dispatch 拒绝未经人工审阅的 CEO 自主立项草稿
51 号文档阶段 A 上线后发现:CEO 心跳自主立项建出的草稿(split:false)其实自动带 1 条 draft task,dispatch 技术上立刻能跑,原以为的"零 task 闸门"并不存在——安全性曾经完全靠 ceo/tools.md 里"不许自己调 dispatch"这一句提示词约束,没有代码层面的东西挡着。 POST /goals/:goalId/dispatch 新增判断:created_by==='ceo' 且 updated_at===created_at (自建成后从没被 PATCH 过)直接 409。复用既有时间戳字段代理"有没有人碰过",不加新列—— 已知简化(ponytail 注释标了天花板:分不清"真审过"和"随手碰一下",精确区分需要专门的 reviewed_at 列,留给阶段 B)。 task-queue-routes.test.js 新增两条用例:未审阅的 ceo 草稿 dispatch 409、PATCH 一次后放行; human/未填 created_by 的草稿不受影响。相关四个测试文件 84 条用例 + 全量 unit 套件跑过, 无回归(4 个不相关的预存在失败:工作区不干净的环境闸门 + 两个与本次改动无关的仓库卫生检查)。 51 号文档 §四/七/八、docs/README.md 索引同步更新。 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AWsH2YBwBWTsQggPaPbsKJ
Showing
5 changed files
with
97 additions
and
8 deletions
docs/README.md
| ... | ... | @@ -70,7 +70,7 @@ Graph Agent 架构重构(27 系列)。**已完成的阶段文档已移入 `d |
| 70 | 70 | | [36-prd-agent-methodology-training-feasibility.md](./todo/system-design/36-prd-agent-methodology-training-feasibility.md) | 📝 **调研 + 阶段一/二均已完成**。回应"落地页太单薄是不是该用 Innovator 先训练专业 agent、再用 Dream 持续迭代"——认同方向。**阶段一**:`graph-engine.js` 结算节点后把 `success`/`fail:<reason>` 写进本体 `memory.md`(此前只有人类对话路径会写,Dream 天天空转)。**阶段二**:新建独立试验节点 `prd-methodology-trial` 走真实 Hunt→Digest→Evolve,2 轮收敛(96/97 分,真实基线 50 分),三个真实历史项目反推验证方法论有效;过程中连带修复同一个 2026-08-16 改名批次遗留的 5 处 `innovator`→`org-innovator` 死链(后端两处硬编码、前端"重新训练"按钮、`/api/agent/list` 展示 id 来源的四个 `config.json`),以及 `AgentScaffold` 默认模板一处裸相对路径缺陷。结论:FDE 对方法论类角色确实管用,但收敛速度/验收严谨度依赖任务书显式写明的自纠正要求,非免人工黑盒。环节 3(训练产物 → 雇佣本体转换)尚未开工 | |
| 71 | 71 | | [49-antigravity-inspired-parallel-verification-proposal.md](./todo/system-design/49-antigravity-inspired-parallel-verification-proposal.md) | 📝 **设计讨论,不代表已拍板**(2026-09-04)。对照 Google Antigravity「Teamwork」多智能体框架博客文章,给 27 号 P4「真并行」与 34/35 号「验证严谨度不稳定」两个议题出的方案草稿。**先纠正一个前提**:重读 27-P4 全文后发现阶段 A/B/C 早已全部实现(又一次「文档落后于代码」),当时判断真并行只剩 T2/role 列/`advanceTree` 兄弟子 run 并发三件小事——2026-09-08 收口时又发现这个判断本身有两处误判(role 列其实早就落库、T1 的✅遗漏了 scope 参数的生产接线),三件小事(连同两处误判一起)已全部核实/补齐,见 [27-P4](./done/system-design/27-p4-parallel-and-artifacts.md)。**新提案 A**:借鉴长证明模式的「竞争候选+对抗批评+综合」,区别于现有 `executor-fanout.json` 的分解型 fan-out——需要先有真实场景验证价值,不建议凭空立项,详细设计见 [50 号文档](./todo/system-design/50-competing-candidates-critic-synthesis-design.md)。**新提案 B**:把 35 号提案 D「浏览器关键词命中才要证据」泛化成 benchmark 段全维度默认要求执行级证据(**唯一建议现在推进的部分**);实地核实过把验收路径引导到 `test_script` 段的工作量后**结论是不做**——撞上 `test_script.path` 解析根不含 `org-coder` 实际写代码的项目根这个结构性冲突,改造面远超预期;新增「陷阱注册表」,把 validator 判卷失败的结构化原因喂回 Dream 机制,落点已定案为独立文件 `known_pitfalls.md`(不复用 `memory.md`,避免语义混淆)。建议推进顺序:陷阱注册表最低成本先做,证据泛化次之,竞争候选需要真实场景才立项 | |
| 72 | 72 | | [50-competing-candidates-critic-synthesis-design.md](./todo/system-design/50-competing-candidates-critic-synthesis-design.md) | 📝 **设计草稿,不代表已拍板**(2026-09-04)。49 号文档提案 A 的展开:竞争候选骨架 JSON 形状草稿(`candidate-council.json`,未定案)、命名(按 27-NAMING 节点带前缀/本体不带前缀两轴)、`workgraph-planning` 选型判据方向。**关键设计约束**:critic(评判)与 synthesis(择优/融合)必须拆成两个节点,依据是仓库真实事故——`qa-judge` 早期"评判+终态判决合一"设计导致过一次无限循环(20 次调用/31 分钟才熔断,记录在 `deliverable.json` 的反模式说明里),新骨架延续 `validator-triager`/`benchmark-validator` 已验证的"归因/打分/判定三者分离"范式。**预算风险量化**:N 路候选并行消耗 N 倍预算,且 `checkBudget` 只在批量拾取前查一次,实际可能超支到 `maxConcurrency-1`(默认最多多花 4 次调用)才被拦下,候选数 N 目前代码层没有硬顶 | |
| 73 | -| [51-ceo-autonomous-goal-refill-design.md](./todo/system-design/51-ceo-autonomous-goal-refill-design.md) | ✅ **阶段 A 已实现**(2026-09-09 设计 / 2026-09-17 实现+实测)。回应"公司能否 7×24 自转"的提问——核实执行层(存活扫描/僵尸 goal 收口/CEO 阻塞任务兜底心跳)已经扎实,真正缺口在决策层:`ceo/heartbeat.md` 与 `tools.md` 原本"该提新任务"判断全部要求先有一个 goal/task 存在,没有一条覆盖"公司没有任何 active/draft/planning goal"这种完全空转场景;CEO 也原本**没有创建 goal 的工具**(`POST /:companyId/goals` 存在于路由层但从未暴露给它)。已接线:心跳先查 `GET /goals` 判空转 → 查公司使命("待定义"时拒绝立项转人工)→ 去重 → `POST /goals` 建 `split:false` + `created_by:'ceo'` 的草稿。**起服务实测(不只是读代码)纠正了两处判断**:① 原设计的多状态/`created_by` 查询参数服务端不支持,改客户端过滤;② 最初以为零 task 的草稿会让 `/dispatch` 天然 409、这就是审批闸门——实测发现 `split:false` 其实自动带 1 条 draft task,`dispatch` 技术上立刻能成功,**没有代码层闸门**,阶段 A 的安全性改为完全依赖 `ceo/tools.md` 里"不许自己调 dispatch"这条提示词约束,是当前最脆弱的一环(见四)。对所有公司默认生效,未改任何路由/服务层代码,纯 agent 人设文档改动(见七)。阶段 B(CEO 自动 dispatch + guardrail + 公司级开关)仍是设计草稿,未实现 | | |
| 73 | +| [51-ceo-autonomous-goal-refill-design.md](./todo/system-design/51-ceo-autonomous-goal-refill-design.md) | ✅ **阶段 A 已实现 + 代码闸门已补上**(2026-09-09 设计 / 2026-09-17 实现+实测 / 2026-09-18 代码闸门)。回应"公司能否 7×24 自转"的提问——核实执行层已经扎实,真正缺口在决策层:`ceo/heartbeat.md`/`tools.md` 原本没有一条覆盖"公司没有任何 active/draft/planning goal"这种完全空转场景,CEO 也原本没有创建 goal 的工具。已接线:心跳查空转 → 查公司使命("待定义"拒绝转人工)→ 去重 → `POST /goals` 建 `split:false` + `created_by:'ceo'` 的草稿。**实测(不只是读代码)纠正了两处判断**:多状态/`created_by` 查询参数服务端不支持,改客户端过滤;`split:false` 其实自动带 1 条 draft task、`dispatch` 技术上立刻能成功——最初设想的"零 task 闸门"不存在。**2026-09-18 补上真正的代码闸门**:`POST /goals/:goalId/dispatch` 新增判断,`created_by==='ceo'` 且 `updated_at===created_at`(从没被 `PATCH` 过)直接 409,用既有字段代理"有没有人碰过",不加新列(已知简化,见文内 ponytail 说明,阶段 B 会换成更精确的 `reviewed_at`);`task-queue-routes.test.js` 新增 2 条用例,相关 4 个测试文件 84 条用例全绿无回归。阶段 B(CEO 自动 dispatch + guardrail + 公司级开关)仍是设计草稿,未实现 | | |
| 74 | 74 | |
| 75 | 75 | ## todo/future/(暂缓,非当前重心) |
| 76 | 76 | ... | ... |
docs/todo/system-design/51-ceo-autonomous-goal-refill-design.md
| 1 | 1 | # 51 · CEO 心跳自主立项设计——补齐"目标耗尽后无人接续"的缺口 |
| 2 | 2 | |
| 3 | -> **状态**: ✅ **阶段 A 已实现**(2026-09-17,见七)。阶段 B(CEO 自动 enqueue + guardrail) | |
| 4 | -> 仍是设计草稿,不代表已拍板,公司级配置字段形状、guardrail 数值需要单独立项再定案。 | |
| 5 | -> **日期**: 2026-09-09(设计)/ 2026-09-17(阶段 A 实现) | |
| 3 | +> **状态**: ✅ **阶段 A 已实现 + 代码闸门已补上**(2026-09-17 实现,2026-09-18 加代码闸门,见七/八)。 | |
| 4 | +> 阶段 B(CEO 自动 dispatch + guardrail)仍是设计草稿,不代表已拍板,公司级配置字段形状、 | |
| 5 | +> guardrail 数值需要单独立项再定案。 | |
| 6 | +> **日期**: 2026-09-09(设计)/ 2026-09-17(阶段 A 实现)/ 2026-09-18(代码闸门) | |
| 6 | 7 | > **背景**: 回应"设计一个 goal,让 company 能 7×24 小时自我运行,差距还有多远"的提问—— |
| 7 | 8 | > 结论是执行层(graph 引擎的存活扫描 / 僵尸 goal 收口 / CEO 阻塞任务兜底心跳)已经相当扎实, |
| 8 | 9 | > 真正的缺口在**决策层**:没有任何机制在"公司没有任何活着的目标"时提出下一个目标。 |
| ... | ... | @@ -189,9 +190,13 @@ task"这道技术闸门**不存在**——`split:false` 唯一确定省下的是 |
| 189 | 190 | `POST /goals {split:false}` 才发现同步路径其实自动带了 1 条 `draft` task |
| 190 | 191 | (`company.js:1859-1862`),`dispatch` 判的是"有没有 `draft` 状态的 task",1 条就满足, |
| 191 | 192 | **技术上立刻能跑,没有任何接口层面的东西挡着**。原先"这就是审批闸门本身"的结论是错的, |
| 192 | - 已重写进 3.3/3.5 节:阶段 A 的唯一把关变成了"`ceo/tools.md` 明确写死 CEO 不许自己调 | |
| 193 | - `dispatch`",一条纯提示词约束,不是代码层面的硬闸门。**这是本文档目前最大的剩余风险**—— | |
| 194 | - 这条约束是否真的会被 CEO 稳定遵守,只能靠实际跑几轮观察,读代码/读提示词都验证不了。 | |
| 193 | + 已重写进 3.3/3.5 节。 | |
| 194 | +- ~~阶段 A 的唯一把关是提示词约束,不是代码闸门,是当前最大的剩余风险~~ **已加代码闸门 | |
| 195 | + (2026-09-18,见七)**:`dispatch` 路由现在会判 `goal.created_by === 'ceo' && | |
| 196 | + goal.updated_at === goal.created_at`(自建成后从没被 PATCH 过),命中直接 409。 | |
| 197 | + `ceo/tools.md` 里"不许自己调 dispatch"那条提示词约束继续保留(纵深防御,不指望单一层), | |
| 198 | + 但已经不是唯一的把关手段了。这道闸门本身是一个已知的简化(见七的 ponytail 说明), | |
| 199 | + 不是万能的"确实被人认真审过"判据。 | |
| 195 | 200 | - **心跳频率与立项频率耦合**:3.4 节的去重能防止无限堆草稿,但没有解决"CEO 多久该重新评估一次 |
| 196 | 201 | 公司使命有没有新方向"这个更长期的问题——目前设计只覆盖"完全空转"这个最紧迫场景,节奏性的 |
| 197 | 202 | 战略复盘不在本文档范围(见二、非目标)。 |
| ... | ... | @@ -251,3 +256,43 @@ task"这道技术闸门**不存在**——`split:false` 唯一确定省下的是 |
| 251 | 256 | - 未改动任何路由 / 服务层代码——`POST /:companyId/goals`、`.../dispatch`、`listCompanyGoals` |
| 252 | 257 | 均按现状直接复用,阶段 A 完全是 agent 人设文档层面的改动。 |
| 253 | 258 | - 阶段 B(公司级自转开关、guardrail 数值、CEO 自动 enqueue)未实现,见五、明确不做。 |
| 259 | + | |
| 260 | +--- | |
| 261 | + | |
| 262 | +## 八、代码闸门实现记录(2026-09-18) | |
| 263 | + | |
| 264 | +补上七、第 2 点标出的缺口——把"CEO 不许自己调 dispatch"从纯提示词约束升级成真正的代码闸门。 | |
| 265 | + | |
| 266 | +**改动**:`server/routes/company.js` 的 `POST /:companyId/goals/:goalId/dispatch` | |
| 267 | +(`requireGoal` 之后、原有"没有待下达的 task"检查之前)新增一段判断: | |
| 268 | + | |
| 269 | +```js | |
| 270 | +if (goal.created_by === 'ceo' && goal.updated_at === goal.created_at) { | |
| 271 | + return res.status(409).json({ error: `goal ${goal.id} 是 CEO 自主立项的草稿,还没有人工审阅过,不能直接下达`, ... }); | |
| 272 | +} | |
| 273 | +``` | |
| 274 | + | |
| 275 | +**判据选择**:`updated_at === created_at`(自建成后从没被 `PATCH` 过)代理"有没有人碰过", | |
| 276 | +不新增数据库列。依据: | |
| 277 | +- `createGoal`(`work-graph-db.js:1800-1825`)在 INSERT 时把 `created_at`/`updated_at` | |
| 278 | + 设成同一个 `now` 值;`updateGoal`(走既有的 `PATCH /:companyId/goals/:goalId` 端点)每次 | |
| 279 | + 调用都会把 `updated_at` 刷新——两者结合,"从没被 PATCH 过"和"两个时间戳相等"严格等价。 | |
| 280 | +- 已核实 `split:false` 同步创建 goal+task 的路径(`insertTasksForGoal`)只写 `tasks` 表, | |
| 281 | + 不会碰 `goals` 行——不会出现"建完就已经被判定为碰过"的假阳性。 | |
| 282 | +- `ponytail`:这是"有没有被 PATCH 过"代理"有没有被认真审过"的简化,天花板是"PATCH 一次 | |
| 283 | + 同值字段"也会被判定为"审过",分不清"真看懂了"和"随手碰一下解锁"。要分清这两者需要专门的 | |
| 284 | + `reviewed_by`/`reviewed_at` 列(谁在什么时候明确按了"我看过了"),留给阶段 B 一起做——阶段 B | |
| 285 | + 本来就要给"CEO 自己 dispatch"设计更细的 guardrail,那时候一并设计更精确的审阅判据更划算, | |
| 286 | + 不为这一步单独开一次 schema 改动。 | |
| 287 | + | |
| 288 | +**测试**:`server/tests/unit/task-queue-routes.test.js` 新增两条用例(复用文件既有的 org | |
| 289 | +graph + `newGoal` 测试夹具,无需新建 harness):`created_by:'ceo'` 且未 `PATCH` 过的草稿 | |
| 290 | +直接 dispatch → 409,`PATCH` 一次后 dispatch → 201;`created_by` 是 `'human'`/不填的草稿不受 | |
| 291 | +这道闸门影响,照常直接 dispatch(防止误伤既有行为)。相关四个测试文件(`task-queue-routes`/ | |
| 292 | +`company-goal-task-routes`/`goal-task-acceptance`/`task-dispatch-store`,共 84 条用例)全部 | |
| 293 | +通过,无回归。 | |
| 294 | + | |
| 295 | +**对三、3.5 节的影响**:阶段 A 现在是纵深防御——`ceo/tools.md` 的提示词约束是第一层(CEO | |
| 296 | +按设计根本不该去调 `dispatch`),本节的代码闸门是第二层(万一提示词约束失效,`created_by` | |
| 297 | +匹配的草稿在没被 `PATCH` 过之前技术上也跑不起来)。`ceo/tools.md` 的措辞保留不变,不因为 | |
| 298 | +有了代码闸门就放松对 CEO 的行为要求。 | ... | ... |
server/routes/company.js
| ... | ... | @@ -1982,6 +1982,22 @@ router.post('/:companyId/goals/:goalId/dispatch', (req, res) => { |
| 1982 | 1982 | const goal = requireGoal(req, res); |
| 1983 | 1983 | if (!goal) return; |
| 1984 | 1984 | |
| 1985 | + // 51 号文档「CEO 自主立项」:CEO 心跳在公司空转时会自己起草一条 `created_by:'ceo'` 的 | |
| 1986 | + // goal 草稿,设计上这条草稿必须等人审过才能跑——但草稿本身(`split:false`)建出来就已经 | |
| 1987 | + // 带着 1 条 draft task,技术上立刻能 dispatch,原设计以为的「零 task 挡住」并不存在(见 | |
| 1988 | + // 51 号 §四)。这里补上真正的代码闸门:`updated_at === created_at` 说明这一行自建成后 | |
| 1989 | + // 从没被 PATCH 过,没人碰过。 | |
| 1990 | + // ponytail: 用「有没有被 PATCH 过」代理「有没有被人审过」,是省一次 schema 改动的简化—— | |
| 1991 | + // 天花板是「PATCH 一次同值字段」也会被判定为「审过」,判不出内容有没有真被看懂。真要分清 | |
| 1992 | + // 「审过」和「随手碰过」,需要专门的 `reviewed_by`/`reviewed_at` 列,留给阶段 B 一起做。 | |
| 1993 | + if (goal.created_by === 'ceo' && goal.updated_at === goal.created_at) { | |
| 1994 | + return res.status(409).json({ | |
| 1995 | + error: `goal ${goal.id} 是 CEO 自主立项的草稿,还没有人工审阅过,不能直接下达`, | |
| 1996 | + hint: `先 PATCH /api/company/${companyId}/goals/${goal.id}(哪怕只是确认一下 title/priority)` | |
| 1997 | + + `做一次人工确认,再重试 dispatch`, | |
| 1998 | + }); | |
| 1999 | + } | |
| 2000 | + | |
| 1985 | 2001 | const drafts = wdb.listGoalTasks(goal.id).filter((t) => t.status === 'draft'); |
| 1986 | 2002 | if (!drafts.length) { |
| 1987 | 2003 | return res.status(409).json({ | ... | ... |
server/tests/unit/task-queue-routes.test.js
| ... | ... | @@ -176,6 +176,33 @@ describe('批 3 排队 · HTTP 端点(27-P3C §六.3)', () => { |
| 176 | 176 | }); |
| 177 | 177 | |
| 178 | 178 | // ──────────────────────────────────────────────────────────────── |
| 179 | + // 51 号文档:CEO 自主立项的草稿,没人碰过不许 dispatch | |
| 180 | + // ──────────────────────────────────────────────────────────────── | |
| 181 | + | |
| 182 | + it('created_by=ceo 且没被 PATCH 过的草稿,dispatch 直接 409;PATCH 一次之后放行', async () => { | |
| 183 | + const cid = newCompany(); | |
| 184 | + const goal = await newGoal(cid, [{ description: '干活' }], { created_by: 'ceo' }); | |
| 185 | + | |
| 186 | + const [blockedStatus, blockedBody] = await call('POST', `/api/company/${cid}/goals/${goal.goalId}/dispatch`); | |
| 187 | + expect(blockedStatus).toBe(409); | |
| 188 | + expect(blockedBody.error).toMatch(/CEO 自主立项/); | |
| 189 | + | |
| 190 | + const [patchStatus] = await call('PATCH', `/api/company/${cid}/goals/${goal.goalId}`, { priority: 'P0' }); | |
| 191 | + expect(patchStatus).toBe(200); | |
| 192 | + | |
| 193 | + const [dispatchStatus, dispatchBody] = await call('POST', `/api/company/${cid}/goals/${goal.goalId}/dispatch`); | |
| 194 | + expect(dispatchStatus).toBe(201); | |
| 195 | + expect(dispatchBody.started.taskId).toBe(goal.tasks[0].taskId); | |
| 196 | + }); | |
| 197 | + | |
| 198 | + it('created_by=human(或不填)的草稿不受这道闸门影响,照常直接 dispatch', async () => { | |
| 199 | + const cid = newCompany(); | |
| 200 | + const goal = await newGoal(cid, [{ description: '干活' }]); // 不传 created_by | |
| 201 | + const [status] = await call('POST', `/api/company/${cid}/goals/${goal.goalId}/dispatch`); | |
| 202 | + expect(status).toBe(201); | |
| 203 | + }); | |
| 204 | + | |
| 205 | + // ──────────────────────────────────────────────────────────────── | |
| 179 | 206 | // 验收 3:409 消失 |
| 180 | 207 | // ──────────────────────────────────────────────────────────────── |
| 181 | 208 | ... | ... |
system/agents/ceo/tools.md
| ... | ... | @@ -124,7 +124,8 @@ draft task 謖ょ惠霑吩クェ goal 荳銀披比ク肴弍髮カ task縲りソ呎э蜻ウ逹霑吩クェ goal |
| 124 | 124 | `POST /goals/<goalId>/dispatch`**窶披皮ォ矩。ケ讓。蠑丈ク倶ス逧′雍」蛻ー"蟒コ螳瑚拷遞ソ"荳コ豁「形dispatch` 霑吩ク豁・ |
| 125 | 125 | 豌ク霑懃蕗扈吩ココ邀サ蜴サ謖峨ょ悪謚"謠蝉ク譚。譁ー莉サ蜉。"驍」闃る"菴陲ォ謗域揀蜿ッ莉・閾ェ荳サ enqueue"逧揀髯千アサ豈泌芦霑咎 |
| 126 | 126 | 驍」譏ッ蟇ケ逹莠コ邀サ蟾イ扈冗恚霑噪 goal 荳句刈逧菴 task瑚ソ咎譏ッ菴閾ェ蟾ア蜃ュ菴ソ蜻ス迪懷譚・逧眠譁ケ蜷托シ御ク、莉カ莠 |
| 127 | -鬟朱勦郤ァ蛻ォ荳堺ク譬キ御ク崎螂礼畑蜷御ク譚。譚剞縲 | |
| 127 | +鬟朱勦郤ァ蛻ォ荳堺ク譬キ御ク崎螂礼畑蜷御ク譚。譚剞縲ゑシ2026-09-18 襍キ `dispatch` 遶ッ轤ケ閾ェ蟾ア荵滉シ壽拠扈晁ソ咏ァ | |
| 128 | +豐。陲ォ `PATCH` 霑噪 `created_by:'ceo'` 闕臥ィソ窶披比スソ呎弍蜈懷コ包シ御ク肴弍蜈∬ョク菴蜴サ隸慕噪逅罰縲ゑシ | |
| 128 | 129 | |
| 129 | 130 | **`decomposed_work` 蟄蝉ササ蜉。襍ー蜷御ク荳ェ遶ッ轤ケ**窶披覗subtask-planning` 蟾イ扈乗滑蜀ョケ諡・ス莠シ御ス荳咲畑蜀榊愛譁ュ |
| 130 | 131 | "貂ク肴ク・"悟宵蛻、譁ュ闃ょ・擾シ育鴫蝨ィ謾セ霑俶弍遲会シ峨ょ酔荳譚。 task 荳狗噪蟄蝉ササ蜉。蜿ッ莉・荳谺。諤ァ蜈ィ驛ィ enqueue | ... | ... |