@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.
- package/README.md +3 -3
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/skills/qingflow-app-form/SKILL.md +12 -9
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +15 -13
- package/src/qingflow_mcp/app_form_compat.py +11 -27
- package/src/qingflow_mcp/app_form_validator.py +298 -35
- package/src/qingflow_mcp/builder_facade/app_form.py +777 -150
- package/src/qingflow_mcp/builder_facade/app_form_schema.py +164 -8
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +5 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +299 -239
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +82 -30
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +113 -37
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +87 -64
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +83 -28
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +83 -28
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +95 -40
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +100 -28
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +201 -111
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +82 -27
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +1594 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +143 -161
- package/src/qingflow_mcp/builder_facade/service.py +3 -2
- package/src/qingflow_mcp/cli/commands/builder.py +2 -0
- package/src/qingflow_mcp/response_trim.py +2 -0
- package/src/qingflow_mcp/server_app_builder.py +9 -4
- 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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
339
|
+
"supportedSchemaKinds": list(context.supported_schema_kinds),
|
|
340
|
+
"schemaKind": resolved_kind,
|
|
185
341
|
"fieldType": field_type,
|
|
186
342
|
"schema": schema,
|
|
187
343
|
}
|