@qingflow-tech/qingflow-cli 1.1.43 → 1.1.44-beta.1
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 +13 -6
- package/entry_point.py +1 -1
- package/npm/bin/qingflow.mjs +1 -2
- package/npm/lib/runtime.mjs +3 -7
- package/package.json +2 -2
- package/pyproject.toml +3 -4
- package/skills/qingflow-app-form/SKILL.md +66 -0
- package/skills/qingflow-app-form/agents/openai.yaml +4 -0
- package/skills/qingflow-cli/SKILL.md +8 -6
- package/skills/qingflow-cli/manifest.yaml +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +4 -4
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
- package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +4 -4
- package/skills/qingflow-cli/reference/builder/10-build-single-app.md +5 -5
- package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +14 -14
- package/skills/qingflow-cli/reference/builder/30-schema-fields.md +106 -103
- package/skills/qingflow-cli/reference/builder/40-layout.md +39 -31
- package/skills/qingflow-cli/reference/builder/README.md +9 -9
- package/skills/qingflow-cli/reference/builder/code-integrations/README.md +4 -2
- package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +2 -0
- package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +2 -0
- package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +45 -38
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
- package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
- package/skills/qingflow-cli/reference/examples/README.md +3 -3
- package/skills/qingflow-common/SKILL.md +47 -0
- package/skills/qingflow-common/agents/openai.yaml +4 -0
- package/src/qingflow_mcp/__main__.py +2 -3
- package/src/qingflow_mcp/app_form_compat.py +104 -0
- package/src/qingflow_mcp/app_form_validator.py +462 -0
- package/src/qingflow_mcp/builder_facade/app_form.py +4196 -0
- package/src/qingflow_mcp/builder_facade/app_form_schema.py +425 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +42 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +2969 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +392 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +236 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +425 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +318 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +580 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +150 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +150 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +345 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +190 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +190 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +190 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +484 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +214 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +236 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +199 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +412 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +454 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +214 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +214 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +1921 -0
- package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +190 -0
- 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 +718 -0
- package/src/qingflow_mcp/builder_facade/service.py +8 -4
- package/src/qingflow_mcp/cli/commands/builder.py +93 -28
- package/src/qingflow_mcp/cli/json_io.py +21 -2
- package/src/qingflow_mcp/cli/main.py +3 -39
- package/src/qingflow_mcp/public_surface.py +5 -2
- package/src/qingflow_mcp/response_trim.py +50 -0
- package/src/qingflow_mcp/server.py +24 -3
- package/src/qingflow_mcp/server_app_builder.py +44 -87
- package/src/qingflow_mcp/tools/ai_builder_tools.py +103 -100
- package/src/qingflow_mcp/tools/app_tools.py +46 -65
- package/src/qingflow_mcp/tools/record_tools.py +3 -3
- package/src/qingflow_mcp/version.py +2 -4
- package/docs/local-agent-install.md +0 -370
- package/skills/qingflow-mcp-setup/SKILL.md +0 -115
- package/skills/qingflow-mcp-setup/agents/openai.yaml +0 -4
- package/skills/qingflow-mcp-setup/references/claude-desktop.md +0 -34
- package/skills/qingflow-mcp-setup/references/environments.md +0 -62
- package/skills/qingflow-mcp-setup/references/generic-stdio.md +0 -32
- package/skills/qingflow-mcp-setup/scripts/check_local_server.sh +0 -38
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Iterable
|
|
9
|
+
|
|
10
|
+
from jsonschema import Draft202012Validator, FormatChecker
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
JSONObject = dict[str, Any]
|
|
14
|
+
SCHEMA_ROOT = Path(__file__).with_name("schemas") / "app_form"
|
|
15
|
+
|
|
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
|
+
|
|
127
|
+
class AppFormContractError(ValueError):
|
|
128
|
+
def __init__(self, error_code: str, message: str, *, path: str | None = None, details: JSONObject | None = None) -> None:
|
|
129
|
+
super().__init__(message)
|
|
130
|
+
self.error_code = error_code
|
|
131
|
+
self.message = message
|
|
132
|
+
self.path = path
|
|
133
|
+
self.details = details or {}
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class AppFormSchemaContext:
|
|
138
|
+
schema_version: str
|
|
139
|
+
api_version: str
|
|
140
|
+
complete_schema: JSONObject
|
|
141
|
+
supported_types: tuple[str, ...]
|
|
142
|
+
supported_schema_kinds: tuple[str, ...]
|
|
143
|
+
contract_source: str
|
|
144
|
+
contract_version: str
|
|
145
|
+
normalizer_version: str
|
|
146
|
+
apply_dto: type[AppFormApplyDTO]
|
|
147
|
+
|
|
148
|
+
def validate(self, payload: JSONObject) -> None:
|
|
149
|
+
validator = Draft202012Validator(self.complete_schema, format_checker=FormatChecker())
|
|
150
|
+
errors = sorted(validator.iter_errors(payload), key=lambda item: (list(item.absolute_path), item.message))
|
|
151
|
+
if not errors:
|
|
152
|
+
return
|
|
153
|
+
compact_errors = compact_schema_errors(errors, self.complete_schema)
|
|
154
|
+
error = compact_errors[0] if compact_errors else errors[0]
|
|
155
|
+
pointer = "".join(f"/{str(part).replace('~', '~0').replace('/', '~1')}" for part in error.absolute_path) or "/"
|
|
156
|
+
raise AppFormContractError(
|
|
157
|
+
"INVALID_SPEC",
|
|
158
|
+
schema_error_message(error),
|
|
159
|
+
path=pointer,
|
|
160
|
+
details={"validator": error.validator, "schemaPath": "/".join(str(part) for part in error.absolute_schema_path)},
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def parse_apply(self, payload: JSONObject) -> AppFormApplyDTO:
|
|
164
|
+
self.validate(payload)
|
|
165
|
+
return self.apply_dto(
|
|
166
|
+
contract_source=self.contract_source,
|
|
167
|
+
contract_version=self.contract_version,
|
|
168
|
+
normalizer_version=self.normalizer_version,
|
|
169
|
+
resource=deepcopy(payload),
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
def assert_apply_dto(self, value: AppFormApplyDTO) -> None:
|
|
173
|
+
if not isinstance(value, self.apply_dto):
|
|
174
|
+
raise AppFormContractError(
|
|
175
|
+
"SCHEMA_VERSION_MISMATCH",
|
|
176
|
+
"AppForm declaration was parsed by a different DTO type",
|
|
177
|
+
details={"expected": self.apply_dto.__name__, "actual": type(value).__name__},
|
|
178
|
+
)
|
|
179
|
+
actual = (value.contract_source, value.contract_version, value.normalizer_version)
|
|
180
|
+
expected = (self.contract_source, self.contract_version, self.normalizer_version)
|
|
181
|
+
if actual != expected:
|
|
182
|
+
raise AppFormContractError(
|
|
183
|
+
"SCHEMA_VERSION_MISMATCH",
|
|
184
|
+
"AppForm declaration was parsed by a different contract version",
|
|
185
|
+
details={"expected": expected, "actual": actual},
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@dataclass(frozen=True, slots=True)
|
|
190
|
+
class AppFormApplyDTO:
|
|
191
|
+
contract_source: str
|
|
192
|
+
contract_version: str
|
|
193
|
+
normalizer_version: str
|
|
194
|
+
resource: JSONObject
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@dataclass(frozen=True, slots=True)
|
|
198
|
+
class AppFormV1Alpha1ApplyDTO(AppFormApplyDTO):
|
|
199
|
+
pass
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
APPLY_DTO_TYPES: dict[str, type[AppFormApplyDTO]] = {
|
|
203
|
+
"v1alpha1": AppFormV1Alpha1ApplyDTO,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class AppFormSchemaRegistry:
|
|
208
|
+
def __init__(self, root: Path = SCHEMA_ROOT, *, enabled_versions: tuple[str, ...] | None = None) -> None:
|
|
209
|
+
self.root = root
|
|
210
|
+
self.manifest = self._read_json(root / "manifest.json")
|
|
211
|
+
versions = self.manifest.get("versions")
|
|
212
|
+
if not isinstance(versions, dict) or not versions:
|
|
213
|
+
raise RuntimeError("AppForm schema manifest must define versions")
|
|
214
|
+
configured = enabled_versions or self._configured_versions()
|
|
215
|
+
self.stored_versions = tuple(str(version) for version in versions)
|
|
216
|
+
self.latest_version = str(self.manifest.get("latestVersion") or "")
|
|
217
|
+
self._stored_api_to_version: dict[str, str] = {}
|
|
218
|
+
for version, metadata in versions.items():
|
|
219
|
+
if not isinstance(metadata, dict):
|
|
220
|
+
raise RuntimeError(f"AppForm schema metadata must be an object: {version}")
|
|
221
|
+
api_version = str(metadata.get("apiVersion") or "")
|
|
222
|
+
if not api_version:
|
|
223
|
+
raise RuntimeError(f"AppForm schema version has no apiVersion: {version}")
|
|
224
|
+
if api_version in self._stored_api_to_version:
|
|
225
|
+
raise RuntimeError(f"duplicate AppForm apiVersion: {api_version}")
|
|
226
|
+
self._stored_api_to_version[api_version] = str(version)
|
|
227
|
+
defaults = tuple(
|
|
228
|
+
str(version)
|
|
229
|
+
for version, metadata in versions.items()
|
|
230
|
+
if isinstance(metadata, dict) and bool(metadata.get("enabledByDefault"))
|
|
231
|
+
)
|
|
232
|
+
self.enabled_versions = configured or defaults
|
|
233
|
+
if self.latest_version not in versions:
|
|
234
|
+
raise RuntimeError("AppForm latestVersion is not stored")
|
|
235
|
+
if self.latest_version not in self.enabled_versions:
|
|
236
|
+
raise RuntimeError("AppForm latestVersion must remain enabled")
|
|
237
|
+
self._contexts: dict[str, AppFormSchemaContext] = {}
|
|
238
|
+
self._api_to_version: dict[str, str] = {}
|
|
239
|
+
for version in self.enabled_versions:
|
|
240
|
+
metadata = versions.get(version)
|
|
241
|
+
if not isinstance(metadata, dict):
|
|
242
|
+
raise RuntimeError(f"enabled AppForm schema version is not stored: {version}")
|
|
243
|
+
api_version = str(metadata.get("apiVersion") or "")
|
|
244
|
+
complete = self._read_json(root / version / "complete.json")
|
|
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}")
|
|
249
|
+
if complete.get("properties", {}).get("apiVersion", {}).get("const") != api_version:
|
|
250
|
+
raise RuntimeError(f"AppForm schema apiVersion mismatch for {version}")
|
|
251
|
+
schema_id = str(complete.get("$id") or "")
|
|
252
|
+
if version not in schema_id:
|
|
253
|
+
raise RuntimeError(f"AppForm schema $id mismatch for {version}")
|
|
254
|
+
Draft202012Validator.check_schema(complete)
|
|
255
|
+
self._validate_local_refs(complete, root / version / "complete.json")
|
|
256
|
+
summary = self._read_json(root / version / "summary.json")
|
|
257
|
+
if summary.get("properties", {}).get("apiVersion", {}).get("const") != api_version:
|
|
258
|
+
raise RuntimeError(f"AppForm summary apiVersion mismatch for {version}")
|
|
259
|
+
Draft202012Validator.check_schema(summary)
|
|
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)
|
|
266
|
+
for field_type in supported:
|
|
267
|
+
field_path = root / version / "fields" / f"{field_type}.json"
|
|
268
|
+
field_schema = self._read_json(field_path)
|
|
269
|
+
Draft202012Validator.check_schema(field_schema)
|
|
270
|
+
self._validate_local_refs(field_schema, field_path)
|
|
271
|
+
context = AppFormSchemaContext(
|
|
272
|
+
version,
|
|
273
|
+
api_version,
|
|
274
|
+
complete,
|
|
275
|
+
supported,
|
|
276
|
+
supported_schema_kinds,
|
|
277
|
+
str(metadata.get("contractSource") or "app_form"),
|
|
278
|
+
str(metadata.get("contractVersion") or version),
|
|
279
|
+
str(metadata.get("normalizerVersion") or version),
|
|
280
|
+
self._apply_dto_type(version, metadata),
|
|
281
|
+
)
|
|
282
|
+
self._contexts[version] = context
|
|
283
|
+
self._api_to_version[api_version] = version
|
|
284
|
+
|
|
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:
|
|
292
|
+
context = self.resolve_schema_version(schema_version)
|
|
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
|
+
)
|
|
316
|
+
schema = self._read_json(self.root / context.schema_version / "summary.json")
|
|
317
|
+
resolved_kind = "summary"
|
|
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
|
+
)
|
|
325
|
+
if field_type not in context.supported_types:
|
|
326
|
+
raise AppFormContractError(
|
|
327
|
+
"UNSUPPORTED_SCHEMA_FIELD_TYPE",
|
|
328
|
+
f"unsupported AppForm field type: {field_type}",
|
|
329
|
+
details={"supportedTypes": list(context.supported_types)},
|
|
330
|
+
)
|
|
331
|
+
schema = self._read_json(self.root / context.schema_version / "fields" / f"{field_type}.json")
|
|
332
|
+
resolved_kind = "field"
|
|
333
|
+
return {
|
|
334
|
+
"schemaVersion": context.schema_version,
|
|
335
|
+
"apiVersion": context.api_version,
|
|
336
|
+
"latestVersion": self.latest_version,
|
|
337
|
+
"storedVersions": list(self.stored_versions),
|
|
338
|
+
"enabledVersions": list(self.enabled_versions),
|
|
339
|
+
"supportedSchemaKinds": list(context.supported_schema_kinds),
|
|
340
|
+
"schemaKind": resolved_kind,
|
|
341
|
+
"fieldType": field_type,
|
|
342
|
+
"schema": schema,
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
def resolve_schema_version(self, schema_version: str | None) -> AppFormSchemaContext:
|
|
346
|
+
version = str(schema_version or self.latest_version).strip()
|
|
347
|
+
if version not in self.stored_versions:
|
|
348
|
+
raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", version)
|
|
349
|
+
if version not in self.enabled_versions:
|
|
350
|
+
raise self._version_error("SCHEMA_VERSION_DISABLED", version)
|
|
351
|
+
return self._resolved_context(version)
|
|
352
|
+
|
|
353
|
+
def resolve_api_version(self, api_version: str) -> AppFormSchemaContext:
|
|
354
|
+
version = self._api_to_version.get(api_version)
|
|
355
|
+
if version is not None:
|
|
356
|
+
return self._resolved_context(version)
|
|
357
|
+
stored = self._stored_api_to_version.get(api_version)
|
|
358
|
+
if stored is not None:
|
|
359
|
+
raise self._version_error("SCHEMA_VERSION_DISABLED", stored)
|
|
360
|
+
raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", api_version)
|
|
361
|
+
|
|
362
|
+
def _resolved_context(self, version: str) -> AppFormSchemaContext:
|
|
363
|
+
return self._contexts[version]
|
|
364
|
+
|
|
365
|
+
@staticmethod
|
|
366
|
+
def _apply_dto_type(version: str, metadata: JSONObject) -> type[AppFormApplyDTO]:
|
|
367
|
+
dto_name = str(metadata.get("applyDto") or version)
|
|
368
|
+
dto_type = APPLY_DTO_TYPES.get(dto_name)
|
|
369
|
+
if dto_type is None:
|
|
370
|
+
raise RuntimeError(f"AppForm schema version {version} references unknown apply DTO: {dto_name}")
|
|
371
|
+
return dto_type
|
|
372
|
+
|
|
373
|
+
def _version_error(self, error_code: str, requested: str) -> AppFormContractError:
|
|
374
|
+
return AppFormContractError(
|
|
375
|
+
error_code,
|
|
376
|
+
f"unsupported AppForm schema selection: {requested}",
|
|
377
|
+
details={
|
|
378
|
+
"requestedVersion": requested,
|
|
379
|
+
"latestVersion": self.latest_version,
|
|
380
|
+
"storedVersions": list(self.stored_versions),
|
|
381
|
+
"enabledVersions": list(self.enabled_versions),
|
|
382
|
+
},
|
|
383
|
+
)
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def _read_json(path: Path) -> JSONObject:
|
|
387
|
+
value = json.loads(path.read_text(encoding="utf-8"))
|
|
388
|
+
if not isinstance(value, dict):
|
|
389
|
+
raise RuntimeError(f"AppForm schema artifact must be an object: {path}")
|
|
390
|
+
return value
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def _validate_local_refs(document: JSONObject, path: Path) -> None:
|
|
394
|
+
stack: list[Any] = [document]
|
|
395
|
+
while stack:
|
|
396
|
+
value = stack.pop()
|
|
397
|
+
if isinstance(value, dict):
|
|
398
|
+
ref = value.get("$ref")
|
|
399
|
+
if ref is not None:
|
|
400
|
+
if not isinstance(ref, str) or (ref != "#" and not ref.startswith("#/")):
|
|
401
|
+
raise RuntimeError(f"AppForm schema contains an external or invalid $ref in {path}: {ref}")
|
|
402
|
+
target: Any = document
|
|
403
|
+
for raw_part in ref[2:].split("/") if ref.startswith("#/") else ():
|
|
404
|
+
part = raw_part.replace("~1", "/").replace("~0", "~")
|
|
405
|
+
if not isinstance(target, dict) or part not in target:
|
|
406
|
+
raise RuntimeError(f"AppForm schema contains a dangling $ref in {path}: {ref}")
|
|
407
|
+
target = target[part]
|
|
408
|
+
stack.extend(value.values())
|
|
409
|
+
elif isinstance(value, list):
|
|
410
|
+
stack.extend(value)
|
|
411
|
+
|
|
412
|
+
@staticmethod
|
|
413
|
+
def _configured_versions() -> tuple[str, ...]:
|
|
414
|
+
raw = os.environ.get("QINGFLOW_APP_FORM_SCHEMA_VERSIONS", "")
|
|
415
|
+
return tuple(part.strip() for part in raw.split(",") if part.strip())
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
_REGISTRY: AppFormSchemaRegistry | None = None
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def app_form_schema_registry() -> AppFormSchemaRegistry:
|
|
422
|
+
global _REGISTRY
|
|
423
|
+
if _REGISTRY is None:
|
|
424
|
+
_REGISTRY = AppFormSchemaRegistry()
|
|
425
|
+
return _REGISTRY
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"latestVersion": "v1alpha1",
|
|
3
|
+
"versions": {
|
|
4
|
+
"v1alpha1": {
|
|
5
|
+
"apiVersion": "builder.qingflow.com/v1alpha1",
|
|
6
|
+
"applyDto": "v1alpha1",
|
|
7
|
+
"contractSource": "app_form",
|
|
8
|
+
"contractVersion": "v1alpha1",
|
|
9
|
+
"enabledByDefault": true,
|
|
10
|
+
"fieldTypes": [
|
|
11
|
+
"text",
|
|
12
|
+
"long_text",
|
|
13
|
+
"number",
|
|
14
|
+
"amount",
|
|
15
|
+
"link",
|
|
16
|
+
"date",
|
|
17
|
+
"datetime",
|
|
18
|
+
"member",
|
|
19
|
+
"department",
|
|
20
|
+
"single_choice",
|
|
21
|
+
"single_select",
|
|
22
|
+
"multi_select",
|
|
23
|
+
"phone",
|
|
24
|
+
"email",
|
|
25
|
+
"address",
|
|
26
|
+
"attachment",
|
|
27
|
+
"q_linker",
|
|
28
|
+
"code_block",
|
|
29
|
+
"data_relation",
|
|
30
|
+
"relation",
|
|
31
|
+
"subtable"
|
|
32
|
+
],
|
|
33
|
+
"normalizerVersion": "v1alpha1",
|
|
34
|
+
"schemaKinds": [
|
|
35
|
+
"summary",
|
|
36
|
+
"field",
|
|
37
|
+
"formula"
|
|
38
|
+
],
|
|
39
|
+
"status": "active"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|