@qingflow-tech/qingflow-cli 1.1.43 → 1.1.44-beta.2

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 (77) hide show
  1. package/README.md +13 -6
  2. package/entry_point.py +1 -1
  3. package/npm/bin/qingflow.mjs +1 -2
  4. package/npm/lib/runtime.mjs +3 -7
  5. package/package.json +2 -2
  6. package/pyproject.toml +3 -4
  7. package/skills/qingflow-app-form/SKILL.md +66 -0
  8. package/skills/qingflow-app-form/agents/openai.yaml +4 -0
  9. package/skills/qingflow-cli/SKILL.md +8 -6
  10. package/skills/qingflow-cli/manifest.yaml +1 -1
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_APP_DELIVERY_WORKFLOW.md +4 -4
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_SCHEMA_APPLY_FIELD_TYPES_AND_SCENARIOS.md +4 -4
  15. package/skills/qingflow-cli/reference/builder/10-build-single-app.md +5 -5
  16. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +14 -14
  17. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +108 -103
  18. package/skills/qingflow-cli/reference/builder/40-layout.md +39 -31
  19. package/skills/qingflow-cli/reference/builder/README.md +9 -9
  20. package/skills/qingflow-cli/reference/builder/code-integrations/README.md +4 -2
  21. package/skills/qingflow-cli/reference/builder/code-integrations/code-block.md +2 -0
  22. package/skills/qingflow-cli/reference/builder/code-integrations/q-linker.md +2 -0
  23. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +45 -38
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +1 -1
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +1 -1
  26. package/skills/qingflow-cli/reference/examples/README.md +3 -3
  27. package/skills/qingflow-common/SKILL.md +47 -0
  28. package/skills/qingflow-common/agents/openai.yaml +4 -0
  29. package/src/qingflow_mcp/__main__.py +2 -3
  30. package/src/qingflow_mcp/app_form_compat.py +104 -0
  31. package/src/qingflow_mcp/app_form_validator.py +462 -0
  32. package/src/qingflow_mcp/builder_facade/app_form.py +4205 -0
  33. package/src/qingflow_mcp/builder_facade/app_form_schema.py +425 -0
  34. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +42 -0
  35. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +2969 -0
  36. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +392 -0
  37. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +236 -0
  38. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +425 -0
  39. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +318 -0
  40. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +580 -0
  41. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +150 -0
  42. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +150 -0
  43. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +345 -0
  44. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +190 -0
  45. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +190 -0
  46. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +190 -0
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +484 -0
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +214 -0
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +236 -0
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +199 -0
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +412 -0
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +454 -0
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +214 -0
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +214 -0
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +1921 -0
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +190 -0
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +1594 -0
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +718 -0
  59. package/src/qingflow_mcp/builder_facade/service.py +8 -4
  60. package/src/qingflow_mcp/cli/commands/builder.py +93 -28
  61. package/src/qingflow_mcp/cli/json_io.py +21 -2
  62. package/src/qingflow_mcp/cli/main.py +3 -39
  63. package/src/qingflow_mcp/public_surface.py +5 -2
  64. package/src/qingflow_mcp/response_trim.py +50 -0
  65. package/src/qingflow_mcp/server.py +24 -3
  66. package/src/qingflow_mcp/server_app_builder.py +44 -87
  67. package/src/qingflow_mcp/tools/ai_builder_tools.py +103 -100
  68. package/src/qingflow_mcp/tools/app_tools.py +46 -65
  69. package/src/qingflow_mcp/tools/record_tools.py +3 -3
  70. package/src/qingflow_mcp/version.py +2 -4
  71. package/docs/local-agent-install.md +0 -370
  72. package/skills/qingflow-mcp-setup/SKILL.md +0 -115
  73. package/skills/qingflow-mcp-setup/agents/openai.yaml +0 -4
  74. package/skills/qingflow-mcp-setup/references/claude-desktop.md +0 -34
  75. package/skills/qingflow-mcp-setup/references/environments.md +0 -62
  76. package/skills/qingflow-mcp-setup/references/generic-stdio.md +0 -32
  77. 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
+ }