@qingflow-tech/qingflow-cli 1.1.51 → 1.1.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +29 -13
  3. package/package.json +1 -1
  4. package/pyproject.toml +8 -2
  5. package/skills/qingflow-cli/SKILL.md +93 -322
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  17. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +3 -2
  18. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
  19. package/skills/qingflow-cli/reference/builder/50-views.md +192 -191
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
  21. package/skills/qingflow-cli/reference/builder/README.md +1 -1
  22. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  26. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  27. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  28. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  29. package/skills/qingflow-cli/reference/data/README.md +32 -0
  30. package/skills/qingflow-cli/reference/examples/README.md +1 -1
  31. package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
  32. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
  33. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
  34. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  35. package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
  36. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  37. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  38. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  39. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  40. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
  41. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  42. package/skills/qingflow-common/SKILL.md +7 -4
  43. package/src/qingflow_mcp/app_form_compat.py +3 -0
  44. package/src/qingflow_mcp/app_form_validator.py +69 -17
  45. package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
  46. package/src/qingflow_mcp/builder_facade/models.py +227 -17
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
  64. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
  65. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
  66. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
  67. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
  68. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
  69. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
  70. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
  71. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
  72. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
  73. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
  74. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
  75. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
  76. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
  77. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
  78. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
  79. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
  80. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
  81. package/src/qingflow_mcp/builder_facade/service.py +1709 -518
  82. package/src/qingflow_mcp/builder_facade/views_extract.py +595 -0
  83. package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
  84. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  85. package/src/qingflow_mcp/cli/commands/builder.py +76 -55
  86. package/src/qingflow_mcp/cli/commands/data.py +134 -0
  87. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  88. package/src/qingflow_mcp/cli/context.py +3 -0
  89. package/src/qingflow_mcp/cli/formatters.py +97 -1
  90. package/src/qingflow_mcp/public_surface.py +8 -0
  91. package/src/qingflow_mcp/response_trim.py +11 -2
  92. package/src/qingflow_mcp/server.py +2 -0
  93. package/src/qingflow_mcp/server_app_builder.py +34 -10
  94. package/src/qingflow_mcp/server_app_user.py +4 -0
  95. package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
  96. package/src/qingflow_mcp/solution/executor.py +96 -8
  97. package/src/qingflow_mcp/solution/spec_models.py +1 -0
  98. package/src/qingflow_mcp/tools/ai_builder_tools.py +218 -398
  99. package/src/qingflow_mcp/tools/csv_export.py +168 -0
  100. package/src/qingflow_mcp/tools/data_tools.py +284 -0
  101. package/src/qingflow_mcp/tools/record_tools.py +11 -46
  102. package/src/qingflow_mcp/tools/solution_tools.py +2 -0
  103. package/src/qingflow_mcp/tools/view_tools.py +0 -6
  104. package/src/qingflow_mcp/tools/view_writer.py +44 -0
  105. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
  106. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
@@ -9,14 +9,14 @@ import pandas as pd
9
9
 
10
10
  files = [
11
11
  "/absolute/path/records-0001.csv",
12
- # include every record_access.files[].local_path
12
+ # 包含 data query 返回的每个 files[].local_path
13
13
  ]
14
14
 
15
15
  frames = [pd.read_csv(path, dtype=str, keep_default_na=False) for path in files]
16
16
  df = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
17
17
 
18
18
  # Columns are readable and field-id anchored, e.g. 项目状态__field_343283094.
19
- fields = [] # optionally paste record_access.fields here if you need field-id metadata
19
+ fields = [] # 需要字段 id 元数据时使用 data query 返回的 fields
20
20
  field_by_id = {int(item["field_id"]): item for item in fields if "field_id" in item}
21
21
  title_by_col = {item["column_name"]: item["title"] for item in fields if item.get("column_name")}
22
22
  ```
@@ -19,7 +19,8 @@ Do not require both surfaces. If only one is installed, complete the workflow on
19
19
  ## Shared Concepts
20
20
 
21
21
  - **AppForm** is the versioned declarative definition for an application form: app metadata, fields, sections, layout, settings, and field configuration. The same JSON declaration is used by CLI and Qingflow MCP.
22
- - **Record** is runtime data written to an existing app. Use the record schema before an insert, update, import, or analysis workflow.
22
+ - **Data** 是既有应用中的普通业务数据。查询、单条读取和直接增删改默认使用视图无关的 data 入口,并以当次 schema、字段能力和访问范围为准。
23
+ - **Record** 用于详情页附件/图片、完整日志、导入、代码块,以及用户明确指定的 record 命令;不要把普通应用数据工作路由到 record。
23
24
  - **Task** is a workflow work item. Prefer task operations for approval, transfer, rollback, and assignee actions.
24
25
  - **Builder resource** is package, app, view, chart, portal, navigation, or workflow configuration. It is separate from record data.
25
26
  - **Schema version** is part of the public contract. Pin it for one AppForm or workflow operation and do not combine values from different versions.
@@ -30,11 +31,13 @@ Do not require both surfaces. If only one is installed, complete the workflow on
30
31
  | --- | --- | --- |
31
32
  | Form schema/read/apply/delete | `qingflow --json builder app-form ...` / `builder app delete` | `app_form_schema`, `app_form_get`, `app_form_apply`, `app_delete` |
32
33
  | AppForm static validation | `qingflow --json builder app-form validate --file ...` | Performed inside `app_form_apply`; no separate tool |
33
- | Record and task work | `qingflow --json record ...` / `task ...` | Qingflow MCP record and task tools |
34
- | Open a workspace or confirmed business page | `qingflow --json auth login-link [--redirect-path PATH]` | `get_login_link` |
34
+ | 普通应用数据查询与直接增删改 | `qingflow --json data ...` | Qingflow MCP `data_query/get/insert/update/delete` |
35
+ | 日志、附件、导入、代码块或用户指定的 record 命令 | `qingflow --json record ...` / `import ...` | Qingflow MCP 对应 record/import 专项工具 |
36
+ | 待办与流程动作 | `qingflow --json task ...` | Qingflow MCP task tools |
37
+ | 免登录打开网页或指定业务页面 | `qingflow --json auth login-link [--redirect-path PATH]` | `get_login_link` |
35
38
  | Package, view, portal, chart, workflow | `qingflow --json builder ...` | Qingflow MCP configuration tools |
36
39
 
37
- The operation names and payload schemas are defined by the public surface. Use the current Schema or tool input schema as the source of truth, not a legacy command name or backend payload.
40
+ The operation names and payload schemas are defined by the public surface. Use the current Schema or tool input schema as the source of truth, not another command's shape or a backend payload.
38
41
 
39
42
  ## Package Boundaries
40
43
 
@@ -10,6 +10,9 @@ from .json_types import JSONObject
10
10
  TYPE_ALIASES = {
11
11
  "multiline": "long_text",
12
12
  "textarea": "long_text",
13
+ "richtext": "rich_text",
14
+ "rich-text": "rich_text",
15
+ "rich_text": "rich_text",
13
16
  "radio": "single_choice",
14
17
  "boolean": "single_choice",
15
18
  "select": "single_select",
@@ -11,9 +11,15 @@ from .app_form_compat import normalize_app_form_declaration
11
11
  from .builder_facade.app_form_schema import AppFormContractError, compact_schema_errors, schema_error_message
12
12
 
13
13
 
14
+ #: code_block / q_linker 输出目标字段类型。后端写链无类型白名单(FormSvcImpl
15
+ #: 原样落库),运行时 DataSvcImpl.getAnswerValueByString:选项/数字/日期有专门
16
+ #: 分支;attachment 按拆分 URL 写入(专门分支);subtable 整表组装(专门分支);
17
+ #: rich_text/data_relation 走 Question.canImport 兜底按字符串写入(不报错)。
18
+ #: 四者为存量合法配置(提取读得出、apply 必须写回,两闸门同口径)。
14
19
  _INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
15
20
  "text",
16
21
  "long_text",
22
+ "rich_text",
17
23
  "number",
18
24
  "amount",
19
25
  "date",
@@ -21,6 +27,9 @@ _INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
21
27
  "single_choice",
22
28
  "single_select",
23
29
  "multi_select",
30
+ "attachment",
31
+ "data_relation",
32
+ "subtable",
24
33
  }
25
34
  _Q_LINKER_TEMPLATE_RE = re.compile(r"\{\{(?P<key>[^{}\s]+)\}\}")
26
35
  _FORMULA_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
@@ -218,19 +227,32 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
218
227
  widths = ["width" in field for field in fields if isinstance(field, dict)]
219
228
  if widths and any(widths) and not all(widths):
220
229
  errors.append((row_path, "width must be present on every field in the row or omitted from all"))
221
- if widths and all(widths) and sum(field.get("width", 0) for field in fields if isinstance(field, dict)) != 100:
222
- errors.append((row_path, "field widths must total 100"))
230
+ # 行宽契约(与后端语义对齐):任意 1-100 整数 + 全有或全无。
231
+ # 后端 html_width 无取值/总和校验,存量存在 33/34 三等分与字段删除后
232
+ # 留白(如 3×25=75)的行;get 读出的原布局必须能原样过校验
233
+ # (apply(get(S)) ≡ S 命题的必要条件)。
234
+ if widths and all(widths):
235
+ bad = [f.get("width") for f in fields if isinstance(f, dict) and not (isinstance(f.get("width"), int) and 1 <= f["width"] <= 100)]
236
+ if bad:
237
+ errors.append((row_path, "field widths must be an integer between 1 and 100"))
223
238
  for field_index, field in enumerate(fields):
224
239
  if isinstance(field, dict):
225
240
  top_fields.append((f"{row_path}/{field_index}", field))
226
241
  def resolve_local_selector(selector: Any) -> tuple[str, tuple[str, dict[str, Any]] | None]:
227
- """Resolve against the declared body, preserving backend-dependent ID-only cases."""
242
+ """Resolve against the declared body, preserving backend-dependent ID-only cases.
243
+
244
+ 返回态:resolved(唯一定位)/ invalid(定位失败)/ ambiguous(方案 A:name-only
245
+ 命中多个同名字段,调用方须报 AMBIGUOUS_FIELD_SELECTOR)/ unknown(存在未命名
246
+ 字段,后端侧再判)/ schema(结构不完整,交给 Schema 校验)。
247
+ """
228
248
  if not isinstance(selector, dict):
229
249
  return "schema", None
230
250
  raw_que_id = selector.get("queId")
231
251
  que_id = raw_que_id if isinstance(raw_que_id, int) and not isinstance(raw_que_id, bool) else None
232
252
  raw_name = selector.get("name")
233
- name = raw_name if isinstance(raw_name, str) and raw_name else ""
253
+ # name 两侧 strip:存量字段名带前导/尾随空格,queId 已同时给出不构成歧义
254
+ # (与 compiler 侧三个 resolve 函数同口径)。
255
+ name = raw_name.strip() if isinstance(raw_name, str) and raw_name.strip() else ""
234
256
  if raw_que_id is not None and que_id is None:
235
257
  return "schema", None
236
258
  if raw_name is not None and not name:
@@ -239,26 +261,52 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
239
261
  matches = [(path, field) for path, field in top_fields if field.get("queId") == que_id]
240
262
  if len(matches) != 1:
241
263
  return "invalid", None
242
- declared_name = str(matches[0][1].get("name") or "")
264
+ declared_name = str(matches[0][1].get("name") or "").strip()
243
265
  if name and declared_name and name != declared_name:
244
266
  return "invalid", None
245
267
  return "resolved", matches[0]
246
268
  if name:
247
- matches = [(path, field) for path, field in top_fields if str(field.get("name") or "") == name]
269
+ matches = [(path, field) for path, field in top_fields if str(field.get("name") or "").strip() == name]
248
270
  if len(matches) == 1:
249
271
  return "resolved", matches[0]
250
272
  if len(matches) > 1:
251
- return "invalid", None
273
+ return "ambiguous", None
252
274
  if any(not str(field.get("name") or "") for _, field in top_fields):
253
275
  return "unknown", None
254
276
  return "invalid", None
255
277
  return "schema", None
256
278
 
279
+ def ambiguity_message(selector: Any, subject: str) -> str:
280
+ name = str((selector or {}).get("name") or "").strip()
281
+ candidate_ids = sorted(
282
+ {
283
+ field.get("queId")
284
+ for _, field in top_fields
285
+ if str(field.get("name") or "").strip() == name
286
+ and isinstance(field.get("queId"), int)
287
+ and not isinstance(field.get("queId"), bool)
288
+ }
289
+ )
290
+ return (
291
+ f"AMBIGUOUS_FIELD_SELECTOR: {subject} name {name!r} matches multiple fields "
292
+ f"(candidate queIds: {candidate_ids}); duplicate names are allowed but "
293
+ "selectors must use queId to disambiguate"
294
+ )
295
+
257
296
  title_selector = spec.get("dataTitleField")
258
297
  if top_fields:
259
298
  if "dataTitleField" not in spec and "appKey" in payload:
260
299
  pass
261
- elif title_selector is None or resolve_local_selector(title_selector)[0] == "invalid":
300
+ elif title_selector is None:
301
+ # dataTitleField=null 仅在带 appKey 的更新/round-trip 场景放行:后端写链
302
+ # FormSvcImpl.normalizeDataTitleQueId 对 null 自动 fallback 到第一个可用
303
+ # 字段,get 读面在后端未设置时原样输出 null。创建场景(无 appKey)仍需
304
+ # 显式 selector。
305
+ if "appKey" not in payload:
306
+ errors.append(("/spec/dataTitleField", "dataTitleField must resolve to exactly one top-level field"))
307
+ elif resolve_local_selector(title_selector)[0] == "ambiguous":
308
+ errors.append(("/spec/dataTitleField", ambiguity_message(title_selector, "dataTitleField")))
309
+ elif resolve_local_selector(title_selector)[0] == "invalid":
262
310
  errors.append(("/spec/dataTitleField", "dataTitleField must resolve to exactly one top-level field"))
263
311
  elif title_selector is not None:
264
312
  errors.append(("/spec/dataTitleField", "dataTitleField must be null when body is empty"))
@@ -266,11 +314,15 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
266
314
  cover_selector = spec.get("dataCoverField")
267
315
  if cover_selector is not None:
268
316
  cover_resolution, cover = resolve_local_selector(cover_selector)
269
- if cover_resolution == "invalid":
317
+ if cover_resolution == "ambiguous":
318
+ errors.append(("/spec/dataCoverField", ambiguity_message(cover_selector, "dataCoverField")))
319
+ elif cover_resolution == "invalid":
270
320
  errors.append(("/spec/dataCoverField", "dataCoverField must resolve to exactly one top-level field"))
271
321
  elif cover_resolution == "resolved" and cover is not None and _field_type(cover[1]) != "attachment":
272
322
  errors.append(("/spec/dataCoverField", "dataCoverField must select a top-level attachment field"))
273
- names: dict[str, str] = {}
323
+ # 方案 A(duplicate-field-name):同父级允许同名(后端以 queId 为身份,40013
324
+ # 实测 1624 组),不再报 duplicate name;歧义在 selector 解析处以
325
+ # AMBIGUOUS_FIELD_SELECTOR 显式拒绝。
274
326
  fields_by_id = {
275
327
  field.get("queId"): field
276
328
  for _, field in top_fields
@@ -278,12 +330,6 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
278
330
  }
279
331
  output_targets: dict[str, tuple[str, str]] = {}
280
332
  for path, field in top_fields:
281
- name = str(field.get("name") or "")
282
- if name:
283
- if name in names:
284
- errors.append((f"{path}/name", f"duplicate top-level field name; first declared at {names[name]}"))
285
- else:
286
- names[name] = f"{path}/name"
287
333
  field_type = _field_type(field)
288
334
  if field_type == "data_relation":
289
335
  config = field.get("config") if isinstance(field.get("config"), dict) else {}
@@ -329,6 +375,9 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
329
375
  selector = item.get("field") if isinstance(item.get("field"), dict) else {}
330
376
  input_path = f"{path}/config/{binding_name}/inputs/{index}"
331
377
  resolution, selected = resolve_local_selector(selector)
378
+ if resolution == "ambiguous":
379
+ errors.append((f"{input_path}/field", ambiguity_message(selector, f"{binding_name}.inputs[{index}].field")))
380
+ continue
332
381
  if resolution == "invalid":
333
382
  errors.append((f"{input_path}/field", "input field must resolve to exactly one top-level field"))
334
383
  continue
@@ -380,13 +429,16 @@ def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
380
429
  )
381
430
  )
382
431
 
383
- producer = name or str(field.get("queId") or field_type)
432
+ producer = str(field.get("name") or "") or str(field.get("queId") or field_type)
384
433
  for index, item in enumerate(binding.get("outputs") or []):
385
434
  if not isinstance(item, dict):
386
435
  continue
387
436
  selector = item.get("targetField")
388
437
  target_path = f"{path}/config/{binding_name}/outputs/{index}/targetField"
389
438
  resolution, selected = resolve_local_selector(selector)
439
+ if resolution == "ambiguous":
440
+ errors.append((target_path, ambiguity_message(selector, f"{binding_name}.outputs[{index}].targetField")))
441
+ continue
390
442
  if resolution == "invalid":
391
443
  errors.append((target_path, "targetField must resolve to exactly one top-level field"))
392
444
  continue