fix(types): restore kernel contract checks for decision scope - #4257
Conversation
`python -m mypy` fails on current main with seven `no-any-return` errors in `loopx/control_plane/todos/decision_scope.py`. The module's private `_evaluate` helper validates one typed effect-runtime result and returns `Any`, but every public wrapper declares a concrete `dict`, `bool`, or list return type, so strict mode rejects each call site. Annotate the seven call sites with `cast(...)` to the return type they already promise. This is a type-level annotation only: no runtime branch, value, or call sequence changes, and `_evaluate` keeps its single validated boundary. The same fix is currently carried inside loopx-project#4233, which is blocked on chat asset retention changes. Every pull request inherits this failure through the required `Type-check kernel contracts` step and the `merge-gate`, so it is extracted here as a standalone repair. Validation: - `python -m mypy` -> Success: no issues found in 22 source files - `python -m pytest tests/control_plane/test_todo_decision_scope_consistency.py tests/control_plane/test_todo_decision_scope_lifecycle.py tests/control_plane/test_todo_decision_scope_cli_validation.py tests/control_plane/test_user_gate_lane_progress.py tests/control_plane/test_scoped_gate_successor_tool_behavior.py` -> 56 passed - `python examples/control_plane/bounded-context-namespace-smoke.py` -> ok Signed-off-by: song <liusongstep@gmail.com>
huangruiteng
left a comment
There was a problem hiding this comment.
审阅对象:PR #4257,exact head 78573ba0423c2a12cc13a99e1f8387cb6b69bec8,合并提交 78573ba,base main。本次审阅该 exact head 的完整增量(1 file, +14/-10),并核对了它声称修复的父侧失败。
动机
这个 PR 修的是"共享 main 上的必需检查坏掉,导致每个 PR 都被连带阻塞"。当时 python -m mypy 在 main 上失败,七处 no-any-return 全部位于 loopx/control_plane/todos/decision_scope.py;由于类型检查是必需步骤,merge-gate 不接受不完整的资格证明,于是任何开放中的 PR 都继承了这次失败,与它们自身改动无关。
这不是纯风格问题:该模块是 Todo decision scope 的 Python 编解码边界,私有的 _evaluate 返回 Any,而每个公开函数都声明了具体返回类型(dict[str, Any]、dict[str, Any] | None、bool、list[list[dict[str, Any] | None]])。声明与实现不一致意味着调用方拿不到类型保护,同时必需门禁无法通过。
改动思路
思路是"在真正的边界上说真话",而不是把检查静音:
- 保留既有的
_evaluate(它仍然校验todo_decision_scope_result_v0信封,非法投影照旧抛TypeError),只在七个公开函数的返回处用typing.cast标注该 operation 真实产生的类型。 cast在运行时是恒等函数,因此调用顺序、分支、取值、错误文案、控制流都没有变化;改动纯粹是签名层面的。- 每个 cast 都与 TypeScript owner 的分支类型逐一对齐:
consistency/relation/scope_relation/exact_relation->JsonObject(或 null)、standing->JsonObject | null、covers->boolean、relations-> 嵌套(JsonObject | null)[][]。这正是evaluateDecisionScope声明的联合类型JsonObject | boolean | null | (JsonObject | null)[][]的分支展开。
没有引入 Python 侧的第二套校验(那会与 TS owner 重复且增加热路径成本),没有用 # type: ignore 掩盖契约,也没有改动 wire schema。
具体改动
唯一文件 loopx/control_plane/todos/decision_scope.py(+14/-10):from typing import Any, cast;为 standing_decision_authority_for_agent、build_required_decision_scope_consistency、decision_scope_covers、decision_scope_gate_relation、exact_todo_gate_relation、todo_gate_relation、todo_gate_relations 七个函数增加与声明类型一致的 cast(...)(部分调用被折行,因此删除行数与新增行数接近)。
关键代码讲解:
_evaluate(未改动):effect_runtime_result("todo.decision_scope.evaluate", ...)之后校验信封 schema,返回result["result"]。它仍是唯一的运行时守门人。decision_scope_covers:保留if not gate or not required: return False短路,再cast(bool, _evaluate("covers", ...))——与 TS 的boolean分支一致。todo_gate_relations:保留空输入时[[] for _ in gates]的位置语义,再cast(list[list[dict[str, Any] | None]], _evaluate("relations", ...))——与 TS 的嵌套数组分支一致。standing_decision_authority_for_agent/*_relation:None的预检与_facts归一化顺序不变,只在返回处声明dict[str, Any] | None。build_required_decision_scope_consistency:源归一化、注册 agent 排序与 authority 处理全部保持原样,仅返回类型被声明为dict[str, Any]。
对主干的风险
纯类型标注改动,运行时行为等价:cast 不改值、不改分支、不改错误文案,因此不存在功能回归路径。唯一需要审视的是"cast 是否掩盖了不真实的类型"——本轮逐一比对了 TS 分支,七个 cast 都与 owner 的实际分支类型相符,没有夸大。
需要如实记录的残余风险:cast 不做运行时校验,因此如果未来某个已知 operation 的返回形状发生变化,mypy 不会再报错;承载该风险的是 TS owner 的联合类型与测试,以及"新增 operation 时必须同时扩展两侧"的约定。另一种理论风险是 covers 若返回真值对象,mypy 仍会通过——这正是 cast 的固有边界,也是我把"与 TS 联合类型比对"当作本次主要证据、而不只看注解的原因。
父侧证据(确认它修的就是真实失败):在本 PR 之前的 checks 作业日志中(#4251 合并头 fa57253 的 job 103344649803)可以直接看到 loopx/control_plane/todos/decision_scope.py:93: error: Returning Any from function declared to return "dict[str, Any] | None" [no-any-return],以及 104/196/200/204 行的同类错误——与本 PR 的定位一致。修复后该 head 的 23 项检查全绿(含 checks、kernel-static-checks、pytest、merge-gate)。
本地验证(该 exact head 的独立 worktree):python -m mypy -> Success: no issues found in 22 source files;pytest -q tests/control_plane/test_todo_decision_scope_consistency.py tests/control_plane/test_todo_decision_scope_lifecycle.py tests/control_plane/test_todo_decision_scope_cli_validation.py tests/control_plane/test_user_gate_lane_progress.py tests/control_plane/test_scoped_gate_successor_tool_behavior.py -> 56 passed in 66.85s。
我的整体评价
APPROVE。这是一个范围极小、目标明确的基础设施修复:它把"必需类型检查在 main 上失败、所有 PR 被连带阻塞"的状态恢复为绿色,做法是在既有编解码边界上如实声明返回类型,而不是用 type: ignore 掩盖或用 Python 侧重复校验替代 owner。七个 cast 与 TypeScript owner 的联合类型分支逐一对应,运行时行为完全等价,56 项聚焦测试与仓库级 mypy 都在该 head 通过,并且父侧日志明确记录了它消除的错误。
无阻断发现。建议性提示(非阻断):cast 不校验运行时形状,因此后续若新增 decision-scope operation,应同步扩展 TS 联合与这里的注解;如果将来希望在 Python 侧对形状做廉价断言,可以在不引入第二套规则的前提下对少数关键 operation 增加轻量检查。作为合并后审计记录,本结论只针对该 exact head,不授予任何合并、回滚或再次修改的权限。
English verdict: APPROVE - #4257 at exact head 78573ba (community contribution by songoow) restores the repository's required kernel type-check for loopx/control_plane/todos/decision_scope.py by declaring the seven public codec returns with typing.cast instead of returning Any, matching the TypeScript owner's per-operation union (consistency/relation/scope_relation/exact_relation -> object or null, standing -> object or null, covers -> boolean, relations -> nested array) while _evaluate's envelope validation, all guards, call order and error texts stay unchanged; cast is runtime-identity, so behavior is equivalent. Verified at the exact head: python -m mypy -> Success: no issues found in 22 source files, 56 focused decision-scope/lane tests pass, and hosted checks are 23/23 green including merge-gate; the parent-side checks log records exactly the no-any-return errors this change removes, which had been blocking every open pull request. No blocking finding; residual limit is the inherent cast blind spot (a future inner-shape drift is guarded by the TS owner's union and tests, not by mypy), and any new operation must extend both sides.
Summary
python -m mypyfails on currentmainwith sevenno-any-returnerrors, all inloopx/control_plane/todos/decision_scope.py. That step is required, andmerge-gaterejects any incomplete qualification, so every open pull request currently inherits the failure.The module's private
_evaluatehelper validates one typed effect-runtime result and returnsAny, while every public wrapper already declares a concretedict,bool, or list return type. Strict mode therefore rejects each call site.Change
Annotate the seven call sites with
cast(...)to the return type they already promise.This is a type-level annotation only. No runtime branch, value, or call sequence changes, and
_evaluatekeeps its single validated boundary.Why standalone
The identical fix is currently carried inside #4233, whose review is blocked on chat asset retention and generated-asset deletion. Extracting the two-line semantic change here unblocks the shared required check without depending on that PR. If #4233 merges first, this branch becomes a no-op and can be closed.
Validation
python -m mypy->Success: no issues found in 22 source filestests/control_plane/test_todo_decision_scope_consistency.pytests/control_plane/test_todo_decision_scope_lifecycle.pytests/control_plane/test_todo_decision_scope_cli_validation.pytests/control_plane/test_user_gate_lane_progress.pytests/control_plane/test_scoped_gate_successor_tool_behavior.pypython examples/control_plane/bounded-context-namespace-smoke.py-> okRisk
Minimal.
castperforms no runtime conversion, so the executed behavior is byte-identical tomain. The pre-existingI001import-sort finding for this file is unchanged by this patch and is not part of the CI ruff rule selection.