feat(semantics): separate action domains and verify production evidence - #4494
huangruiteng merged 10 commits into
Conversation
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
songoow
left a comment
There was a problem hiding this comment.
Request changes conclusion (author-owned PR; GitHub blocks formal self-review)
REQUEST_CHANGES:原 M0.5/M1 代码审查通过,但当前主干整合必须修复并重新验证。 这是具体整合阻塞,不是声称已发现新的运行时缺陷。
Reviewed head:2c703172174f3e43bcb0f4d371d4a9b83f86ae00。原比较基线:08f0e08d617d811e4e10480d33779296266ae1f5;本轮新主干审查:0796430385601b971d8c3740f68a9d265bb1d684。完整 PR 为66文件、+3843/-347。M2、后续语义债务及M3/M4不在本PR完成声明内。
动机
原来的 effective_action 混合quota决策、Frontier动作、replay判决和诊断错误码;枚举消费又可能被误当成实际生产证据。新增一个更大的字符串集合无法解决含义混用,也无法证明值确实由生产者输出。
本PR让调用者读对字段,并让后续维护者区分新增决策与新增诊断。只写glossary或机械替换常量名不足以完成这一目标;立即合并全部Turn词表又会引入不必要的迁移。复用原owner、派生绑定和限定扫描语法是合理范围。
改动思路
EffectiveAction的32个decision与Frontier的4个action保持分离;根should-run/envelope使用有明确成员及不相交检查的注册并集。TS绑定由Python owner派生,没有新增运行时registry权威或第二个控制器。
Frontier v1新写入只保留action;兼容reader先读action,再兜底旧字段,历史v0计划/签名读取不被重写。Turn-result observation的动作固定为JSON null/Python None,判决留在decision;quota仍为字符串,replay省略冗余动作。选择失败保留error_code并使用quota_skip;scoped fallback修复对未生产skip的判断,repair/replan优先级保留。
这些行为变化、维护成本和证明边界已写入双语RFC及Effect packet契约。有限语法检查不是全程序可达性或任意数据流证明。
具体改动
完整范围包括runtime常量迁移、两个生成绑定、registry/inventory、Python/TS扫描器、现有CI的解析器依赖、兼容文档和持久回归测试。多数runtime改动保留原scalar值;nullable result、Frontier writer、diagnostic action和fallback分别按有意变化验证。
关键边界:
scripts/generate_semantic_bindings.py::build_artifacts严格检查两个owner及并集后再生成,第二个owner失败不会留下半套输出。production.py::validate_production将closedness与confirmed output分开;keyword_unproved保留值域/unknown诊断,不获得生产者存活或登记信用。显式tuple路径与reviewed builder仍可提供有限输出证据。check_coverage_floor要求所有call/return-path selector等于代码锚点,其他词表默认空;registry-only扩展无法自行增加受信任证据。read_frontier_action复用两个真实caller的兼容读取,避免重复展开并保持热文件预算。interpretTurnResultPacket固定result动作为空,executor继续读decision;host验证、journal和effect职责保持。
原F1–F5反例已关闭:consumer/collection假生产、真实monitor裸字面量逃逸、静态computed键、混合字段BoolOp误归因和额外selector。正例与负例均保留。reviewed builder参数仍依赖body审查,没有冒充任意helper语义证明。
相对已审e59d594,本head仅增加已独立审查的三文件UTF-8/双语动机delta;其余63个PR文件字节不变。原shard3失败是扫描器subprocess缺少显式UTF-8,现已修复;不是通过放松测试消除失败。
对主干的风险
P2(阻塞整合):六处真实冲突,旧head检查不能证明新主干组合
新主干已包含#4496共享FreshTurnDecisionOwner、#4497 legacy authority迁移、#4459 policy5语义候选分类与unknown边界,以及steward/fixture变化。独立读取这些owner/caller变化并执行只读merge分析,确认以下六处content conflict:
docs/architecture/rfcs/semantic-vocabulary-convergence-v0.mddocs/architecture/rfcs/semantic-vocabulary-convergence-v0.zh-CN.mdexamples/semantic-vocabulary-drift-smoke.pyloopx/semantics/inventory_v0.jsonloopx/semantics/vocabulary_v0.jsontests/architecture/test_semantic_vocabulary_drift.py
远端回读也返回CONFLICTING / DIRTY。不能整体选择一方以丢弃M1执行约束,或丢弃已落地主干的candidate decisions/unknown契约。Turn caller虽然可文本自动合并,仍须验证共享owner接线;authority迁移也不能由旧组合测试代替。
最小修复:同步已核验canonical主干,逐处合并上述六路径并保留双方契约;重新生成inventory,校验bindings;重跑semantic/F1–F5、Turn shared-owner与真实managed-step/CLI、受影响authority迁移测试、risk-based premerge和新head必需CI。提交新的精确整合身份后复审。无需在此刻推测新运行时bug,也不应提高预算。
已成立的验证及其边界
独立180项semantic、15项受影响TS monitor、typecheck及真实Effect/CLI/journal、旧签名验证,在来源字节未变的范围内仍有效;UTF-8修复又有原AST守卫和172项lead定向测试。各批有重叠,不相加。
本head远端实测为24 SUCCESS、4 SKIPPED、0失败/等待;pytest、四个shard、merge-gate、真实PostgreSQL及平台检查成功。实际跳过的是deploy、upload-release、publish-pypi、presentation,不能将packet归一化后的“28 successful”说成28项均执行通过。它们也不证明与随后推进的主干整合通过。
我的整体评价
原M0.5/M1实现是必要且可回滚的阶段包,公开兼容变化已有披露,原源码问题已关闭。当前REQUEST_CHANGES只针对已经复现的六路径整合阻塞及组合验证缺口。
请先完成最小整合和新head验证,再更新评审结论。保持独立owner、严格生成/selector边界与历史读取兼容,不增加一般数据流框架或提前退休旧持久字段。本评审不授权merge,也不关闭整个RFC。
English verdict: REQUEST_CHANGES for exact head 2c703172174f3e43bcb0f4d371d4a9b83f86ae00. The scoped M0.5/M1 implementation and UTF-8 fix pass review;24 remote checks succeeded and4 were skipped. Current canonical main 0796430385601b971d8c3740f68a9d265bb1d684 produces six concrete semantic docs/guard/registry/test conflicts, confirmed by read-only merge analysis and remote CONFLICTING status. Preserve both contracts, regenerate inventory, and validate shared Turn/authority integration plus required CI on the resolved new head. No new runtime bug, merge authorization or whole-RFC completion is claimed.
huangruiteng
left a comment
There was a problem hiding this comment.
@/private/tmp/review-body-4494.md
huangruiteng
left a comment
There was a problem hiding this comment.
动机
should_run.effective_action 是每个 Turn 的根判决槽。主干上 loopx/semantics/vocabulary_v0.json 登记了 33 个字面值、却没有 owner 符号,并且一个字段名在同一个 Turn Envelope 里同时承载三套词表(decision、agent-scope frontier、journal replay)。具体代价已经在代码里显形:interpretTurnResultPacket 会把 host 传入的 action 或 result_kind(wait/advance/blocked)直接复制进 quota action 槽;选择失败把 quota_action_selection_deferred/rejected 这个错误码当作判决写进 effective_action;should-run 状态矩阵 fixture 甚至断言了一个生产者从不返回的合成值 operator_gate,而 quota_effective_action 在主干上早就返回 operator_gate_notify。
受影响的调用方是根判决槽的全部读者:quota CLI、status/attention 投射、Markdown renderer、TypeScript effect runtime、turn driver,以及需要读/修这张注册表的维护者。后果不是崩溃,而是读者无法区分"新决策"与"新诊断/回放判决":同一个槽位里既有真正的决策值,也有只被比较、从不生产的值,还有本应属于 error_code 的诊断码。
作者给出的 before/after 是:33 个字面值、一个字段名三套词表、turn result 的 action 等于 result_kind、selection 失败把错误码放进 action 槽;变成 32 个 owner 声明的决策值加一个显式登记的并集、嵌套 frontier 只输出一个 action、turn result 的 effective_action 为 null、selection 失败写 quota_skip + error_code。这个目标与 RFC docs/architecture/rfcs/semantic-vocabulary-convergence-v0.md 的 M1 行以及它的 ratchet 行(33 → 32,退掉 skip、observe_replay、block_replay、两个 quota_action_selection_* 与合成 operator_gate)完全一致。
为什么最近的更小修法不够:逐文件把裸字面量换成常量就是主干现状,它正是这次 33 字面量漂移的成因;只冻结 enum 而不带生产者关系,则会把"只被比较、从不生产"的值当成 canonical(本 PR 的守卫会直接以 values have no observed producer 拒绝这种注册);只改注册表而不引入 owner,则 union 槽位、v0 别名与 replay 槽位仍然没有声明处。
改动思路
权威状态分两层:loopx/semantics/vocabulary_v0.json 声明槽位关系与生产者注册,两个 owner 模块(quota/effective_action.py::EffectiveAction、agents/agent_scope_frontier.py::AgentScopeFrontierAction)拥有值域。并集本身是代码所有权而非注册表数据:production.py::QUOTA_ACTION_VOCABULARIES 与 quota_action_domain() 要求槽位恰好等于这份锚定列表且两臂不相交,否则抛错,因此注册表不能偷偷加入第三条臂。
正路径是:改 owner enum → scripts/generate_semantic_bindings.py 重新渲染 TS 绑定与 glossary(owner 与注册表不一致时拒绝写入)→ --check 在评测/CI 中判定陈旧 → examples/semantic-vocabulary-drift-smoke.py 读取 index 边界、解析 owner 符号、按受控语法扫描生产者形态、执行并集锚点/字面量规则/生产者关系 → loopx canary premerge 会跑这个 smoke。
与既有实现的比较(都在主干上):loopx/semantics/inventory_v0.json 是 carrier 的报告,由 generate_semantic_inventory.py 从源码再生成,它没有 admitted-value 集合、没有 owner 符号也没有生产者关系,表达不了 producer closedness 或 canonical liveness;scripts/generate_coordination_state_contract.py 是"渲染 Python/TS 绑定 + --check + freshness 测试"的同类机制,但 owner 方向相反(JSON 契约持有真源)。M1 这次刻意让 Python enum 当 owner(RFC 的 Q3 早已定案),RFC 也把向该生成器收敛写在 M2 行里,所以这是一处有期限、已披露的机制重叠,而不是第二个决策 owner。
所有权取舍:嵌套 frontier 的重复 effective_action 键被删掉、根槽位保留该值,历史签名文档按字节保留;读者侧只保留一个共享别名读取器,而不是让每个调用方各自决定读哪个键。
具体改动
66 个文件、+3843/−347。按角色拆开看:约 1500 行新测试与 mutation case(14 个文件)、约 770 行文档(RFC 中英、glossary、effect-interpreter packet)、约 1000 行新机制(production.py 219、python_production.py 436、generate_semantic_bindings.py 126、semantic_production_scan.mjs 100、effective_action.py 45、两个 generated TS 93)、38 个文件里约 390 行的机械替换(EffectiveAction.X.value)、约 270 行注册表/inventory 数据,以及 3 行 CI 步骤。按行数绝大部分是行为保持的:机械替换的线上字节不变,注册表 ratchet、生成物、glossary 与新测试本身不改变运行时判决。
关键代码讲解
EffectiveAction(loopx/control_plane/quota/effective_action.py:13):32 个成员的决策值域 owner。改动前 effective_action 在 35 个文件里是裸字符串,注册表只有一份名字清单;现在生产者与消费者 import 这个枚举,线上字段仍靠 .value 保持字符串。关键不变量是成员集合就是值域,而裸字面量即使已注册也被拒绝——这正好回答了"为什么不能顺手写回字符串"。
quota_action_domain(loopx/semantics/production.py:29):并集的代码锚点。它读取注册表的 should_run.effective_action 槽位,要求 vocabularies 恰好等于 ('effective_action','agent_scope_frontier_action'),并要求两臂值集不相交;槽位缺失、被加臂或值重叠都会抛错。这样"一个字段名下三套词表"就变成一个被声明、被检查的离散并集,而不是靠名字约定。
validate_production(loopx/semantics/production.py:147):F1/F2 证据规则。写入形态(dict/assignment/object/keyword*)按并集域校验,函数返回值(return)只按 owner 域校验,因此组合域永远不会放宽 canonical 返回类型;keyword_unproved 不提供任何 liveness 证据;owner 成员本身不能证明生产,未声明的生产者站点与已声明却不再写入的站点都会被点名。它同时返回 unresolved 站点,而不是把它们当成覆盖率。
read_frontier_action(loopx/control_plane/agents/agent_scope_frontier.py:33):v1 只写 action,这个读者优先读 action、回落到 v0 持久化的 effective_action 别名;同时 build_agent_scope_frontier_payload 通过 extra_fields 保留键守卫阻止别名被重新注入。它让"历史签名文档不被改写"有一个可执行入口,而不是靠每个读者自己判断。
interpretTurnResultPacket(loopx/control_plane/effect_program.ts:346):turn result 的 action 槽被固定为 null,判决从 decision(result_kind)读;类型参数 EffectObservation<Decision, Action> 让这个"无动作"在类型上不可被误当成字符串。改动前这里是 truthyString(packet.effective_action) || resultKind,也就是 host 的任意 action 或结果类型都能冒充 quota 决策。
对主干的风险
首要事实(合并门,不是代码缺陷):我评审的 exact head 2c7031721 目前与 origin/main 在本 PR 自己拥有的 6 个文件上冲突——RFC 中英两版、drift smoke、loopx/semantics/inventory_v0.json、loopx/semantics/vocabulary_v0.json、tests/architecture/test_semantic_vocabulary_drift.py。main 在这些文件上比 merge base 多出了 286 行(含注册表 62 行级改动,来自同领域的 #4459)。本 PR 的每个 ratchet 数字(33 → 32、零裸字面量、覆盖率下限、并集槽位措辞)都是在合并前基线上量出来的,而 inventory 是从合并后的 carrier 再生成的。合入 main 后必须重新生成 inventory 与绑定、重跑 drift smoke 与 tests/architecture,再在新 head 上重新请求评审(rebase 会重置本次结论)。这是最强的回归场景,因为一个陈旧注册表会让所有人的 canary premerge 门变红。
P3(非阻断)— RFC 的 M1 退出条件与本次 diff 不再一致。 M1 行的退出证据写着"status/should-run 的 parity fixture 不变",但本 head 修改了 tests/fixtures/turn_envelope_state_matrix.json:operator-gate 用例的 effective_action 由合成值 operator_gate 改为 operator_gate_notify。改动本身正确(主干 decision_summary.py:403-404 早就返回 operator_gate_notify,fixture 此前断言的正是那个从不被生产的合成值),但一条规范性的退出判据与 diff 不再吻合,会让评审者无法照行验收。最小修法:把这一处有意的 fixture 校正写进 M1 的退出证据单元(或挪到已有的"retiring the synthetic operator_gate value" ratchet 行),与 diff 对齐。
P3(非阻断)— 共享别名读者没有被真正共享。 loopx/control_plane/agents/agent_scope.py:532 仍直接读 frontier.get("action"),而同一次 should-run 计算的 should_run_packet.py:1017、work_items/goal_route_hint.py:262 已改用 read_frontier_action,RFC 第 474 行与 glossary 也写明"读者优先读 action,保留旧 v0 别名兜底"。我在 head 上做了反例:把 payload 压成"只带别名"的 v0 形状(正是本 PR 自己的 _legacy_decision(action_present=False) fixture 刻画的那种),_agent_lane_frontier_hint 返回 None;同样的 v1 payload 返回 quiet_noop_blocker(他人前沿等待)或 add_next_advancement + resume_blocked_by_open_monitor。进程内构造的 guard 总是带 action,所以今天是潜在问题;但它一旦成立,投射会静默丢掉整条 frontier hint。最小修法:这里改用 read_frontier_action(frontier),或在注册表注记/文档字符串里写明 v0 记录必定带 action。回归建议:在 examples/control_plane/agent-scope-projection-characterization-smoke.py 里补一个"只带别名"的用例,断言与 v1 得到相同 decision/source/reason_code。
残余风险与证据边界。 drift smoke 明确打印 unresolved_producer_sites=43 (not proven safe),所以 closedness 覆盖的是"已识别形态",不是全程序数据流;注册表自己仍把 consumer-domain closure 标为 advisory、persistence-version 兼容性标为 unproved。本次评审没有真实 Goal 状态、没有真实 host turn、没有老版本二进制写出的 v0 journal,也没有跑远程 CI(该 PR 目前没有 CI rollup),v0 兼容性是通过仓库自带 fixture(固定签名哈希 + 字节稳定 resume)验证的。回滚面是干净的:RFC 写明"回退为字面量;注册表保留集合"。
我的整体评价
观测语义我做了独立复现,而不是只看 diff:把相同合成输入送进真实公开函数,基线(f6a6d1139)的 turn-result 投射对 {wait}、{advance, effective_action: normal_run}、{blocked} 分别给出 effective_action = wait / normal_run / blocked,head 给出 null / null / null;replay observation 由 observe_replay/block_replay 变成不含该键;selection 失败由错误码变成 quota_skip + error_code。在此基础上,head 的 tests/architecture 195 项、聚焦 control-plane 116 项、TS 30 项全部通过,drift smoke 输出 ok(vocabularies 26/26、owner_symbols 49/49、relations 9/9)。我还自己造了两条负向对照:在 tracked 文件里写一个未注册的裸字面量、以及写一个已注册的裸字面量,smoke 都以点名文件与值的 actionable 文案 fail closed 并返回 1(对照后已还原,工作区干净)。
我的判断是:这次改动的体积主要落在测试与文档,机制部分是受控扫描器(不执行被检查源码、不做跨过程推断、把 unresolved 站点摆出来而不是猜),并且复用了主干已有的注册表/smoke/inventory 三件套;唯一新增的机制重叠(绑定生成器)是 RFC 早已计划在 M2 收敛的有期限债务。两条 P3 都不阻断:一条是 RFC 退出行措辞需要与 fixture 校正对齐,一条是别名读者应当在三处共享。没有发现阻断性问题;最重要的证据缺口是合并后必须重跑的那一轮验证——本次 APPROVE 只对 exact head 有效,rebase/更新后需要重新评审。
English verdict: APPROVE — exact head 2c703172174f3e43bcb0f4d371d4a9b83f86ae00 of #4494. No blocking finding: the typed owner, the code-anchored two-arm union, the producer-closedness/liveness guard and the four intended wire deltas hold at this head (195 architecture tests, 116 focused control-plane tests, 30 TS tests, drift smoke ok, and two independent bare-literal negative controls failing closed with named repairs). Two non-blocking P3s: the RFC's M1 exit cell still claims unchanged status/should-run parity fixtures while the operator-gate fixture was intentionally corrected to the value production already emitted, and agents/agent_scope.py:532 still bypasses the shared read_frontier_action alias reader, which drops the frontier hint on an alias-only v0 payload (reproduced at the head). Residual risk: the head conflicts with main on the six semantic-vocabulary files, so the registry, generated inventory and bindings must be regenerated and the smoke plus tests/architecture re-run before merge; this approval is exact-head scoped and a rebase restarts review.
Superseded: this submission carried a shell file placeholder instead of the review text. The full bilingual review is the next APPROVE review on this PR (exact head 2c70317).
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
Signed-off-by: song <liusongstep@gmail.com>
为什么需要这个改动
本 PR 的目标是让后续 PR 能区分“新增控制决策”和“新增诊断信息”,并让语义 CI 给出可修复的反馈。它解决两个具体问题:
effective_action曾混合 quota 决策、frontier 动作、replay 判决和错误码。Turn-result 投射还会把result_kind或 host action 放入该字段,调用者难以判断应该按哪个值域分发。现在判决放decision、诊断放error_code,根动作只保留已登记的 quota/frontier 并集。skip、而实际输出为quota_skip就是具体例子。本 PR 分离 owner、生产者与消费者证据,并修复该不一致。这不是全程序正确性证明。令决策域为 D、前沿域为 F,检查
D ∩ F = ∅后,根动作D ⊔ F可通过值区分来源,无需为所有字符串增加线上标签。生产证据仍是已支持语法范围内的结构性证据,不证明任意变量流或所有分支可达。行为与兼容性
action;读者保留旧 v0 兜底;历史签名计划不在读取时改写effective_action=null,从decision读取判决;不迁移落盘 result/receipt/journaleffective_action=quota_skip;从保持不变的error_code区分原因新增成本,以及 CI 失败怎么修
复用现有 pytest/smoke 作业,没有新增 required CI job,但增加了检查义务和运行工作量。full-public smoke 舰队增加锁定的 TypeScript 解析器依赖安装。本地也需项目支持的 Python/Node;Python 命令统一使用
uv run,不要求可执行文件名为python3.11。error_codeuv run python scripts/generate_semantic_bindings.pyuv run python scripts/generate_semantic_inventory.py输出完整源码树 JSON;需要文件时加--output .local/semantic-inventory.json,报告不入库本地先运行
npm ci --ignore-scripts准备解析器,再执行uv run python examples/semantic-vocabulary-drift-smoke.py --report。不得通过扩宽值集、降低覆盖或放宽预算来掩盖失败。必要性、成本和证明边界已补入现有 中文 RFC §5 和 English RFC §5 的 M1 小节;§10 保留运行与修复命令。不另增加重复长文。
验证与交付边界
Related to #4447. Consolidates the unmerged M0.5 foundation from #4480. M2 generation, remaining semantic debt, legacy retirement and runtime twin migration remain separate; this PR does not close the full RFC issue. No merge has been performed.
English summary: Separate action domains and distinguish production evidence from enum consumption so future changes expose real semantic additions. Preserve historical signed plans, disclose the intentional observation/CLI projection changes, and derive TypeScript bindings from their Python owners. Existing CI jobs enforce the new obligations; bounded syntax checks are not a whole-program correctness proof. The bilingual RFC records rationale, costs and repair guidance.
Q9:取消动态清单的提交义务
根据 维护者反馈,删除已提交的
inventory_v0.json,改为每次从完整已跟踪loopx/树计算一次。owner、生产者、scope、覆盖与预算检查继续使用该结果;不读取旧报告,不改成只扫描 PR diff。词表、锚点、生成绑定和术语表的义务保留。默认生成命令只输出 JSON;
--output <文件> --check只检查明确指定的可选报告。原来的裸--check给出迁移说明。中英文 RFC、测试指南与 review 修复指引已同步;没有新增 required CI job。当前验证状态
当前 head
739535ac8已整合主干f4ed58de9,保留主干全部实际源码改动,并按 Q9 保持结构快照删除。Q9 的 186 项定向测试、独立复审的 218 项测试和真实全树反例均通过(有重叠,不累计);15 项 premerge 加四项基础检查通过。最新主干组合另有 117 项相关测试及 steward 入口 smoke 通过。默认无报告写入、旧/损坏报告不能掩盖跨文件分叉均已验证。该 head 的项目原生 policy5 review 和远端 CI 正在核验;旧 head 的结论不继承为新 head 批准。未执行合并。