fix(nodes): case 节点 case["name"] 与自身校验文案矛盾——按文档省略 name 直接裸 KeyError 崩在节点构造期
Nobody has claimed this yet.
Assessment
- Difficulty
- 2/5
- Estimated time
- 1-3 hours
- Newbie friendliness
- 75/100
Research direction
Start at SwitchLegacy.setup_branches in plaita/node/decide.py around line 362, where label is read unconditionally from the case's name key. Make that read optional, as the issue suggests. Add a unit test beside test_case_targets_count_as_edges in tests/unit/test_mfix_i_rules_hook.py: a case without name should construct, its first branch's name should be the case id, and the missing-id and missing-value ValueErrors should still fire. Done when Flow.model_validate accepts that IR without a KeyError.
Written by the indexing model from the issue text.
Description
优先级 P2 · 依赖:无
基线 HEAD = 65f48f2
Summary
case 节点(SwitchLegacy)的校验与取值对 name 字段自相矛盾:校验循环明确宣称 name
可选,紧接着的分支构造却无条件用 case["name"] 取值,name 缺失时抛裸 KeyError。
plaita/node/decide.py:350-361—— 校验只覆盖id/value,且文案自述name可选:
for i, case in enumerate(cases):
# 缺 id 曾直接裸 KeyError: 'id',无任何上下文(R5 差分评审 P1-5)
if not isinstance(case, dict) or "id" not in case:
raise ValueError(
f"switch-case node {values.get('id', '?')!r}: cases[{i}] 缺少必填的 "
f"'id' 字段(得到 {case!r})。每个 case 需要 id/value,可选 name。"
)
if "value" not in case:
raise ValueError(
f"switch-case node {values.get('id', '?')!r}: cases[{i}] ({case.get('id')!r}) "
"缺少必填的 'value' 字段。"
)
plaita/node/decide.py:362-372—— 紧随其后无条件取case["name"]:
branches = [
Branch(
name=case["id"],
condition=Condition(field=values.get("target"), operator=CONDITION_OP_EQ, value=case["value"]),
priority=0,
label=case["name"],
next=case["id"],
is_default=False,
)
for case in cases
]
plaita/node/decide.py:149—— 落点label本身可选,即缺失name在数据模型上是合法的:
label: Optional[str] = None
- 上游 IR 静态门不拦
name。plaita/dsl/ir_validate.py:182-188只按id/next建边:
for c in node.get("cases") or []:
if not isinstance(c, dict):
continue
t = c.get("id") or c.get("next")
if isinstance(t, str):
yield t
- 崩点位于节点构造期,
plaita/core/flow.py:169处 dict 节点进入 registry 解析:
resolved.append(reg.parse_node(n))
- 无
name的 case 形状被当作正向用例存在于本仓测试中,
tests/unit/test_mfix_i_rules_hook.py:164-173:
def test_case_targets_count_as_edges(self):
validate_flow_ir(
_flow([
{"type": "start", "id": "s", "next": "c"},
{"type": "case", "id": "c", "target": "$INPUT.k",
"cases": [{"id": "a", "value": 1}], "default": "e"},
{"type": "mock", "id": "a", "next": "e"},
{"type": "end", "id": "e"},
])
)
复现(实跑,Python 3.12,plaita editable 安装):
$ /tmp/plaita-audit-venv/bin/python /tmp/pnb-repro/t2_case.py
raised: KeyError KeyError('name')
$ /tmp/plaita-audit-venv/bin/python /tmp/pnb-repro/t2b_case_e2e.py
validate_flow_ir: PASSED (no error)
Flow.model_validate raised: KeyError KeyError('name')
即:validate_flow_ir 对这份 IR 判定通过,随后 Flow.model_validate(ir) 直接抛 KeyError('name')。
影响
手写 JSON IR、MCP/LLM 生成的 IR、以及 console「JSON 侧」直接编辑节点参数的作者会踩到:
plaita-console/frontend/src/components/flow/schemaForm/coreFields.ts 把 cases 作为 case
节点的透传字段,用户在 JSON 中写 {"id": "...", "value": ...}(完全符合节点自身报错文案
「每个 case 需要 id/value,可选 name」)时,发布前静态门 validate_flow_ir 放行,
解析期 Flow.model_validate 抛裸 KeyError('name')——没有节点 id、没有 cases[i] 下标、
没有修复提示,作者只能看到 pydantic mode="before" validator 里逃逸出来的裸异常。
代价:错误信息不可定位(对比同函数对缺 id/缺 value 都给了带节点 id 与下标的
ValueError),排查成本被推给作者;且失败点从「静态门」后移到「解析/加载期」,
任何按「过门即安全」组织的发布或加载流程都会在门后崩溃。
不修的后果:同一个节点对同一字段给出互相矛盾的两套语义(文档与报错说可选、代码要求必填),
缺 name 的 IR 永远无法加载,且只能得到无上下文的 KeyError。
建议
修复方向(plaita/node/decide.py 的 SwitchLegacy.setup_branches):把 label=case["name"]
改为可选读取,与 Branch.label 的可选语义及自身校验文案对齐:
- 最小修法(一行):
label=case["name"]→label=case.get("name");此时case缺name
时label为None,与decide.py:149的声明一致。若希望保留展示名,可退化为
label=case.get("name") or case["id"],但需同步确认该处语义是否影响下游展示/路由。
验收条件:
- 新增(或改造
test_case_targets_count_as_edges同形状的)单测,
断言SwitchLegacy(id="c", target="$INPUT.k", cases=[{"id": "a", "value": 1}], default="e")
可正常构造,且构造出的branches[0].name == "a"。 - 端到端断言:对上述第 6 条所示的 IR,
validate_flow_ir(ir)与Flow.model_validate(ir)
均不再抛异常;/tmp/pnb-repro/t2b_case_e2e.py应输出Flow.model_validate OK
而不是raised: KeyError KeyError('name')。 - 回归:缺
id、缺value两条路径仍抛带节点 id 与下标的ValueError(现有文案不得回退为裸异常)。
边界
- 只修
SwitchLegacy.setup_branches的name取值与「可选 name」语义矛盾,不顺手改动
id/value两条已有校验分支的文案,也不改plaita/dsl/ir_validate.py的建边逻辑。 - 不改变「
case的跳转目标恒为id」这一既有约定,next归一化仍留在plaita/dsl/builder.py
的case()中(该函数不补name,是暴露路径之一,但本次不要求它代填)。 - 与其他路径的掩盖关系需说明,避免误判影响面:
plaita/dsl/sexpr.py:584的_c_match
总会生成name,docs-site/docs/guide/yaml-and-dsl.md:176-179的官方示例每条 case 都写了
name,因此主流前端路径会掩盖该缺陷;本单只针对手写 JSON / LLM 生成 / console JSON 侧
这条暴露路径。 - 查重:
gh issue list -R jeffkit/plaita --state all --limit 100共 33 条(含 closed),
grepcase/switch/SwitchLegacy无同根条目(#16 是map子流程与if作用域的内核限制,
#31 虽同为plaita/node/下 HTTP 节点但缺陷面无关),本单非重复。
- Dominant language
- Python
- Stars
- 0
- Forks
- 1
- Avg merge
- 3h 53m
- Merged PRs (30d)
- 3
Getting set up
This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from jeffkit/plaita
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 86/100
UKGovernmentBEIS/inspect_ai#5802 ·
Maintainers usually reply within 2 days
-
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
no-human-ai/no_human#660 ·
Maintainers usually reply within 1 day
-
documentation good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
documentation need help question
Difficulty 1/5 1-3 hours Newbie friendliness 66/100
phonology024/babelscribe#26 ·
-
bug
Difficulty 2/5 1-3 hours Newbie friendliness 62/100