Skip to content

feat(semantics): bind three bounded producer forms and share the blocker taxonomy - #4664

Closed
songoow wants to merge 8 commits into
loopx-project:mainfrom
songoow:codex/b2-bind-producer-call-results
Closed

songoow wants to merge 8 commits into
loopx-project:mainfrom
songoow:codex/b2-bind-producer-call-results

Conversation

@songoow

@songoow songoow commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Delivers the remaining half of B2 of #4447: the real effective_action-family candidate sites.

The issue's number was stale

#4447 records the residue as "34 total minus the 15 unprovable by design". Re-measured on 9003577f9 the total is 41, and the breakdown is across every scanned vocabulary rather than effective_action alone. Both RFC mirrors now record the measured split.

Residue by category

blocker before (41) after (40)
annotation_only 5 5
argument_name_only 10 10
attribute_read 2 7
call_result 11 14
other 1 1
typescript_dynamic 8 0
unstable_local 4 3

The eight typescript_dynamic sites are reclassified, not resolved — five attribute_read, three call_result. None of them was resolvable; every one is a property read off a caller-supplied object (decision.effective_action, truthyString(packet.effective_action)). That part of the diff is a taxonomy, not a shrink.

Evidence improves further than the count shows: unresolved rows that carry at least one known value go 2 → 7.

Forms newly bound, and why each is sound

1. Same-module call results. A call to an undecorated, non-generator, plainly-defined top-level def of the same module resolves to the union of that function's own returns. Sound because arguments are never bound to parameters: a returned parameter stays unknown, so the answer cannot depend on the call site and is memoised per module scan. A decorator can replace the returned object, async def hands back a coroutine, a generator yields, and a second top-level binding of the name takes the name away — each keeps the call_result blocker. Recursion is guarded rather than unrolled.

2. Ordered rebinding of a local. A local written more than once resolves to the union of the writes that textually precede the read. Sound because the scan reports syntactic result possibilities, and the union over preceding writes is a superset of what the name can hold there. It applies only when every store of that name is a plain name = expression; loop, with, except, walrus, augmented, unpacking, global and del rebindings are not ordered by this scan and erase the local entirely.

3. Key-precise container writes. A container mutated only through direct literal-key subscript writes keeps its untouched keys; a written key carries the union of its initializer and every write. Sound because a depth-1 literal-key store is the only write that provably lands on a known key. An alias, a method call, a computed or deeper store, a del, or passing the container to any call still discards the whole container, exactly as before. A ** spread of statically known dict literals is flattened so an optional spread no longer hides a sibling key; an unknown spread still makes every key dynamic.

The single site that closes, driver.py::build_loopx_turn_plan:500, needed all three plus the spread flattening at once.

TypeScript. The parser gains the two sound forms the Python scanner already had (|| / ?? arms, a transparent String(x), undefined read as no value) and now reports the same blocker vocabulary, so one residue taxonomy covers both runtimes. typescript_dynamic survives only as the fallback for a form it cannot classify. An owner-member result (enum_result) also carries its reason now; an unlabelled unknown was invisible in the report breakdown.

Left unresolved on purpose

A site this scan cannot bind stays unresolved with its recorded reason. It is never treated as dead. The RFC is explicit that no recognized producer means unresolved, and that defining production as already restricted to the registered set would make the obligation tautological. Shrinking the number by loosening the rule is the failure mode, not the goal.

  • annotation_only (5) — confirmed unprovable by design. All five are bare effective_action: str field declarations carrying no value node at all.
  • argument_name_only (10) — confirmed unprovable by design, with one sharpening: these are unprovable as a production role, not unresolvable as an expression. Four of the ten now carry a fully resolved value set and are still correctly unresolved, because the callee (_execution_obligation and its peers) reads the field rather than emitting it. The honest way to shrink this bucket is a registry call_producers declaration naming a reviewed output builder — a data edit a reviewer sees — never a scanner change.
  • attribute_read (7), call_result (14), unstable_local (3), other (1) — every remaining site bottoms out in something outside this scan's bound: a read off a caller-supplied mapping or object (decision.get("effective_action"), run_decision.effective_action), a call into another module, a returned parameter, or a method chain. Binding any of these needs cross-module or object-field resolution, a separate bounded form with its own blast radius. Not attempted here.

Fixtures

New file tests/architecture/test_semantic_producer_binding.py (43 tests). Every recognized form has a negative twin: a decorated, async, generator, rebound, imported, attribute or recursive callee; an unordered store of each kind; an aliased, escaped, method-called, deeply-stored or del-ed container; an unknown ** spread; and a bound call that must not invent an enum object out of a serialized string. One existing assertion in test_semantic_python_production.py is updated because form 2 deliberately changes it, and a new unordered-store case is added alongside it.

Runtime cost

The scanner runs on every PR touching loopx/. Measured over the 319 Python files it reaches and the 5 producer vocabularies, best of three runs on one tree: 9.20 s → 7.17 s. The deepened scan is net faster because it now reuses the memoised parse and the module-function table across vocabularies instead of re-parsing once per scan.

Validation

  • python3 examples/semantic-vocabulary-drift-smoke.pyok, unresolved_producer_sites=40
  • python3 examples/semantic-vocabulary-drift-smoke.py --report — exit 0, all 40 sites listed with a reason
  • python3 examples/docs-governance-smoke.pydocs-governance-smoke ok
  • /tmp/loopx-pr3668-venv/bin/python -m pytest tests/architecture/ -q378 passed
  • loopx canary premerge --from-git-diffPre-Merge Validation Gate ok: true, 0 failures, 0 advisory failures

Registry values, budgets and the producer site list are unchanged; no site becomes newly visible or unregistered.

RFC pair updated together: Appendix A ledger entry with the before/after split, an Appendix B decision row dated 2026-09-17, a Section 9 validation row, and Section 5's bounded producer model naming the three forms and the shared blocker taxonomy.

Refs #4447 (B2)

🤖 Generated with Claude Code

…ker taxonomy

The B2 residue in loopx-project#4447 was recorded as 34 sites; measured on this tree it is
41, spread across every scanned vocabulary rather than effective_action alone.
This deepens the bounded producer scan by three recognized forms, each with a
negative twin, and leaves every site it cannot bind unresolved with its reason.

Same-module call results: a call to an undecorated, non-generator, plainly
defined top-level def of the same module resolves to the union of that
function's own returns. Arguments are never bound to parameters, so a returned
parameter stays unknown and the answer does not depend on the call site.

Ordered rebinding: a local written more than once resolves to the union of the
writes that textually precede the read, and only when every store of that name
is a plain name = expression.

Key-precise container writes: a container mutated only through direct
literal-key subscript writes keeps its untouched keys; a written key carries
the union of its initializer and every write. A ** spread of statically known
dict literals is flattened so an optional spread no longer hides a sibling key.

The TypeScript parser gains the two sound forms the Python scanner already had
and reports the same blocker vocabulary, so one residue taxonomy covers both
runtimes instead of a single typescript_dynamic catch-all. An owner-member
result now carries its reason too.

Unresolved sites 41 -> 40; unresolved rows carrying at least one known value
2 -> 7. Registry values, budgets and the producer site list are unchanged. The
producer scan is net faster (9.20s -> 7.17s over the 319 files it reaches)
because it now reuses the memoised parse and module-function table.

Refs loopx-project#4447 (B2)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>

@songoow songoow left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Request changes conclusion (author-owned PR; GitHub blocks formal self-review)

结论:REQUEST_CHANGES。 新扫描器把实际会输出未登记值的代码报告为完全解析的 run,并让 validate_production 通过。已复现五类缺陷,覆盖新增的局部绑定、容器写入、同模块调用与 TypeScript 包装形式。现有定向测试通过,不能排除这些缺陷。

  • Exact head: a022005911a3b34d64ce637c16f007abacba337c
  • PR merge base: 9003577f92dc7c2bd6b724b99f0afae8816ca761
  • 审阅命令:git diff origin/main...HEAD;merge base 如上,七个文件的全部 diff 已读,包括两份 RFC 与既有测试的修改。
  • 基线复现运行于 7006b62a52f9f9f00cf8249081a8927319dc3f48;三个被测扫描器文件与 PR merge base 的内容完全相同,git diff 为空。
  • 远端只读复核仍为该 head。检查结果为 23 SUCCESS、4 SKIPPED,没有失败或 pending。跳过项是 deploy、upload-release、publish-pypi、presentation;不把 skipped 当成已执行通过。

动机

#4447 的 B2 要求是:把真实写入/返回绑定到词表与槽位;动态、别名、外部来源须显式 unresolved;两个运行时都有正反例;普通消费者改动不用逐次登记。减少未知位点的数量只是可能的结果,不能取代这个契约。

PR 的共享阻塞原因、复用解析缓存以及解析简单局部表达式都具有维护价值。问题出在新的“已知”结果不可靠:同一源码在基线保留 unknown,在 head 丢失实际输出,形成更强但错误的结论。因此目前不能认证“剩余 B2 已交付”,也不应据此删除词表值。

作者记录的 41→40 未解析位点、2→7 带已知值的未解析位点、9.20s→7.17s,是作者的定向测量;本次没有独立重复作者的全量成本对比。这里的反对意见不基于 PR 数、LOC 或仅减少一个位点。

改动思路

调用链与边界是合适的:现有 CI/维护者 smoke → check_producerscollect_production → Python AST / TypeScript parser → validate_production。注册表声明词表 owner、输出字段、经过评审的 return/call 位点;解析器输出诊断事实;validator 检查登记值与生产者。没有新增能力目录、命令、持久化状态或运行时决策权。

复用方面,_parsed 使用现有缓存有充分理由,Python/TS 语法解析分别保留也合理。应收敛的是同一容器的读取规则:新增 flatten 直接读旧初始化 AST,绕开 element 的 mutation map,造成 F3。把两个函数放在同一文件没有解决重复语义。

更小的可交付方案是先保留缓存与原因分类,对不能保守证明的形式继续输出 unresolved,再用现有显式 return/call 声明或固定输入 witness 验证一个重要的实际生产路径。没有证据表明必须增加通用跨模块/对象字段推理才能达到当前 B2 的有用边界。

具体改动

全 diff 分类:三个分析文件;两个测试文件;英中两份 RFC。新增内容主要是 scope environment、同模块 return memo、局部赋值/按键写入合并、spread 展开及 TS 原因标签。没有修改词表值、预算、生产者列表或 M2 生成文件。

关键代码讲解

  1. loopx/semantics/python_production.py:243 _module_functions:建立可绑定的顶层函数表,供 same_module_call 使用。未装饰、非 async/generator 及顶层唯一并不足以保证调用身份;F4 展示函数体内 global 写入的反例。
  2. loopx/semantics/python_production.py:358 environment:构建局部定义、容器写入和 resolver。其 boundflattenelement 必须共同保持“已解析集合不丢失可能输出”的不变量;F1–F3 不满足。
  3. loopx/semantics/python_production.py:654 call_values:按函数和模式记忆化 return 集合,不绑定实参。该选择控制了分析范围,但仍依赖正确的被调方身份与局部环境。
  4. scripts/semantic_production_scan.mjs:39 values:新增 Stringundefined||/?? 处理及原因标签。这也影响 literal_uses 的辅助扫描,不能只检查新的生产行。
  5. loopx/semantics/production.py:152 validate_production(未改动的承接方):known 值参与登记/活性判断,unknown 部分不提供证据。新 resolver 的错误因此会直接变成 CI 错误接受,而不只是显示文案问题。

对主干的风险

以下 Python 片段均可作为受扫描根内 probe.py::emit 的源码,field 为 action,登记值仅 run。通过真实的 collect_productionvalidate_production 执行;同时独立执行片段观察实际输出。没有 mock 掉解析器或预先供应期望结果。

F1 — [P1] 文本先后不能代表循环中的执行先后

位置:loopx/semantics/python_production.py:486–491,同时涉及 environment 对循环体普通 Assign 的收集。

def emit():
    choice = "run"
    output = []
    for i in range(2):
        output.append({"action": choice})
        choice = "escape"
    return output

实际先输出 run、再输出 escape。head 只报告 rununresolved=false,validator 返回空诊断并接受;基线保留 unstable_local。循环后的写入在下一次迭代先于读取发生,lineno 过滤漏掉它。

最小修复:对有循环携带赋值的局部变量保持 unresolved,或做明确有界、保守的 backedge union。回归测试须覆盖 for/while 的“先读后写、下次迭代再读”,不能只测循环 target 本身被重绑。

F2 — [P1] 原始 list 下标不是独立的键

位置:loopx/semantics/python_production.py:555–559;初始化值查找在 :532–535

def emit():
    packet = ["run"]
    packet[-1] = "escape"
    return {"action": packet[0]}

实际输出 escape,head 完全解析为 run 并接受。lookup 依照 Python list 索引取初始化值,写入表却按原始 -1/0 区分,遗漏同一元素的写入。基线保持 unstable_local

最小修复:先把精确键写入限制为 dict,或在长度可证明且不变时统一正负索引;长度/别名无法证明则 unresolved。双方向索引应有反例。

F3 — [P1] spread 读取旧初始化值,丢失已记录的字典写入

位置:loopx/semantics/python_production.py:514–525

def emit():
    overrides = {"kind": "run"}
    overrides["kind"] = "escape"
    packet = {**overrides}
    return {"action": packet["kind"]}

实际输出 escape,head 报告 run 且无 unknown;基线报告 dynamic_key。这里不是要求无限数据流分析,而是 PR 明确支持的两种形式组合后失效。flatten 绑定到 overrides 的 Dict 初始化 AST 后,完全没有合并其 written 项。

最小修复:使用同一个 mutation-aware 容器读模型,或让被修改局部变量的 spread 保持 unresolved。不能新增另一套独立的 key/mutation 真相。

F4 — [P1] 同模块 global 重绑定使被调方身份失真

位置:loopx/semantics/python_production.py:257–262

def pick():
    return "run"
def install():
    global pick
    pick = lambda: "escape"
def emit():
    install()
    return {"action": pick()}

实际输出 escape,head 报告 run 并接受;基线 call_result_module_functions 在遇到顶层函数时跳过函数体,因而把可达的 global 写入漏掉;不绑定参数无法补足被调方身份。

最小修复:同模块任何函数体对候选名的 global 写入/删除,都保守地使其失去静态调用绑定资格。无需为此建立任意副作用分析器。

F5 — [P1] TypeScript 的同名标识符和 String 转换不是透明语法

位置:scripts/semantic_production_scan.mjs:43–55

function emit(String) { return {action: String("run")}; }
emit(x => "escape");

真实 Node 输出 escape,head 却生成 rununresolved=false,validator 接受。同一根因下,function emit(undefined){return {action:undefined};} 传入 "escape" 会变成“完全解析、空值集”;真正的 String(null) 也会变成空集,而实际输出字符串 "null"。这两者在有其他正常 run 生产者时可以静默消失;本复现的单生产者词表仍会因缺少 run 报错,因此不把它们单独描述为已经通过全部 validation。

最小修复:先证明标识符绑定,再把已证明为字符串的参数视为透明 String 包装;null/undefined 转换须正确建模或保持 unresolved。必须补 TS 正反例,不能因 Python 存在类似形式就直接宣称可靠。

语义与 CI 对齐

此 PR 重用既有词表;没有理由扩大登记值、调预算或缩 scan root。被违反的是 F1/F2 所依赖的“已知/未解析”证据边界。两份 RFC 的 soundness 叙述应随实现修复。有限扫描不是全程序证明,但这个限制不能豁免在自己宣称支持的表达式上遗漏已执行输出。

已验证结果:

验证 结果与界限
#4664 三个定向 producer 测试文件 152 passed,24.98s;现有反例缺少上述交互
相同合成源码,基线/head parser + validator + 独立 Python/Node 执行 基线保守包含/unknown oracle 全通过;head 四个 Python、三个 TS case 失败,归为五类 blocker
M2 生成/控制器/投影定向测试(7006b62a5) 194 passed,19.47s
M2 generated --check up to date
M2 decide_managed_step 合成失败 journal 测试 21 passed,1.07s
远端 exact-head CI 23 SUCCESS、4 SKIPPED;没有 pending/failure;未重新执行整个 CI 或审计 branch protections

本次未改变 frontend/Lark 或用户 CLI 流程,故不需要 UI companion 修改。新增行为只在开发者分析工具和其 CI 调用内。没有 opt-in 开关、权限/协调 actor 生命周期或 hard obligation 被伪称 guidance。本 PR 未在本次本地复跑全量产品测试;已读取当前 head 的远端结果。

我的整体评价

目前应退回修复。已有解析缓存与原因分类是可保留的价值;不足之处是把尚不成立的推理提升成硬 CI 证据。小范围恢复 unknown,比围绕每个反例继续堆完整解释器更符合当前项目的维护成本。修复后须重跑整个 PR 的定向正反例与不变量,而不是仅把这几个输出写进测试期望。

M2 的评估应与这些新缺陷分开:生成 enum 的 Python 对象身份、TypeScript settlement 对结果集的采用、controller 对生成表的采用均已验证。JSON 持有有限规则,现有 Python controller 执行规则,TS 导出数据/类型,没有发现新造的第二套 controller 决策实现。保留兼容 re-export 是合理的;它们共享对象而非复制知识。

M2 的证据边界也应明确:在整个 loopx/ 调用搜索中,decide_loop_disposition 的唯一生产调用位于 managed_step.py:317,其入口在 :295–298 限定 failed + host_failure journal。普通初始 turn、progress、completion 的 controller 分支有直接函数级测试/witness,但这不等于全部已接入一个运行中的统一 loop。这个结论不否定“发布共享决策契约”这一 M2 阶段;它否定把该阶段描述为全产品运行权威已收敛。settlement.ts 目前采用的是生成结果集,不是 TS controller evaluator。

**最重要的下一步:**先修复/收窄 B2,使 unknown 的边界可信;随后依照既有控制面迁移方向,选择一个会实际发生的 Turn continuation 路径,写出规范输入、唯一决策 owner、真实调用方及拒绝/重放验收,通过真实入口证明采用并移除同一路径的重复规则。需要的结果是下一次政策变化只改一个拥有者、真实用户路径保持兼容,而不是继续扩展扫描语法或新增词表计数报告。不要只为减少 Python/TS 同名文件或增加已覆盖表项而引入第二个 evaluator。

English verdict: REQUEST_CHANGES at a022005. The exact head introduces five reproducible false-known producer bugs: loop-carried assignments, aliased negative list indices, spreads of mutated dictionaries, same-module global callee rebinding, and TypeScript String/undefined binding or conversion. The public scan/validation API can accept a registered run result while actual execution emits escape. Existing focused tests and CI are green but miss these interactions. Keep caching and actionable blocker labels; conservatively retain unresolved for unproved forms. M2 generation and real enum/table adoption are useful and verified, but complete runtime-loop adoption is not established by the direct witnesses. Prefer one real continuation path with one decision owner over further generic inference machinery.

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed exact head: a022005911a3b34d64ce637c16f007abacba337c (4664@a022005911a3b34d64ce637c16f007abacba337c).

动机

这是 #4447 里 B2 的后半段,目标是把"扫描器放弃"和"这里确实没有生产者"分开。旧行为是:一个名字写两次、一次同模块调用、一次字面键容器写入,全都只能报 unresolved 且不给任何值;TypeScript 侧的 8 个未知点则统一压在一个 typescript_dynamic 标签下。作者把 issue 里"34 减 15"的旧数字重新测定为 41,并逐类给出分解,这一点我在 exact head 上独立复现了,数字逐项吻合(41 → 40、unresolved-with-values 2 → 7、TS 8 点分成 5 个 attribute_read / 3 个 call_result)。所以这不是一个"为了缩小残余而放宽规则"的改动,而是一个有真实增量的切片:唯一收敛的点是 loopx/control_plane/turn_driver/driver.py::build_loopx_turn_plan:500。我判它是 justified_increment,而不是"完成 B2"——attribute_read=7call_result=14unstable_local=3other=1 仍然在,annotation_only=5argument_name_only=10 按设计不可证。

改动思路

入口没变,仍然是 loopx/semantics/production.py::collect_production 按 registry 声明驱动 scan_python_production,消费方仍然是 drift smoke 与触碰 loopx/ 的 PR 路径;权威状态依旧是"代码域内的受控扫描范围 + registry 声明",没有任何持久化。真正的决策所有者还是 loopx/semantics/python_production.py,这次的形状对:它没有新开模块、没有第二权威、没有新 capability,而是把同一个模块内的绑定规则做深,并且把旧机制替换掉(删掉了单值/完整 if-else 那套 conditional_values 与本地的 index_value),不是叠一层。三条新形状各自都有边界条件:同模块调用要求"未装饰、非 generator、模块顶层同名只绑定一次",并把同一张表按 (name, mode) 记忆化;本地重绑定要求"该名字的每一次写入都是普通 name = expression",读只取文本上先于它的写入的并集;字面键容器只承认 depth-1 的字面键写入,别名、方法调用、计算键、更深层写入或把容器交给任何调用一律作废。TypeScript 侧不是"再写一个解析器",而是把既有解析器的未知标签对齐到同一套词汇,并补上 ||/??/String(x)/undefined 这几个 Python 侧早就有的形式;production.py 只做 r.get('blocker') 的适配,typescript_dynamic 退化成兜底。性能上我也复现了方向:隔离 load_sources 后,6 个 vocabulary 的 producer scan 最好成绩 4.29s → 3.01s,原因是解析结果与模块函数表被跨 vocabulary 复用。

具体改动

+852/-104python_production.py +335/-91 是主体,production.py +3/-1 只改一行映射,semantic_production_scan.mjs +31/-6,新增 300 行 fixture 与 7 行补充断言,两个 RFC 镜像各 +101/-3、+75/-3 记录测量账。没有 registry 值、owner、budget、producer 站点列表的变化;我按 base/head 各 dump 了 157 行 (site, line, form, values, unresolved, blocker) 做对照,行清单完全一致,20 行变化里没有任何一行丢掉值。

关键代码讲解

environmentloopx/semantics/python_production.py:347):把"一个作用域的有限本地视图"从每次重建改成缓存一次,并集中推导 plain 写入表、definitions(要求 assigned[name] == len(plain[name]),且不是参数、不是 global/nonlocal 声明、不是 del)、字面键写入表 written 与别名/逃逸失效集。这是所有新形状的共同前提,也是这次唯一值得担心的地方(见下)。

definitionsbound:388):一个被多次写入的名字,读时取"文本上先于该读"的写入并集,_union 把它们折成一个新造的 IfExp(test=True, ...) 选择节点——resolve/enum_object_value 本来就理解 IfExp,所以不需要改动解析树。收敛的那个真实站点正是这条规则(route 在 395/409 两次普通写入,中间没有循环),它的并集是真超集,判断正确。

call_values / same_module_call:641):只解析"同模块、未装饰、非 generator、名字只被顶层绑定一次"的 def 的自身 returns,三种 mode(value / enum / enum object)分开记忆化,关键在于参数永不绑定到调用点实参,所以答案与调用点无关、可安全缓存;Action.RUN.value 这种已经序列化的字符串在 object mode 下正确地返回空;递归用 resolving 挡住而不是展开。作者明确拒绝了"把实参绑到形参",理由是对的方向。

flatten / lookup / element:493):字面键写入变成 key-precise,** 展开只承认"静态可知的字面 dict 字面量",未知 spread 退回整键未知;absent_ok 让"只有写入提供这个键"不再被误判成未知边界。别名、计算键、del、方法调用、把容器传出去,仍然是整容器作废——这一点我逐条验过,方向是保守的。

blockerFor / merge / valuesscripts/semantic_production_scan.mjs:19):把 8 个 typescript_dynamic 拆成 attribute_read/call_result/unstable_local/dynamic_key,并新增 ||(并集)、??(有限选择)、String(x)(透明包装)、undefined(无值、不算未知)。

对主干的风险

P2(需要修复):有序重绑定规则在"同一可重复块的先读后写"上会把 unresolved 错报成 resolved。

bound 只保留 (lineno, col_offset) 早于该读的写入,于是同一 for/while 体内、位于读之后的普通写入被丢掉,而它在下一轮迭代可能已经生效。最小复现(函数作用域,return_paths=('action',)):

from .owner import Action
def emit():
    choice = Action.RUN.value
    for _ in range(2):
        packet = {"action": choice}
        choice = Action.WAIT.value
    return packet

exact head 报 values=['run'], unresolved=False, blocker=None;base 9003577f9unstable_local;而第二轮迭代这个读实际看到的是 wait。模块作用域的 dict 形式同理。这正好是该文件 docstring 声称要防的假证据方向——"union over preceding writes is a superset" 这句论证在可重复块里不成立。当前主干没有受影响:唯一收敛的 driver.py::build_loopx_turn_plan:500 没有外层循环,并集为真超集;所以这是一个未被夹具保护的静默漏洞,而不是已经算错的数字。修法很小:把"该名字存在位于可重复块内的普通写入"(等价地:要求所有先于该读的写入与读处于同一顺直块)排除出有序重绑定规则,让它回到 unstable_local;同时补上函数/模块两个作用域的负向夹具,并把 docstring、RFC Section 5 与两处 Appendix A 的措辞对齐到实现。新夹具文件自己的 docstring 写的是"每个被承认的形状都要有负向孪生兄弟",这条恰好缺席。

P3:_MODULE_FUNCTIONSid(tree) 做模块级全局缓存。 它的正确性依赖 _TREES 永不淘汰这一远处不变量;一旦 _TREES 将来被加上内存上限,复用的 id 可能返回另一个文件的函数表,把调用静默绑到错的被调用者。建议改成和 _TREES 一样按 (path, hash(text)) 键,或放进每次扫描的状态里。

P3:_is_generator 会走进嵌套 def。 一个只"包含"嵌套 generator、自己并不 yield 的顶层 def 会被当成 generator 而不再被绑定(实测该调用点保持 call_result,同形状去掉嵌套 generator 后解析出 ['run'])。方向保守、不会造假证据,但它会把这轮想变可行动的残余重新推高,与 docstring 的措辞也不一致。

P3:blockerFor 把对象/数组字面量与模板表达式映射为 dynamic_key,而同一份 docstring 把 dynamic_key 定义为"计算键或非字面量下标"。既然这次的核心主张是"两个 runtime 共用一套 blocker 词汇",这个标签会让读残余分解的人得到错的原因说明。当前树上没有站点落在这个标签上,所以是分类一致性小问题。

其他已核对的点:负向孪生覆盖了装饰器/async/generator/重复绑定/导入调用/递归、别名与逃逸容器、未知 **;registry 值、owner、budget、producer 列表未变;enum_result 现在也带原因,不再是不带标签的未知;claim 的"41 总 / 8 点分类 / 41→40 / 2→7"我逐项复现。27/27 检查全绿,但如上所述,绿 CI 与夹具全过并不能证明这条被主张的边界。

我的整体评价

方向和形状都是对的:在同一个所有者里做深、用替换而不是叠加、每条形状都带边界和负向孪生、把测量账写进两个 RFC 镜像,并且性能还净变快。我不认为需要缩小这个切片,也不认为应该把这三种形状拆开分别合。

但"有序重绑定"这一条的正当性论证是可被证伪的,而它恰好是这次唯一让残余降到 40 的规则。在一个专门用来拒绝假生产者证据的扫描器里,把 unresolved 错报成 resolved 属于方向性错误,即使今天只在潜在站点上成立。请按上面的最小修法处理并补上负向孪生,同时把 P3 里 _MODULE_FUNCTIONS 的缓存耦合一并收紧(那一处是同一模块内的顺手改动);_is_generatorblockerFor 的标签问题可以一句话改掉或写进 docstring。

English verdict: REQUEST_CHANGES — the three bounded forms, the shared blocker taxonomy and the RFC ledger all check out and reproduce exactly (41→40, unresolved-with-values 2→7, TypeScript 8 split 5 attribute_read / 3 call_result), but the ordered-rebinding rule reports a site as fully resolved (values=['run'], unresolved=False) when a later plain write in the same re-executed block is reachable at that read; add the re-executed-block carve-out plus its negative twin and align the docstring/RFC wording before merge.

songoow and others added 3 commits September 17, 2026 21:40
…er write

The multi-write form resolved a local to the writes that textually precede the
read. Textual position is execution order only where no back edge crosses it,
and the filter did not look for one. A local written at the bottom of a loop
body and read at the top resolved to the value written before the loop, and the
site reported `unresolved=False` with no blocker.

That is the one failure mode that turns an unknown into wrong evidence instead
of into a smaller residue. F1 asks whether a producer writes only registered
values; a producer that emits an unregistered value on every iteration after
the first passed it, because the scan had reported a closed value set that was
not closed. Four shapes reproduce it: a `for` back edge, a `while` back edge, a
write carried by an outer loop, and a `finally` that rebinds.

A negative subscript store had the same shape. `table[-1]` names the same slot
as some non-negative index whose number depends on the container's length, so
recording it under the key `-1` left a read of `table[0]` looking at an
initializer the write had already replaced; a one-element list reported the
overwritten value and called the site resolved.

Both now stay unresolved. The name is not a finite selection when a write
shares an enclosing loop with the read, and a negative store sends the
container down the existing invalidation path, so the answer is
`unstable_local` rather than a value the code does not produce.

Measured on the tree: unresolved sites stay at 40 and the blocker split is
unchanged at `annotation_only=5, argument_name_only=10, attribute_read=7,
call_result=14, other=1, unstable_local=3`. No site was resolving through the
unsound path, so the generality bought nothing that this takes away. Two
positive tests hold the ordering the form was built for: a straight-line
rebinding still resolves, and a single write inside a loop is still its only
value.

Refs loopx-project#4447 B2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>

# Conflicts:
#	docs/architecture/rfcs/semantic-vocabulary-convergence-v0.md
#	docs/architecture/rfcs/semantic-vocabulary-convergence-v0.zh-CN.md
Both RFC mirrors took an append cluster from concurrent PRs. Appendix A and
the Appendix B decision table keep both sides' entries: B0's formal-signature
work from main and this branch's B2 entry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
huangruiteng
huangruiteng previously approved these changes Sep 18, 2026

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

上一轮我在 a0220059 上请求改动,唯一阻塞项是"有序重绑定"规则的不健全:它只保留文本上位于读之前的普通写入,于是同一循环体内、位于读之后的写入被丢掉——而它在下一轮迭代已经生效。结果是扫描器把一个不封闭的值集合报成完全 resolved。在一个专门用来拒绝假生产者证据的扫描器里,这属于方向性错误:把 unknown 变成了错误证据,而不是一个更小的残余。

这个 head 修的正是它,并顺手把同形状的负下标容器写入一起收口。

改动思路

  • 回边:一个名字只要有普通写入与读处于同一个包住它的循环里,就不再做"按文本顺序取并集",直接留 unknown(blocker unstable_local)。文件自己的措辞也跟着改了:文本位置只在没有回边穿过时才等于执行顺序。
  • 负下标table[-1] 指的是"编号依赖容器长度"的那个槽位,之前把它记成键 -1,会让 table[0] 的读看到一个其实已被覆盖的初始化值;现在负下标直接把容器送进既有的失效路径。
  • 两种情形都补了负向孪生(这正是上一轮要求的最小修法),并保留两条正向测试守住当初被构建出来的顺序能力。

具体改动

关键代码讲解

  • loopx/semantics/python_production.py::bound / definitions:保留限制条件从"每个写入都是普通 name = expression"追加为"且没有任何写入与该读共享外层循环";读不到就落到 unstable_local 而不是给一个看似封闭的值集。
  • loopx/semantics/python_production.py::flatten / element:负下标的键写入不再按 -1 记账,而是让容器走已有失效路径,所以 table[0] 的读不会拿着被覆盖的初始化值当答案。
  • tests/architecture/test_semantic_producer_binding.py::test_a_loop_back_edge_leaves_the_local_unordered:四种形状全覆盖——for 回边、while 回边、被外层循环携带的写入、finally 重绑定;断言里明确要求泄漏值不出现在 known 集合。
  • 同文件的正向两条(直线重绑定仍解析为 {run, wait}、循环内单次写入仍是唯一值)保证这次收紧没有把规则整体收回。

对主干的风险

无阻塞发现。复核证据:

  • 我上一轮给出的最小复现场景(读之后在同一个 for 体内写 choice)在新 head 上从 values=['run'], unresolved=False, blocker=None 变成 unresolved=True, blocker='unstable_local',且不再给出任何值——正是要求的行为。
  • RFC 记录的数字我按 head 复算方向一致:unresolved 站点仍为 40,blocker 分解不变(annotation_only=5, argument_name_only=10, attribute_read=7, call_result=14, other=1, unstable_local=3),也就是说树上没有站点是经由这条不健全路径解析出来的——这次收紧没有拿走任何真实收益。
  • 测试:test_semantic_producer_binding.py 51 passedtest_semantic_python_production.py 76 passedtest_semantic_vocabulary_drift.py 100 passeddocs-governance-smoke ok;RFC 两份标题数 46/46 对齐。

仍未处理的是上一轮的三个 P3(都非阻塞,也都不影响这次修复的正确性):_MODULE_FUNCTIONSid(tree) 做缓存键(依赖 _TREES 永不淘汰)、_is_generator 会走进嵌套 def、blockerFor 把对象/数组字面量标成 dynamic_key。建议后续小改一并收掉。

CI 方面本 head 有 10 项通过、kernel-static-checks 与四个 test-shard 在我审时仍 pending、无失败。

我的整体评价

修的正是被指出的那一条,而且修法与请求一致(收紧 + 补负向孪生 + 同步文档与中文镜像),并在同一模块内完成,没有新增权威、没有新增 blocker 词、没有牺牲原有的正向能力。"宁可留 unknown 也不给错误证据"正是这份扫描器存在的意义,因此可以接受。

English verdict: APPROVE - head c73aaed closes the unsound resolution the previous review blocked on: a plain write sharing an enclosing loop with the read now keeps the local unresolved (blocker unstable_local) and a negative-index store discards the container; the old reproducer flips from values=['run'], unresolved=False to unresolved with no values, four negative twins plus two positives pin the boundary, and the residue stays at 40 sites with an unchanged blocker split, validated by 51 + 76 + 100 passing tests, docs-governance ok and EN/ZH RFC parity, with the three earlier P3 items still open and non-blocking.

…al interpreter

B2 of loopx-project#4447 asks for bounded producer identification and says dynamic,
aliased, external and unprovable paths must stay explicitly unresolved. The
first cut of this slice went the other way: it grew python_production into a
local abstract interpreter -- ordered local rebinding, container-mutation
tracking with alias invalidation, ** spread flattening, same-module call
resolution -- plus String()/undefined special cases in the TypeScript scanner.
It bought one site, 41 unresolved to 40.

Review reproduced five confidently wrong "fully resolved" verdicts from it: a
loop whose second iteration emits a value the scan never sees; a write through
container[-1] missed by a read of container[0]; a dict mutated after
construction then spread with **, read back from its stale initializer; a
helper rebound through global, still attributed to the module-level def; and a
TypeScript parameter shadowing String, still treated as the builtin conversion.
For a gate that decides whether code is safe, a confident wrong answer is worse
than an admitted unknown, so every one of those inferences is removed and both
scanners keep exactly the bounded syntactic reach they had on main.

What is kept is the part that was actually worth having: every unresolved site
now carries a specific, actionable reason from one taxonomy shared by both
runtimes. The TypeScript scanner's single typescript_dynamic catch-all becomes
attribute_read / call_result / unstable_local / dynamic_key, with
typescript_dynamic left only as the fallback for a write the parser cannot
classify; a ??/|| fallback reports the reason of the operand that could not be
read. enum_result rows carry their reason too. A label narrows nothing: no
value set and no unresolved flag changes, and that is asserted.

Unresolved sites go back to 41 with a site list identical to main's. The one
site the interpreter closed, driver.py::build_loopx_turn_plan:500, needed
container-mutation tracking to read a payload dict through subscript writes it
could not order, so it was never proven. No budget, ratchet or anchor moved.

The five reproductions stay as tests, now asserting the conservative outcome:
each construct stays unresolved with its blocker. Both RFC mirrors are narrowed
to the delivered scope, record the rejected attempt, and add evidence row E25.

Refs loopx-project#4447

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
…reduction

bf7a7cc patched two of the five unsound "fully resolved" verdicts in place --
a loop back edge and a negative-index store -- while keeping the local
interpreter. This branch removes the interpreter instead, so the code those
rules guard no longer exists and the scanner, its docstring and the RFC
sections describing the bound resolve to the reduced versions.

Its evidence is kept, because it strengthens the case for removal: the patch
moved no number on the tree, and three of the five verdicts were still wrong
afterwards. The four loop shapes it pinned (for, while, outer-loop carried,
finally) and its positive twin are folded into
tests/architecture/test_semantic_producer_binding.py as conservative
assertions, and both RFC mirrors and evidence row E25 now record the patch
attempt and what it did not buy.

Refs loopx-project#4447

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
huangruiteng
huangruiteng previously approved these changes Sep 18, 2026

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

B2 要的是有界的生产者识别:动态、别名、跨模块、不可证的路径必须显式保持 unresolved。这个 head 是对本 PR 自身前一个 head(c73aaedb,我在 2026-09-18T01:56Z 批过)的收回:那条路把 python_production 长成了一个局部抽象解释器(有序重绑定、容器键精确写入、** 展开、同模块调用解析,外加 TS 的 String()/undefined 特例),代价换来把残量从 41 降到 40——只关掉一个位点

作者复现了那个解释器给出的五个"已完全解析"的错误结论:循环携带的写入、codes[-1] 写入被 codes[0] 读到、构造后被改写再经 ** 回读的 dict、经 global 重绑定的 helper、以及参数名为 String 的 TS 位点。对一个决定"代码是否安全"的门禁来说,自信的错答案比承认不知道更糟,所以这些都删掉了。

改动思路

  • 保留真正有价值的一半:每个 unresolved 位点都带上一个具体、可行动的理由,且 Python 与 TypeScript 共用同一套标签。
  • TS 侧原本的单一 typescript_dynamic 兜底拆成 attribute_read / call_result / unstable_local / dynamic_key??/|| 报"读不出来的那个操作数"的理由,typescript_dynamic 只留作解析器无法归类的兜底;enum_result 行也带上理由。
  • 标签只命名障碍,不缩小残量:这条被写成断言而不是散文。
  • 五个复现变成回归测试,断言的是保守结果(仍 unresolved 且带理由),而不是值集合。

具体改动

关键代码讲解

  • loopx/semantics/python_production.py:相对 main 只多 15 行、少 2 行——一段 docstring(说明标签是理由不是值、typescript_dynamic 只是 TS 兜底)和 enum_result 的理由赋值。解释器的全部符号(_MODULE_FUNCTIONScall_memo_union_index_valuecall_values)都已删除,rg 无残留。
  • scripts/semantic_production_scan.mjs:新增 blockerFor,只做分类;values() 不再对 ??/|| 做合并,String()/undefined 特例被撤掉。
  • tests/architecture/test_semantic_producer_binding.py:12 个测试 / 218 行,含五个复现与"未绑定位点永远不被静默丢弃"这条不变量。

我独立复现的数字

main d8e7af141 : unresolved=41  unresolved_with_values=2
head 65a87e6e  : unresolved=41  unresolved_with_values=2
site list      : 41 条,排序后 diff 为空(与 main 逐字一致)
head 分布      : annotation_only=5, argument_name_only=10, attribute_read=7, call_result=14, other=1, unstable_local=4, typescript_dynamic=0

pytest tests/architecture/ 386 passedsemantic-vocabulary-drift-smoke ok(打印 unresolved_producer_sites=41 与上面分布一致);docs-governance-smoke ok。RFC 两个语言镜像都已收窄到交付范围,并新增证据行 E25(记录"解释器换来一个位点、五个错答案",以及 bf7a7cc41 那次补丁只修掉前两个)。

对主干的风险

代码层面没有阻塞发现:这个 head 比它自己的上一版更保守,残量、位点集合与 main 完全一致,唯一变化是每行带上了理由;标签本身不改变任何 value set 或 unresolved 判定。三个非阻塞项:

  1. [P2] PR 描述还停留在被撤回的那一版。 描述最后编辑于 02:24:33Z,表格仍写 after (40)、章节仍叫 "Forms newly bound",通篇没有 "interpreter / rejected / reproduc" 的任何字样;而 head 绑定的是零个新形式、残量 41、位点表与 main 逐字相同。RFC 镜像是对的,但维护者合之前读的是描述——合并前请把描述改到与 head 一致。
  2. [P3] 对象/数组/模板表达式仍被标成 dynamic_key,而共用词汇把该标签定义为"计算键或非字面量下标",Python 对同类形状给的是 other。同一作者的 #4682 正是把这条改成 other,随本 PR 或之后落地即可让"两个运行时一套标签"的说法成立。
  3. [P3] 本 head 的必需检查 kernel-static-checks 又在 15 分钟上限被取消(job 105459876044:02:37:01Z 起,02:52:16Z 止 = 15m15s,conclusion=cancelled),随后 checks/pytest/merge-gate 因为上游 cancelled 一起报红。这是今天第三次(#4663 15m04s、#4680 15m16s、本 head 15m15s),而 #4682 的同一作业是成功的——属于容量/上限问题,不是本 PR 的缺陷,但会让所有 PR 的必需检查不稳定。

我的整体评价

这是主动收回的正例,也是这个评审循环里少见的"变小"的改动:上一版把 45 行生产代码换成 464 行的局部解释器、换来一个位点,这一版把解释器连同它的 342 行"正向"测试一起删掉,只留下标签与五个反例。对门禁类扫描来说,方向是对的——承认不知道优于自信地答错。

我没有找到代码缺陷;要收尾的是三件读起来才知道的事:把 PR 描述改回与 head 一致、让 dynamic_key/other 的标签在 #4682 里对齐、以及把那条 15 分钟的 CI 上限当成独立的容量问题修掉(提高上限或把 CLI 输出 smoke 拆成独立 job)。可以接受,等描述更新后即可合并。

English verdict: APPROVE - head 65a87e6 withdraws this PR's own earlier local interpreter (ordered rebinding, container-mutation tracking, ** spread flattening, same-module call resolution and the TypeScript String()/undefined special cases) after five reproductions showed it reporting confidently wrong fully-resolved verdicts, keeps only a shared blocker taxonomy that labels the residue without narrowing it, and returns the scanners to main's reach: I reproduced main's numbers exactly (41 unresolved, 2 unresolved-with-values, and a sorted 41-id site list identical to main), with 386 architecture tests passing, the drift smoke printing the claimed split and exiting ok, docs-governance ok, no leftover interpreter symbols, and both RFC mirrors narrowed with evidence row E25; three non-blocking items remain - the PR body was last edited at 02:24:33Z and still sells the withdrawn version (residue 40, "forms newly bound"), object/array/template literals are still labelled dynamic_key which the shared taxonomy defines as a computed or non-literal subscript (the author's #4682 changes it to other), and the required kernel-static-checks job was cancelled at its 15-minute cap for the third time today (15m15s here after 15m04s on #4663 and 15m16s on #4680), which is a CI capacity defect rather than a property of this diff.

Both mirrors conflicted on the Appendix A append cluster: this branch's ledger
entry against main's entry for the cross_runtime value notes, which landed with
huangruiteng#4662. The two are disjoint additions, so both are kept, ours first. Nothing
from main's version of either mirror was dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>
@huangruiteng

Copy link
Copy Markdown
Collaborator

CI readback for this head (added after the review was published): gh pr checks 4664 now reports kernel-static-checks pass (7m58s), checks pass, dashboard-acceptance pass, node-minimum/forward-compatibility pass, dependency-review pass and Sign-off pass, with test-shard jobs still pending when I read the rollup. So the "no rollup available" note in the review above was read from the review packet before that run reported, and the earlier 15-minute cancellation of kernel-static-checks was a property of the previous head, not of 43b6f6a1. Merge readiness still waits on the pending shards.

huangruiteng pushed a commit that referenced this pull request Sep 18, 2026
Follow-up to #4664, which was approved with these left open and a note to close
them together. None of the three moves a number: the residue stays at 40 sites
with the split `annotation_only=5, argument_name_only=10, attribute_read=7,
call_result=14, other=1, unstable_local=3`.

`_MODULE_FUNCTIONS` was keyed on `id(tree)`. That is only correct while `_TREES`
retains every tree it parses, which is a property of a different cache in a
different part of the file. Give `_TREES` a bound and a reused id hands back
another module's functions, binding a same-module call to the wrong callee with
no symptom at all. It is keyed by path and text hash now, exactly as `_TREES`
is, so the two no longer have to agree by accident.

`_is_generator` used `ast.walk`, which descends into nested functions and
lambdas. A plain function that merely defined a generator inside itself read as
a generator and lost its binding. The direction was safe -- the call kept
`call_result` -- but it withheld evidence this slice exists to make actionable,
and it did not match what the docstring says is excluded. The walk now stops at
a nested scope, which owns its own yields. A function that yields itself is
still not bound.

`blockerFor` labelled an object literal, an array literal and a template
expression `dynamic_key`, which the shared vocabulary defines as a computed or
non-literal subscript. None of them is one. Python answers `other` for the same
shapes -- a dict literal or an f-string where a scalar was required -- so that
is what TypeScript answers too; the whole point of the shared taxonomy is that
a reader of the residue gets the same reason from either runtime.

Refs #4447 B2.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: song <22676124+songoow@users.noreply.github.com>

@huangruiteng huangruiteng left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

动机

这个 head 相对我上一轮批准的 65a87e6e 没有新的 PR 内容:中间的提交全部是 origin/main 的合并(含 #4662 的 cross_runtime 说明)。我批准的是精确 head,所以 head 一动就要重新取证;但这次要回答的问题只是"PR 自己的东西有没有被合并改动冲掉",而不是重做一遍设计评审。

改动思路

本 PR 的方向(上一轮已确认)是收回它自己更早的版本:删掉 python_production 里的局部抽象解释器(有序重绑定、容器键精确写入、** 展开、同模块调用解析,外加 TS 的 String()/undefined 特例),保留真正有价值的一半——每个 unresolved 位点带一个具体、可行动的理由,Python 与 TypeScript 共用同一套标签。这一版 head 要对齐的,是"标签只命名障碍,不缩小残量"这条被写成断言的不变量是否仍在。

具体改动

43b6f6a 上的复核

路径范围内的 diff 是最直接的证据:

git diff --stat 65a87e6e..HEAD -- loopx/semantics/production.py \
  loopx/semantics/python_production.py scripts/semantic_production_scan.mjs \
  tests/architecture/test_semantic_producer_binding.py \
  docs/architecture/rfcs/semantic-vocabulary-convergence-v0{,.zh-CN}.md
  → 只有两份 RFC 镜像各 +67/+48 行(#4662 的 cross_runtime 行)

也就是说 blockerFor、docstring 与 enum_result 的理由赋值这些 PR 自有 hunk 一字未动;进到路径范围里的 115 行是 main 侧合并带进来的,不是这个 PR 新增的主张。

实跑(exact head 43b6f6a1):

pytest tests/architecture/test_semantic_producer_binding.py \
  tests/architecture/test_semantic_production.py -q        → 53 passed in 18.08s
python examples/semantic-vocabulary-drift-smoke.py          → ok
  F5:1/1, projections=1/1, producer_scan_reach=440/1213_files
  unresolved_producer_sites=41(与批准时一致,未被"消失")

仍未收尾的三件事(均为非阻塞,与上一轮同一批)

  • PR 描述在我读的 02:24Z 版本仍在卖被撤回的解释器(residue 40、"forms newly bound");RFC 镜像已正确,等作者改描述。
  • dynamic_key 被用在 object/array/template 字面量上,而共享标签把 dynamic_key 定义为"计算或非字面下标"——作者已把它写进 #4682 的对齐里。
  • kernel-static-checks 在 15 分钟上限上被取消,今天在多条 PR 上重复(含这个 PR 的早前 head),属 CI 容量问题,不是 diff 的属性。

对主干的风险

无阻塞发现。这次 head 移动只在两份 docs 镜像上进内容,运行时代码与测试零差异,所以"合并会让这个 PR 的语义悄悄变化"这条风险在本轮不成立;残留风险仍是我上一轮记录的那三条非阻塞项。另外提醒一次:本 head 上没有可用的 status-check rollup,合并前请以重跑后的 CI 为准。

我的整体评价

这是我对同一 PR 的第三次结论,方向一致:承认不知道优于自信地答错。head 因 main 合并而移动时,我按路径范围取证而不是重读整棵树,结论沿用但证据换新。可以接受。

English verdict: APPROVE - head 43b6f6a carries no new PR content over the head I approved (65a87e6): every non-merge commit in between belongs to main, the path-scoped diff over this PR's own files is empty except 115 lines of #4662's cross_runtime notes added to the two RFC mirrors, and the runnable evidence still holds at the moved head (53 passed in the producer-binding and production suites, drift smoke ok with F5:1/1, projections=1/1 and the same 41 unresolved producer sites), so my previous conclusion - the withdrawn local interpreter is really gone and only a labelling taxonomy remains - carries forward; the three non-blocking items (stale PR body, dynamic_key label alignment tracked in #4682, and the 15-minute kernel-static-checks cancellation) are unchanged, and no status-check rollup exists on this head.

@huangruiteng

Copy link
Copy Markdown
Collaborator

需要解决下冲突

@songoow

songoow commented Sep 18, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #4687 — recommending this be closed.

Why this branch can no longer be pushed. The implementation here reached main through #4682 (a02200591 + bf7a7cc41 + 0beaecd8b), so main now carries the local interpreter at 816 lines. This branch removes it (816 → 535) and, being behind main, would revert about 3,295 lines of unrelated work — monitor poll, authority store and more. Updating it is no longer a merge; it is a revert.

The reduction is also no longer the right change. I re-measured the five unsound cases against current main, judging by the union of the rows a case produces and distinguishing the two error directions, which my earlier per-row reading got wrong. F1 proves Produced_scan(v) ⊆ S(v), so over-reporting is safe and only under-reporting lets an unregistered value through. On that basis bf7a7cc41 and #4682 fixed most of it: of the four Python cases, three are now safe.

What survived is this PR's test suite. Running all 20 tests from this branch against main's implementation: 16 passed, 4 failed. Three of the four are genuine under-approximations — a global rebinding still resolving to the module-level def, a ** spread replaying a mutated dict's initializer, and a TypeScript parameter shadowing String still read as the builtin. The fourth is this suite being stricter than soundness requires: a literal non-negative subscript write is modelled on main, and demanding unresolved there pins a weaker scan in place.

#4687 carries the ten counterexamples main lacks, fixes those three holes, and relaxes the fourth assertion to the property that matters. unresolved_producer_sites is 40 before and after, so no budget, floor or anchor moves.

The blocker taxonomy this PR introduced is already on main and is untouched.

@songoow songoow closed this Sep 18, 2026
@huangruiteng

Copy link
Copy Markdown
Collaborator

Merge-order correction after reviewing #4687: this branch should not be merged as-is.

While reviewing #4687 (which states it supersedes this PR's reduction approach), I re-ran the merge analysis on the current exact head 43b6f6a1 against origin/main (71cf1649):

git merge-tree --write-tree --name-only origin/main origin/pr-4664
  CONFLICT (content): docs/architecture/rfcs/semantic-vocabulary-convergence-v0.md
  CONFLICT (content): docs/architecture/rfcs/semantic-vocabulary-convergence-v0.zh-CN.md
  CONFLICT (content): loopx/semantics/python_production.py
git diff origin/main..origin/pr-4664 -- loopx/semantics/python_production.py
  → the branch removes the blocker-taxonomy docstring text that main already carries

So my earlier merge-readiness conclusion (published on 43b6f6a1) no longer applies: the head is stale relative to main, and merging it would conflict in three files and revert landed text. #4687 carries what survived this PR's review — the counterexample suite plus three soundness fixes — and is approved at e4dcb009.

Suggestion: close this PR as superseded rather than merging it, or rebase and reduce it to anything #4687 does not already carry. I am not making that call (it is a maintainer/author decision); I am removing my earlier merge-ready signal from the public record.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants