@qingflow-tech/qingflow-cli 1.1.43-beta.3 → 1.1.43-beta.5

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 (39) hide show
  1. package/README.md +3 -3
  2. package/package.json +1 -1
  3. package/pyproject.toml +1 -1
  4. package/skills/qingflow-app-form/SKILL.md +12 -9
  5. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +15 -13
  6. package/src/qingflow_mcp/app_form_compat.py +11 -27
  7. package/src/qingflow_mcp/app_form_validator.py +298 -35
  8. package/src/qingflow_mcp/builder_facade/app_form.py +777 -150
  9. package/src/qingflow_mcp/builder_facade/app_form_schema.py +164 -8
  10. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +5 -0
  11. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +299 -239
  12. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +82 -27
  13. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +82 -27
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +82 -30
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +113 -37
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +87 -64
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +82 -27
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +82 -27
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +83 -28
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +82 -27
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +82 -27
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +82 -27
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +83 -28
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +82 -27
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +82 -27
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +82 -27
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +95 -40
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +100 -28
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +82 -27
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +82 -27
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +201 -111
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +82 -27
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +1594 -0
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +143 -161
  35. package/src/qingflow_mcp/builder_facade/service.py +3 -2
  36. package/src/qingflow_mcp/cli/commands/builder.py +2 -0
  37. package/src/qingflow_mcp/response_trim.py +2 -0
  38. package/src/qingflow_mcp/server_app_builder.py +9 -4
  39. package/src/qingflow_mcp/tools/ai_builder_tools.py +13 -3
@@ -5,7 +5,7 @@ from dataclasses import dataclass
5
5
  import json
6
6
  import os
7
7
  from pathlib import Path
8
- from typing import Any
8
+ from typing import Any, Iterable
9
9
 
10
10
  from jsonschema import Draft202012Validator, FormatChecker
11
11
 
@@ -14,6 +14,116 @@ JSONObject = dict[str, Any]
14
14
  SCHEMA_ROOT = Path(__file__).with_name("schemas") / "app_form"
15
15
 
16
16
 
17
+ def _discriminator_score(schema: Any, instance: Any, root_schema: Any = None, seen_refs: set[str] | None = None) -> int:
18
+ """Score a schema branch against its input discriminator values."""
19
+ if not isinstance(schema, dict) or not isinstance(instance, dict):
20
+ return 0
21
+ reference = schema.get("$ref")
22
+ if isinstance(reference, str) and reference.startswith("#/") and isinstance(root_schema, dict):
23
+ seen_refs = seen_refs or set()
24
+ if reference in seen_refs:
25
+ return 0
26
+ target: Any = root_schema
27
+ for part in reference[2:].split("/"):
28
+ if not isinstance(target, dict):
29
+ return 0
30
+ target = target.get(part.replace("~1", "/").replace("~0", "~"))
31
+ return _discriminator_score(target, instance, root_schema, seen_refs | {reference})
32
+ score = 0
33
+ properties = schema.get("properties")
34
+ if isinstance(properties, dict):
35
+ for key, constraint in properties.items():
36
+ if not isinstance(constraint, dict) or key not in instance:
37
+ continue
38
+ value = instance.get(key)
39
+ if "const" in constraint:
40
+ score += 3 if value == constraint["const"] else -3
41
+ elif isinstance(constraint.get("enum"), list):
42
+ score += 3 if value in constraint["enum"] else -3
43
+ for combinator in ("allOf", "anyOf", "oneOf"):
44
+ children = schema.get(combinator)
45
+ if not isinstance(children, list):
46
+ continue
47
+ child_scores = [_discriminator_score(child, instance, root_schema, seen_refs) for child in children]
48
+ if combinator == "allOf":
49
+ score += sum(child_score for child_score in child_scores if child_score > 0)
50
+ elif child_scores:
51
+ score += max(child_scores)
52
+ return score
53
+
54
+
55
+ def _selected_combinator_children(error: Any, root_schema: Any = None) -> list[Any] | None:
56
+ """Return errors from the branch matching a known discriminator.
57
+
58
+ jsonschema flattens errors from nested ``allOf`` branches into the parent
59
+ combinator context, so branch identity is recovered from the schema path.
60
+ """
61
+ if not isinstance(error.schema, dict):
62
+ return None
63
+ branches = error.schema.get(error.validator)
64
+ if not isinstance(branches, list) or not branches:
65
+ return None
66
+ scores = [_discriminator_score(branch, error.instance, root_schema) for branch in branches]
67
+ best = max(scores)
68
+ if best <= 0:
69
+ return None
70
+ selected = {index for index, score in enumerate(scores) if score == best}
71
+ prefix = tuple(error.absolute_schema_path)
72
+ children = [
73
+ child
74
+ for child in error.context
75
+ if len(tuple(child.absolute_schema_path)) > len(prefix)
76
+ and tuple(child.absolute_schema_path)[: len(prefix)] == prefix
77
+ and tuple(child.absolute_schema_path)[len(prefix)] in selected
78
+ ]
79
+ return children or None
80
+
81
+
82
+ def _iter_compact_schema_errors(error: Any, root_schema: Any = None) -> Iterable[Any]:
83
+ if error.validator in {"oneOf", "anyOf"}:
84
+ selected = _selected_combinator_children(error, root_schema)
85
+ if selected:
86
+ for child in selected:
87
+ yield from _iter_compact_schema_errors(child, root_schema)
88
+ return
89
+ yield error
90
+ return
91
+ if error.context:
92
+ for child in error.context:
93
+ yield from _iter_compact_schema_errors(child, root_schema)
94
+ return
95
+ yield error
96
+
97
+
98
+ def compact_schema_errors(errors: Iterable[Any], root_schema: Any = None) -> list[Any]:
99
+ """Collapse polymorphic-schema noise while retaining actionable errors."""
100
+ flattened = [leaf for error in errors for leaf in _iter_compact_schema_errors(error, root_schema)]
101
+ descendant_paths = [tuple(error.absolute_path) for error in flattened]
102
+ result: list[Any] = []
103
+ seen: set[tuple[tuple[Any, ...], str, str]] = set()
104
+ for error in flattened:
105
+ path = tuple(error.absolute_path)
106
+ if error.validator == "unevaluatedProperties" and any(
107
+ len(other) > len(path) and other[: len(path)] == path for other in descendant_paths
108
+ ):
109
+ continue
110
+ key = (path, str(error.validator), schema_error_message(error))
111
+ if key in seen:
112
+ continue
113
+ seen.add(key)
114
+ result.append(error)
115
+ return result
116
+
117
+
118
+ def schema_error_message(error: Any) -> str:
119
+ """Render a bounded, user-facing message for a JSON Schema error."""
120
+ if error.validator == "oneOf":
121
+ return "value is not valid under any of the given schemas"
122
+ if error.validator == "anyOf":
123
+ return "value is not valid under any of the given schemas"
124
+ return str(error.message)
125
+
126
+
17
127
  class AppFormContractError(ValueError):
18
128
  def __init__(self, error_code: str, message: str, *, path: str | None = None, details: JSONObject | None = None) -> None:
19
129
  super().__init__(message)
@@ -29,6 +139,7 @@ class AppFormSchemaContext:
29
139
  api_version: str
30
140
  complete_schema: JSONObject
31
141
  supported_types: tuple[str, ...]
142
+ supported_schema_kinds: tuple[str, ...]
32
143
  contract_source: str
33
144
  contract_version: str
34
145
  normalizer_version: str
@@ -39,11 +150,12 @@ class AppFormSchemaContext:
39
150
  errors = sorted(validator.iter_errors(payload), key=lambda item: (list(item.absolute_path), item.message))
40
151
  if not errors:
41
152
  return
42
- error = errors[0]
153
+ compact_errors = compact_schema_errors(errors, self.complete_schema)
154
+ error = compact_errors[0] if compact_errors else errors[0]
43
155
  pointer = "".join(f"/{str(part).replace('~', '~0').replace('/', '~1')}" for part in error.absolute_path) or "/"
44
156
  raise AppFormContractError(
45
157
  "INVALID_SPEC",
46
- error.message,
158
+ schema_error_message(error),
47
159
  path=pointer,
48
160
  details={"validator": error.validator, "schemaPath": "/".join(str(part) for part in error.absolute_schema_path)},
49
161
  )
@@ -131,6 +243,9 @@ class AppFormSchemaRegistry:
131
243
  api_version = str(metadata.get("apiVersion") or "")
132
244
  complete = self._read_json(root / version / "complete.json")
133
245
  supported = tuple(str(item) for item in metadata.get("fieldTypes") or ())
246
+ supported_schema_kinds = tuple(str(item) for item in metadata.get("schemaKinds") or ("summary", "field"))
247
+ if not {"summary", "field"}.issubset(supported_schema_kinds):
248
+ raise RuntimeError(f"AppForm schema version must support summary and field Schemas: {version}")
134
249
  if complete.get("properties", {}).get("apiVersion", {}).get("const") != api_version:
135
250
  raise RuntimeError(f"AppForm schema apiVersion mismatch for {version}")
136
251
  schema_id = str(complete.get("$id") or "")
@@ -143,6 +258,11 @@ class AppFormSchemaRegistry:
143
258
  raise RuntimeError(f"AppForm summary apiVersion mismatch for {version}")
144
259
  Draft202012Validator.check_schema(summary)
145
260
  self._validate_local_refs(summary, root / version / "summary.json")
261
+ if "formula" in supported_schema_kinds:
262
+ formula_path = root / version / "formula.json"
263
+ formula = self._read_json(formula_path)
264
+ Draft202012Validator.check_schema(formula)
265
+ self._validate_local_refs(formula, formula_path)
146
266
  for field_type in supported:
147
267
  field_path = root / version / "fields" / f"{field_type}.json"
148
268
  field_schema = self._read_json(field_path)
@@ -153,6 +273,7 @@ class AppFormSchemaRegistry:
153
273
  api_version,
154
274
  complete,
155
275
  supported,
276
+ supported_schema_kinds,
156
277
  str(metadata.get("contractSource") or "app_form"),
157
278
  str(metadata.get("contractVersion") or version),
158
279
  str(metadata.get("normalizerVersion") or version),
@@ -161,12 +282,46 @@ class AppFormSchemaRegistry:
161
282
  self._contexts[version] = context
162
283
  self._api_to_version[api_version] = version
163
284
 
164
- def schema(self, *, schema_version: str | None = None, field_type: str | None = None) -> JSONObject:
285
+ def schema(
286
+ self,
287
+ *,
288
+ schema_version: str | None = None,
289
+ field_type: str | None = None,
290
+ schema_kind: str | None = None,
291
+ ) -> JSONObject:
165
292
  context = self.resolve_schema_version(schema_version)
166
- if field_type is None:
293
+ requested_kind = str(schema_kind or ("field" if field_type is not None else "summary")).strip()
294
+ if requested_kind not in context.supported_schema_kinds:
295
+ raise AppFormContractError(
296
+ "UNSUPPORTED_SCHEMA_KIND",
297
+ f"unsupported AppForm schema kind: {requested_kind}",
298
+ details={"supportedKinds": list(context.supported_schema_kinds)},
299
+ )
300
+ if requested_kind == "formula":
301
+ if field_type is not None:
302
+ raise AppFormContractError(
303
+ "INVALID_SCHEMA_SELECTION",
304
+ "field_type cannot be combined with schema_kind=formula",
305
+ details={"schemaKind": requested_kind, "fieldType": field_type},
306
+ )
307
+ schema = self._read_json(self.root / context.schema_version / "formula.json")
308
+ resolved_kind = "formula"
309
+ elif requested_kind == "summary":
310
+ if field_type is not None:
311
+ raise AppFormContractError(
312
+ "INVALID_SCHEMA_SELECTION",
313
+ "field_type requires schema_kind=field or an omitted schema_kind",
314
+ details={"schemaKind": requested_kind, "fieldType": field_type},
315
+ )
167
316
  schema = self._read_json(self.root / context.schema_version / "summary.json")
168
- schema_kind = "summary"
317
+ resolved_kind = "summary"
169
318
  else:
319
+ if field_type is None:
320
+ raise AppFormContractError(
321
+ "INVALID_SCHEMA_SELECTION",
322
+ "schema_kind=field requires field_type",
323
+ details={"schemaKind": requested_kind, "supportedTypes": list(context.supported_types)},
324
+ )
170
325
  if field_type not in context.supported_types:
171
326
  raise AppFormContractError(
172
327
  "UNSUPPORTED_SCHEMA_FIELD_TYPE",
@@ -174,14 +329,15 @@ class AppFormSchemaRegistry:
174
329
  details={"supportedTypes": list(context.supported_types)},
175
330
  )
176
331
  schema = self._read_json(self.root / context.schema_version / "fields" / f"{field_type}.json")
177
- schema_kind = "field"
332
+ resolved_kind = "field"
178
333
  return {
179
334
  "schemaVersion": context.schema_version,
180
335
  "apiVersion": context.api_version,
181
336
  "latestVersion": self.latest_version,
182
337
  "storedVersions": list(self.stored_versions),
183
338
  "enabledVersions": list(self.enabled_versions),
184
- "schemaKind": schema_kind,
339
+ "supportedSchemaKinds": list(context.supported_schema_kinds),
340
+ "schemaKind": resolved_kind,
185
341
  "fieldType": field_type,
186
342
  "schema": schema,
187
343
  }
@@ -31,6 +31,11 @@
31
31
  "subtable"
32
32
  ],
33
33
  "normalizerVersion": "v1alpha1",
34
+ "schemaKinds": [
35
+ "summary",
36
+ "field",
37
+ "formula"
38
+ ],
34
39
  "status": "active"
35
40
  }
36
41
  }