Commit 0a4c2cfb8b45b5489530853f8aee5ba477feadb3
1 parent
1813f9d6
docs: move fully-implemented todo docs to docs/done/
work-graph-partial-artifact-persistence and work-graph-self-planning- and-loop-back (requirements + design) are all shipped and tested; move them out of docs/todo/ and fix up the docs/todo/*.md and src/*.py path references that pointed at their old location. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Showing
10 changed files
with
14 additions
and
14 deletions
docs/todo/work-graph-partial-artifact-persistence-requirements.md renamed to docs/done/work-graph-partial-artifact-persistence-requirements.md
docs/todo/work-graph-self-planning-and-loop-back-design.md renamed to docs/done/work-graph-self-planning-and-loop-back-design.md
| 1 | 1 | # Planner 动态自规划 Work Graph(含节点判断与失败回路)——详细设计(SDD) |
| 2 | 2 | |
| 3 | 3 | **日期**: 2026-08-08 |
| 4 | -**性质**: 详细设计,对应需求文档 `docs/todo/work-graph-self-planning-and-loop-back-requirements.md`(已确认)。本设计给出可直接实施的 schema/接口/算法/测试方案,逐条落实需求文档「8. 已确认的决策」。 | |
| 4 | +**性质**: 详细设计,对应需求文档 `docs/done/work-graph-self-planning-and-loop-back-requirements.md`(已确认)。本设计给出可直接实施的 schema/接口/算法/测试方案,逐条落实需求文档「8. 已确认的决策」。 | |
| 5 | 5 | **依赖代码**:`src/work_graph_orchestrate.py`(`run_graph`/`topological_layers`/`_run_one_node`/`_verify_and_maybe_regenerate`/`_ensure_node_row`/`_LockedConn`)、`src/work_graph_plan.py`(`_PROMPT_TEMPLATE`/`_normalize_graph_manifest`/`GRAPH_MANIFEST_SCHEMA`)、`src/e2e_pipeline.py`(`plan_documents`/`check_script_compiles`/`_check_prd_consistency`)、`src/settings_config.py`(`get_/upsert_concurrency_config` 先例)、`src/schema.py`(`migrate_expand_artifact_types` 先例)。 |
| 6 | 6 | **状态**: ✅ 已实现(2026-08-08)。实施顺序按第 11 节执行,`python3 -m unittest discover -s src`(692 tests)+ `discover -s supervisor -p "test_*.py"`(126 tests)全绿。实现过程中用真实测试发现并修正了本设计原文的几处问题(设计文档本身未回填,实现是最终准确来源): |
| 7 | 7 | - §2 最初的 `on_fail.target_node_id` 字段设计,在实现前按用户反馈改成了从 `on_failure` 边读取(已反映在正文),但 §4.0(`topological_layers()` 必须过滤 `on_failure` 边)和 §8.3(`_validate_loop_back_targets` 改成校验边而不是字段)是**开始写代码之后**才发现遗漏并现场修的两处,不是设计阶段就想全的。 |
| ... | ... | @@ -425,7 +425,7 @@ def _resolve_loop_limit(conn, org_node, graph_manifest, target_node_id): |
| 425 | 425 | def migrate_add_loop_back_event_type(conn): |
| 426 | 426 | """2026-08-08: 给 execution_event.event_type 的 CHECK 枚举加 |
| 427 | 427 | 'loop_back_triggered',用于记录 Work Graph 内部回路——见 |
| 428 | - docs/todo/work-graph-self-planning-and-loop-back-design.md §7。 | |
| 428 | + docs/done/work-graph-self-planning-and-loop-back-design.md §7。 | |
| 429 | 429 | 幂等,安全对已有数据库无条件调用(同 migrate_expand_artifact_types 的写法)。 |
| 430 | 430 | """ |
| 431 | 431 | row = conn.execute("SELECT sql FROM sqlite_master WHERE type='table' AND name='execution_event'").fetchone() | ... | ... |
docs/todo/work-graph-self-planning-and-loop-back-requirements.md renamed to docs/done/work-graph-self-planning-and-loop-back-requirements.md
| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | |
| 3 | 3 | **日期**: 2026-08-08 |
| 4 | 4 | **性质**: 需求文档(BRD/PRD 层面),不含详细技术设计。回应用户 2026-08-07/08-08 两轮讨论:先是"要不要让 Planner 自己判断要不要有 test_script 节点",深入后明确为一个更完整的诉求——Planner 第一步应该是自己规划 Work Graph(要不要 brd/prd/sdd/benchmark/test 的组合、未来还有更多 tool/skill 可组合调用),并为每个节点规划输入输出边约束、判断成功/失败的方式,失败时把结果送回更早的节点形成回路(loop),并把 loop 过程中的产出记录下来供后续轮次参考改进。详细设计(SDD 层面:graph_manifest 的具体 JSON 形状、`topological_layers()`/`run_graph()` 的具体改造点、循环历史的具体持久化方式)留到本文档确认后单独产出。 |
| 5 | -**状态**: ✅ 已实现(2026-08-08)。原「8. 开放问题」的 7 个分叉点均已拍板,记录见「8. 已确认的决策」,正文相应章节已按决策更新。详细设计见 `docs/todo/work-graph-self-planning-and-loop-back-design.md`;Part A(判断+回路引擎)与 Part B(Planner 自规划)均已实现,`python3 -m unittest discover -s src`(692 tests)+ `discover -s supervisor -p "test_*.py"`(126 tests)全绿。尚未做真实 `RUN_REAL_AGENT_E2E=1` 端到端验证(需要真实 API 额度,等待用户决定是否现在跑)。 | |
| 5 | +**状态**: ✅ 已实现(2026-08-08)。原「8. 开放问题」的 7 个分叉点均已拍板,记录见「8. 已确认的决策」,正文相应章节已按决策更新。详细设计见 `docs/done/work-graph-self-planning-and-loop-back-design.md`;Part A(判断+回路引擎)与 Part B(Planner 自规划)均已实现,`python3 -m unittest discover -s src`(692 tests)+ `discover -s supervisor -p "test_*.py"`(126 tests)全绿。尚未做真实 `RUN_REAL_AGENT_E2E=1` 端到端验证(需要真实 API 额度,等待用户决定是否现在跑)。 | |
| 6 | 6 | |
| 7 | 7 | ## 1. 背景 |
| 8 | 8 | ... | ... |
docs/todo/org-graph-as-work-graph-unified-loop-requirements.md
| 1 | 1 | # Org Graph 本身也变成一个 Work Graph(Planner/Coder 之间加判断+精确回路)——需求文档 |
| 2 | 2 | |
| 3 | 3 | **日期**: 2026-08-08 |
| 4 | -**性质**: 需求文档草稿(BRD/PRD 层面),尚未逐条讨论确认——跟 `docs/todo/work-graph-self-planning-and-loop-back-requirements.md` 不同,那份是走完"逐条开放问题讨论→用户拍板"流程后才写的,这份是用户提出想法后直接要求"先记到 todo 里",还没有走那个流程。「6. 待确认的问题」列的都是需要讨论、不是已经拍板的。 | |
| 4 | +**性质**: 需求文档草稿(BRD/PRD 层面),尚未逐条讨论确认——跟 `docs/done/work-graph-self-planning-and-loop-back-requirements.md` 不同,那份是走完"逐条开放问题讨论→用户拍板"流程后才写的,这份是用户提出想法后直接要求"先记到 todo 里",还没有走那个流程。「6. 待确认的问题」列的都是需要讨论、不是已经拍板的。 | |
| 5 | 5 | **触发场景(真实复现)**:任务 `2084590a`(修复 `work_node_run.py` 的 `execution_log_file` bug)里,Planner 自规划出的 `benchmark` 节点连续 4 次产出的都是"叙述我做了什么"的散文(跟 `docs/todo/validator-test-script-narrative-output-issue.md` 同一类问题,但这次发生在 benchmark 上,不是 test_script),LLM judge 的 `on_fail=retry_self` 循环耗尽 `max_attempts=3` 后任务直接 `blocked_pending_human`。用户追问引出两个观察:① Validator 从来没有机会在 Coder 开始实现之前复核 Planner 自己的产出;② 现有的 `org_level_loop.py`/`org_loop_route.py` 只在 Coder 完整实现之后才起作用,而且只能粗粒度地把整个任务打回"Planner 阶段"或"Coder 阶段"重来,不能精确跳到某个具体 Work Graph 节点。 |
| 6 | 6 | **状态**: 草稿,待讨论。 |
| 7 | 7 | |
| ... | ... | @@ -51,5 +51,5 @@ |
| 51 | 51 | |
| 52 | 52 | ## 7. 相关文档 |
| 53 | 53 | |
| 54 | -- `docs/todo/work-graph-self-planning-and-loop-back-requirements.md`/`-design.md`:今天实现的 Work-Graph-内部判断+回路机制,这份需求本质是把它推广到 Org Graph 层面。 | |
| 54 | +- `docs/done/work-graph-self-planning-and-loop-back-requirements.md`/`-design.md`:今天实现的 Work-Graph-内部判断+回路机制,这份需求本质是把它推广到 Org Graph 层面。 | |
| 55 | 55 | - `docs/todo/validator-test-script-narrative-output-issue.md`:叙述文字问题的原始记录(目前只覆盖 test_script),这次真实复现证明同一个根因也会发生在 benchmark 上,需要更新范围。 | ... | ... |
docs/todo/task-execution-resume-assumes-planner-artifacts-exist.md
| 1 | 1 | # `task_execution.py` 恢复逻辑假设 Planner 一定已产出 sdd/prd/benchmark artifact——问题记录 |
| 2 | 2 | |
| 3 | 3 | **日期**: 2026-08-08 |
| 4 | -**性质**: 问题记录(真实复现,根因已定位,未修复)。发现于 `docs/todo/work-graph-self-planning-and-loop-back-requirements.md`(Planner 自规划)上线后的第一次真实端到端验证。 | |
| 4 | +**性质**: 问题记录(真实复现,根因已定位,未修复)。发现于 `docs/done/work-graph-self-planning-and-loop-back-requirements.md`(Planner 自规划)上线后的第一次真实端到端验证。 | |
| 5 | 5 | **状态**: 待修复,未立项。 |
| 6 | 6 | |
| 7 | 7 | ## 1. 现象(真实复现) |
| ... | ... | @@ -16,7 +16,7 @@ |
| 16 | 16 | |
| 17 | 17 | `org_node_lifecycle.trigger_org_node_full(conn, "planner", ...)` 在 `run_task_lifecycle()` 里**先于** `plan_documents()` 执行,并把 `task.status` 设成 `_POST_STATE["planner"] = "planning_brd_prd_sdd"`(`org_node_lifecycle.py:32`)——这个状态转移只代表"Planner 的 ready 确认调用成功了",不代表"Planner 的真实规划(`plan_documents()`)已经产出了任何东西"。 |
| 18 | 18 | |
| 19 | -`task_execution.py::run_or_resume_task_lifecycle()` 里 `planning_brd_prd_sdd` 分支(`task_execution.py:126-129`)的注释写的是"Planner done, Coder not yet triggered"——这个假设在 Work Graph 自规划(`docs/todo/work-graph-self-planning-and-loop-back-requirements.md`)上线前基本成立:固定链的第一个节点是 brd,几乎不可能在第一个节点就失败到零产出。自规划之后这个假设不再可靠——自规划出的图可能把一个更容易失败的节点(比如需要更复杂 judgment 的 sdd)排在第一位,如果它在第一次尝试就彻底失败(耗尽 loop_back/retry 上限,或者像这次一样是一个跟自规划本身相关的新 bug),Planner 的 `work_graph_run` 会整个失败,不会有任何 artifact 落盘,但 `task.status` 已经先一步被置成了 `planning_brd_prd_sdd`。 | |
| 19 | +`task_execution.py::run_or_resume_task_lifecycle()` 里 `planning_brd_prd_sdd` 分支(`task_execution.py:126-129`)的注释写的是"Planner done, Coder not yet triggered"——这个假设在 Work Graph 自规划(`docs/done/work-graph-self-planning-and-loop-back-requirements.md`)上线前基本成立:固定链的第一个节点是 brd,几乎不可能在第一个节点就失败到零产出。自规划之后这个假设不再可靠——自规划出的图可能把一个更容易失败的节点(比如需要更复杂 judgment 的 sdd)排在第一位,如果它在第一次尝试就彻底失败(耗尽 loop_back/retry 上限,或者像这次一样是一个跟自规划本身相关的新 bug),Planner 的 `work_graph_run` 会整个失败,不会有任何 artifact 落盘,但 `task.status` 已经先一步被置成了 `planning_brd_prd_sdd`。 | |
| 20 | 20 | |
| 21 | 21 | `test_dataset_content`/`test_script_content` 已经用 `_artifact_content_or_default()`(读不到就给默认值,不硬失败)处理过同一类问题(2026-08-06,那次是"这两个 artifact 类型本来就允许不存在"),但 `sdd`/`prd`/`benchmark` 三个仍然用会硬失败的 `_artifact_content()`——这个区分在固定链时代是对的(sdd/prd/benchmark 曾经是保证存在的),自规划之后不再成立。 |
| 22 | 22 | ... | ... |
docs/todo/validator-test-script-narrative-output-issue.md
| ... | ... | @@ -2,7 +2,7 @@ |
| 2 | 2 | |
| 3 | 3 | **日期**: 2026-08-07 |
| 4 | 4 | **性质**: 问题记录 + 待调查,不是已确认根因的需求文档(区别于同目录下其他几份——那几份是已经定位到具体代码 bug 并修复过的,这份目前还只是"观察到的现象",根因未确定)。 |
| 5 | -**状态**: 根因已定位(见第 6 节)。用户决策(2026-08-07):不再继续在提示词/重试反馈上打补丁,**先直接禁用 test_script/validator_test_script 节点**(Planner 的 `work_graph_plan._PROMPT_TEMPLATE` 不再要求这个节点,Validator 的 `e2e_pipeline._VALIDATOR_MANIFEST` 从 3 节点缩成 2 节点),后续如果真的需要再重新设计(见第 8 节的结构性修复方向:把"探索"和"最终作答"拆成两次独立调用)。`_NARRATION_RETRY_HINT`(第 7 节)代码仍保留在 `_test_script_verify_fn` 里,只是当前这条路径不会被触发——如果以后重新启用这个节点,这个缓解依然生效。**2026-08-08 更新(第四轮,与自规划 Work Graph 需求的交叉决策)**:`docs/todo/work-graph-self-planning-and-loop-back-requirements.md`(Planner 自规划节点集合 + 失败回路需求)上线后,test_script 会重新作为可选节点开放给 Planner 自由规划——用户明确决定**不等本文档的结构性修复**,narrative-output 问题可能因此重现,这是已知且被接受的风险。本文档记录的根因(第 6 节)的真正结构性修复,安排在**另一份尚未撰写、关于"给 Coder agent 加入 test_script 模块"的文档**里一起解决,不是本文档单独继续跟进的范围。**2026-08-07 更新(第二轮):不只是 Validator 的问题——`911861fe` 这个纯后端任务在 Planner 自己的 `n_test_script` 节点上也复现了同一模式。累计 3 个任务、6 次到达 test_script 类节点,6 次全部复现(100%),横跨 Planner 和 Validator 两种场景。建议提高优先级,不要再靠重试撞运气。** **2026-08-07 更新(第三轮,根因排查):这是 08-06 "Benchmark 叙述事故"(commit `07a0a27`)的同类复发——test_script 类节点当时已经同时拿到提示词纠偏和机械拦截网双重防护,仍然 6/6 复现。根因不是"self-hosting 词汇污染"这个泛泛的猜测,而是 test_script 提示词本身要求"先做工具探索再产出内容",模型探索后的收尾话术天然是"汇报我做了/启动了什么"而不是切换成纯文本吐出模式;self-hosting 只是放大器,让叙述读起来更精确(因为探索对象就是 graphlemon 自己,词汇表精确对应),不是必要条件。BRD/PRD/SDD/Benchmark/test_dataset 这几个不需要先探索的纯组合型节点,自 08-06 后再未复现,与此吻合。** | |
| 5 | +**状态**: 根因已定位(见第 6 节)。用户决策(2026-08-07):不再继续在提示词/重试反馈上打补丁,**先直接禁用 test_script/validator_test_script 节点**(Planner 的 `work_graph_plan._PROMPT_TEMPLATE` 不再要求这个节点,Validator 的 `e2e_pipeline._VALIDATOR_MANIFEST` 从 3 节点缩成 2 节点),后续如果真的需要再重新设计(见第 8 节的结构性修复方向:把"探索"和"最终作答"拆成两次独立调用)。`_NARRATION_RETRY_HINT`(第 7 节)代码仍保留在 `_test_script_verify_fn` 里,只是当前这条路径不会被触发——如果以后重新启用这个节点,这个缓解依然生效。**2026-08-08 更新(第四轮,与自规划 Work Graph 需求的交叉决策)**:`docs/done/work-graph-self-planning-and-loop-back-requirements.md`(Planner 自规划节点集合 + 失败回路需求)上线后,test_script 会重新作为可选节点开放给 Planner 自由规划——用户明确决定**不等本文档的结构性修复**,narrative-output 问题可能因此重现,这是已知且被接受的风险。本文档记录的根因(第 6 节)的真正结构性修复,安排在**另一份尚未撰写、关于"给 Coder agent 加入 test_script 模块"的文档**里一起解决,不是本文档单独继续跟进的范围。**2026-08-07 更新(第二轮):不只是 Validator 的问题——`911861fe` 这个纯后端任务在 Planner 自己的 `n_test_script` 节点上也复现了同一模式。累计 3 个任务、6 次到达 test_script 类节点,6 次全部复现(100%),横跨 Planner 和 Validator 两种场景。建议提高优先级,不要再靠重试撞运气。** **2026-08-07 更新(第三轮,根因排查):这是 08-06 "Benchmark 叙述事故"(commit `07a0a27`)的同类复发——test_script 类节点当时已经同时拿到提示词纠偏和机械拦截网双重防护,仍然 6/6 复现。根因不是"self-hosting 词汇污染"这个泛泛的猜测,而是 test_script 提示词本身要求"先做工具探索再产出内容",模型探索后的收尾话术天然是"汇报我做了/启动了什么"而不是切换成纯文本吐出模式;self-hosting 只是放大器,让叙述读起来更精确(因为探索对象就是 graphlemon 自己,词汇表精确对应),不是必要条件。BRD/PRD/SDD/Benchmark/test_dataset 这几个不需要先探索的纯组合型节点,自 08-06 后再未复现,与此吻合。** | |
| 6 | 6 | |
| 7 | 7 | **2026-08-08 更新(第五轮,真实复现,推翻了第三轮"Benchmark 不需要先探索、所以不会复现"这个判断)**:任务 `2084590a`(Work Graph 自规划 + 失败回路上线后的第一次真实端到端验证)里,Planner 自规划出的 `benchmark` 节点连续 4 次真实产出全部是叙述文字,例如:"The planner Work Node finished. It produced a Benchmark artifact covering a real bug in `src/work_node_run.py::run_work_node()`... **No further action taken unless the user wants this handed off to the Coder**",以及 "**No files were changed in this checkout (git status clean) — this run only generated the benchmark text; it did not apply the fix or commit anything.**"——都是"我做了什么"的散文,不是真实的 benchmark 场景+断言内容。这次是自规划场景下 `judgment.kind="llm"` 的 LLM judge(不是机械检查)在把关,4 次里第 3 次的 judge 反馈甚至精确诊断出了这一点:"the text only asserts scenarios exist, without stating them... No benchmark.md matching this content was found on disk anywhere in the repo... recommend surfacing the actual artifact file/text... rather than a prose summary of it"——但这个诊断只是混在其他反馈条目里的一条,没有变成能强制拦住的机制,第 4 次原样复现,最终 `loop_back` 耗尽 `max_attempts=3`,任务 `blocked_pending_human`。 |
| 8 | 8 | ... | ... |
src/schema.py
| ... | ... | @@ -274,7 +274,7 @@ CREATE INDEX idx_execution_event_project_created ON execution_event(project_id, |
| 274 | 274 | CREATE INDEX idx_execution_event_task_created ON execution_event(task_id, created_at); |
| 275 | 275 | |
| 276 | 276 | -- 1.11.1 work_graph_loop_limit_config (Work Graph self-planning + loop-back, |
| 277 | --- docs/todo/work-graph-self-planning-and-loop-back-design.md §6) | |
| 277 | +-- docs/done/work-graph-self-planning-and-loop-back-design.md §6) | |
| 278 | 278 | CREATE TABLE work_graph_loop_limit_config ( |
| 279 | 279 | config_id TEXT PRIMARY KEY, |
| 280 | 280 | max_loop_attempts INTEGER NOT NULL, |
| ... | ... | @@ -596,7 +596,7 @@ def migrate_relax_org_node_constraints(conn): |
| 596 | 596 | |
| 597 | 597 | def migrate_add_work_graph_loop_limit_config(conn): |
| 598 | 598 | """2026-08-08 (Work Graph self-planning + loop-back, |
| 599 | - docs/todo/work-graph-self-planning-and-loop-back-design.md §6): adds | |
| 599 | + docs/done/work-graph-self-planning-and-loop-back-design.md §6): adds | |
| 600 | 600 | work_graph_loop_limit_config on an ALREADY-POPULATED database. Plain |
| 601 | 601 | `CREATE TABLE IF NOT EXISTS` -- a brand-new table, no CHECK constraint |
| 602 | 602 | to rebuild around, unlike migrate_expand_artifact_types. Idempotent and |
| ... | ... | @@ -612,7 +612,7 @@ def migrate_add_work_graph_loop_limit_config(conn): |
| 612 | 612 | |
| 613 | 613 | def migrate_add_loop_back_event_type(conn): |
| 614 | 614 | """2026-08-08 (Work Graph self-planning + loop-back, |
| 615 | - docs/todo/work-graph-self-planning-and-loop-back-design.md §7): adds | |
| 615 | + docs/done/work-graph-self-planning-and-loop-back-design.md §7): adds | |
| 616 | 616 | 'loop_back_triggered' to execution_event.event_type's CHECK enum on an |
| 617 | 617 | ALREADY-POPULATED database -- same create-copy-drop-rename dance as |
| 618 | 618 | migrate_expand_artifact_types (SQLite has no ALTER-based constraint | ... | ... |
src/task_query.py
| ... | ... | @@ -174,7 +174,7 @@ def get_task(conn, task_id): |
| 174 | 174 | # on_failure edge / a retry_self node's "sent back Nx" badge -- |
| 175 | 175 | # neither is derivable from graph_manifest/work_node_execution |
| 176 | 176 | # alone (loop_back_triggered is execution_event-only by design, |
| 177 | - # see docs/todo/work-graph-self-planning-and-loop-back-design.md §7). | |
| 177 | + # see docs/done/work-graph-self-planning-and-loop-back-design.md §7). | |
| 178 | 178 | "loop_back_events": _loop_back_events_for_run(conn, w[0]), |
| 179 | 179 | } |
| 180 | 180 | for w in runs | ... | ... |
src/test_schema.py
| ... | ... | @@ -748,7 +748,7 @@ class MigrateAddWorkGraphLoopLimitConfigTest(unittest.TestCase): |
| 748 | 748 | self.conn = schema.connect(":memory:") |
| 749 | 749 | old_sql = schema.SCHEMA_SQL.replace( |
| 750 | 750 | "-- 1.11.1 work_graph_loop_limit_config (Work Graph self-planning + loop-back,\n" |
| 751 | - "-- docs/todo/work-graph-self-planning-and-loop-back-design.md §6)\n" | |
| 751 | + "-- docs/done/work-graph-self-planning-and-loop-back-design.md §6)\n" | |
| 752 | 752 | "CREATE TABLE work_graph_loop_limit_config (\n" |
| 753 | 753 | " config_id TEXT PRIMARY KEY,\n" |
| 754 | 754 | " max_loop_attempts INTEGER NOT NULL,\n" | ... | ... |
src/work_graph_plan.py
| ... | ... | @@ -308,7 +308,7 @@ def _normalize_graph_manifest(manifest): |
| 308 | 308 | hardcoded canonical chain to fall back on if an LLM response omits |
| 309 | 309 | dependency info, so a real reproduction of the 2026-08-03 incident |
| 310 | 310 | shape is a genuine, currently-unmitigated risk this design accepted |
| 311 | - (see docs/todo/work-graph-self-planning-and-loop-back-requirements.md | |
| 311 | + (see docs/done/work-graph-self-planning-and-loop-back-requirements.md | |
| 312 | 312 | §7's risk list) -- FR-B3's own acceptance criterion only covers the |
| 313 | 313 | "wrong key names" failure mode this backfill already handles, not a |
| 314 | 314 | complete absence of dependency info with no canonical chain to guess from. | ... | ... |