@qingflow-tech/qingflow-cli 1.1.43-beta.4 → 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 (36) 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 +7 -6
  5. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +3 -2
  6. package/src/qingflow_mcp/app_form_validator.py +97 -0
  7. package/src/qingflow_mcp/builder_facade/app_form.py +15 -2
  8. package/src/qingflow_mcp/builder_facade/app_form_schema.py +50 -5
  9. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +5 -0
  10. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +7 -12
  11. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +1 -1
  12. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +6 -4
  13. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +1 -1
  14. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +1 -1
  15. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +1 -1
  16. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +1 -1
  17. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +1 -1
  18. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +1 -1
  19. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +1 -1
  20. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +1 -1
  21. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +1 -1
  22. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +1 -1
  23. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +1 -1
  24. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +6 -4
  25. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +1 -1
  26. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +1 -1
  27. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +1 -1
  28. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +1 -1
  29. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +1 -1
  30. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +7 -12
  31. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +1 -1
  32. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +1594 -0
  33. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +1 -1
  34. package/src/qingflow_mcp/cli/commands/builder.py +2 -0
  35. package/src/qingflow_mcp/server_app_builder.py +7 -2
  36. package/src/qingflow_mcp/tools/ai_builder_tools.py +13 -3
package/README.md CHANGED
@@ -7,7 +7,7 @@ Qingflow CLI 是轻流面向开发者和 AI Agent 的命令行工具。它把轻
7
7
  ## 安装
8
8
 
9
9
  ```bash
10
- npm install @qingflow-tech/qingflow-cli@1.1.43-beta.4
10
+ npm install @qingflow-tech/qingflow-cli@1.1.43-beta.5
11
11
  ```
12
12
 
13
13
  推荐从官网复制这一条一键安装 Qingflow CLI:
@@ -21,7 +21,7 @@ npx qingflow-cli@latest install
21
21
  ## 运行
22
22
 
23
23
  ```bash
24
- npx -y -p @qingflow-tech/qingflow-cli@1.1.43-beta.4 qingflow
24
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.43-beta.5 qingflow
25
25
  ```
26
26
 
27
27
  全局安装后可以直接运行:
@@ -62,7 +62,7 @@ qingflow-skills install --agent codex --scope user
62
62
  如果你通过一次性 `npx -p` 执行 skills 安装,建议加 `--copy`,避免 symlink 指向 npm 临时执行缓存:
63
63
 
64
64
  ```bash
65
- npx -y -p @qingflow-tech/qingflow-cli@1.1.43-beta.4 qingflow-skills install --agent codex --scope user --copy
65
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.43-beta.5 qingflow-skills install --agent codex --scope user --copy
66
66
  ```
67
67
 
68
68
  `qingflow-skills install` 默认使用 symlink,且遇到同名 skill 不会覆盖;只有显式传入 `--force` 才会替换。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-cli",
3
- "version": "1.1.43-beta.4",
3
+ "version": "1.1.43-beta.5",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.1.43b4"
7
+ version = "1.1.43b5"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -21,11 +21,12 @@ Before creating an app, let AppForm derive a stable default icon and color from
21
21
 
22
22
  1. Read the AppForm Schema and pin its `schemaVersion` and `apiVersion` for the entire operation.
23
23
  2. Read the pinned field Schema for every canonical field type used, including subtable child types.
24
- 3. For an update, read the draft form with `being_draft=true`. Preserve every field, row, section, and subfield that should remain. For a create, build a declaration without `appKey` and with `packageId`, `spec.appName`, and complete `spec.body`; AppForm derives the icon and color from `spec.appName`.
25
- 4. Keep existing `queId`, `sectionId`, `attachId`, and `aliasId` values from get. Omit them only for new resources. Treat `body`, `subfields`, explicit `attachments`, and custom serial-number `components` as complete target state.
26
- 5. Save the declaration as JSON. In CLI, run `qingflow --json builder app-form validate --schema-version PINNED --file DECLARATION.json` before every apply and fix every reported pointer. In MCP, call apply directly; it performs the same declaration validation internally. YAML is not accepted.
27
- 6. Apply the declaration on the selected surface. Record a newly returned `appKey` immediately. On success, show only `operation`, the non-empty `changes/actions` summary, and warnings. On any non-success result, show the recovery diagnostics, especially deleted/changed field names in `diff/remainingDiff`, and the failed phase.
28
- 7. A successful apply has already read back the draft, verified the complete target, and verified the published form. Do not call get only to verify it again. For any non-success result, follow Recovery; never replay a saved backend payload or edit version.
24
+ 3. When a field uses `defaultType: 3` or another config accepts a formula, read the pinned formula Schema with CLI `--schema-kind formula` or MCP `schema_kind="formula"`; use its field-reference, operator, and function catalog instead of inventing formula syntax.
25
+ 4. For an update, read the draft form with `being_draft=true`. Preserve every field, row, section, and subfield that should remain. For a create, build a declaration without `appKey` and with `packageId`, `spec.appName`, and complete `spec.body`; AppForm derives the icon and color from `spec.appName`.
26
+ 5. Keep existing `queId`, `sectionId`, `attachId`, and `aliasId` values from get. Omit them only for new resources. Treat `body`, `subfields`, explicit `attachments`, and custom serial-number `components` as complete target state.
27
+ 6. Save the declaration as JSON. In CLI, run `qingflow --json builder app-form validate --schema-version PINNED --file DECLARATION.json` before every apply and fix every reported pointer. In MCP, call apply directly; it performs the same declaration validation internally. YAML is not accepted.
28
+ 7. Apply the declaration on the selected surface. Record a newly returned `appKey` immediately. On success, show only `operation`, the non-empty `changes/actions` summary, and warnings. On any non-success result, show the recovery diagnostics, especially deleted/changed field names in `diff/remainingDiff`, and the failed phase.
29
+ 8. A successful apply has already read back the draft, verified the complete target, and verified the published form. Do not call get only to verify it again. For any non-success result, follow Recovery; never replay a saved backend payload or edit version.
29
30
 
30
31
  ## Read Source
31
32
 
@@ -56,7 +57,7 @@ Before creating an app, let AppForm derive a stable default icon and color from
56
57
 
57
58
  - Pin one Schema version; never combine details from different versions or silently upgrade recovery work.
58
59
  - Ordinary and section rows contain one to four fields. A section is its own outer block. Subtable columns use only the allowed subfield types returned by the subtable detail Schema.
59
- - Field declarations may set `defaultType` (`1` static, `2` relation/binding, `3` formula) and `defaultValue`; `defaultValue` is `string | null` because the backend `queDefaultValue` is a string, so numeric literals must be passed as strings. Use `null` to clear a default. These properties also apply to subtable fields. Choice defaults belong in `config.defaultOptions`; address, attachment, subtable, and relation defaults belong in `config.defaultValues`. Omit a default when none is intended.
60
+ - Field declarations may set `defaultType` (`1` static, `2` relation/binding, `3` formula) and `defaultValue`; `defaultValue` is `string | null` because the backend `queDefaultValue` is a string, so numeric literals must be passed as strings. For `defaultType: 3`, query the same version with `--schema-kind formula` or `schema_kind="formula"` before composing the formula. Use `null` to clear a default. These properties also apply to subtable fields. Choice defaults belong in `config.defaultOptions`; address, attachment, subtable, and relation defaults belong in `config.defaultValues`. Omit a default when none is intended.
60
61
  - For `code_block`, when `config.codeBlockBinding.outputs` is non-empty, the `code` must explicitly assign `qf_output`, for example `qf_output = { score: input.score };`; `return {...}` alone does not satisfy output writeback, and each output `path` must point under `qf_output`.
61
62
  - A relation formula default is `config.defaultValueFormula` and must satisfy the pinned `relation` field Schema's `FormulaExpression`; do not infer its syntax from another version or from backend DTO names.
62
63
  - If widths are present, every field in that row must have one and the total must equal 100.
@@ -164,7 +164,7 @@ current CLI payload.
164
164
 
165
165
  ## Field type reference
166
166
 
167
- > **口径**:`field.questionType` 枚举和字段形状以固定版本的 **`qingflow --json builder app-form schema`** 返回结果为准;每个 canonical 类型还可用 `--field-type TYPE` 获取细节。不要从已退休的 `app_schema_apply` contract 推断当前 AppForm payload。
167
+ > **口径**:`field.questionType` 枚举和字段形状以固定版本的 **`qingflow --json builder app-form schema`** 返回结果为准;每个 canonical 类型还可用 `--field-type TYPE` 获取细节,`defaultType: 3` 的公式语法和函数目录用 `--schema-kind formula` 获取。不要从已退休的 `app_schema_apply` contract 推断当前 AppForm payload。
168
168
  > **CLI**:表单写入使用 `qingflow [--profile NAME] --json builder app-form schema/get/validate/apply`;`validate/apply` 接受 `--file`,`get` 接受 `--app-key`。旧 `builder schema apply` 及 `builder layout apply` 不在当前 parser 的公开命令集合中。
169
169
  > **新建应用主规则**:`spec.body` 非空时必须用 `spec.dataTitleField` 唯一选择一个顶层字段作为数据标题;数据封面可选,只能用 `spec.dataCoverField` 选择顶层 `attachment` 字段。
170
170
  > **权限**:包内新建应用按后端 `CreateAppBean` 链路只预检目标包 **AddAppAuth**;已有应用的字段/基础信息变更才走应用自身 **EditAppAuth**。
@@ -292,7 +292,8 @@ current CLI payload.
292
292
  |------|------|
293
293
  | **当前完整系统主入口** | [20-build-complete-system.md](./20-build-complete-system.md) |
294
294
  | **历史字段类型能力探针(不是新建主模板)** | [schema_apply_add_fields_all_types.json](../examples/schema/schema_apply_add_fields_all_types.json)(仅用于旧字段能力覆盖测试;当前新建和更新统一使用上文 AppForm) |
295
- | **`q_linker` / `code_block` / relation 公式**(契约内置形状) | `qingflow --json builder app-form schema --schema-version VERSION --field-type TYPE` 返回的固定版本字段详情 |
295
+ | **`q_linker` / `code_block` / relation 配置**(契约内置形状) | `qingflow --json builder app-form schema --schema-version VERSION --field-type TYPE` 返回的固定版本字段详情 |
296
+ | **默认值与其他公式文本** | `qingflow --json builder app-form schema --schema-version VERSION --schema-kind formula` 返回的同版本公式语法、字段引用、运算符和函数目录 |
296
297
  | 最小新建字段(1 个 text 标题字段) | [schema_add_fields_minimal.example.json](../examples/schema/schema_add_fields_minimal.example.json) |
297
298
  | 历史 scalar / complex 批测(可选) | [_batch_schema_scalar.json](../examples/schema/_batch_schema_scalar.json)、[_batch_schema_complex.json](../examples/schema/_batch_schema_complex.json) |
298
299
 
@@ -23,6 +23,7 @@ _INTEGRATION_OUTPUT_TARGET_FIELD_TYPES = {
23
23
  "multi_select",
24
24
  }
25
25
  _Q_LINKER_TEMPLATE_RE = re.compile(r"\{\{(?P<key>[^{}\s]+)\}\}")
26
+ _FORMULA_IDENTIFIER_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")
26
27
 
27
28
 
28
29
  class AppFormValidationError(ValueError):
@@ -68,6 +69,18 @@ def _local_schema_call(schema_version: str | None, field_type: str | None) -> di
68
69
  return {"status": "success", **app_form_schema_registry().schema(schema_version=schema_version, field_type=field_type)}
69
70
 
70
71
 
72
+ def _supported_formula_functions(schema_version: str) -> frozenset[str]:
73
+ """Load the version-pinned formula function catalogue used by local validation."""
74
+ from .builder_facade.app_form_schema import app_form_schema_registry
75
+
76
+ response = app_form_schema_registry().schema(schema_version=schema_version, schema_kind="formula")
77
+ schema = response.get("schema") if isinstance(response, dict) else None
78
+ functions = schema.get("x-qingflow-functions") if isinstance(schema, dict) else None
79
+ if not isinstance(functions, dict) or not functions:
80
+ raise RuntimeError(f"/: AppForm formula Schema has no function catalogue for {schema_version}")
81
+ return frozenset(str(name).upper() for name in functions if isinstance(name, str) and name)
82
+
83
+
71
84
  def _compose(summary: dict[str, Any], details: list[dict[str, Any]]) -> dict[str, Any]:
72
85
  if not details:
73
86
  raise RuntimeError("at least one field Schema is required to compose AppForm validation")
@@ -101,6 +114,87 @@ def _pointer(parts: Iterable[Any]) -> str:
101
114
  return "/" + "/".join(encoded) if encoded else "/"
102
115
 
103
116
 
117
+ def _formula_function_calls(expression: str) -> set[str]:
118
+ """Extract calls outside quoted string literals without parsing the full formula grammar."""
119
+ calls: set[str] = set()
120
+ index = 0
121
+ while index < len(expression):
122
+ character = expression[index]
123
+ if character in {"'", '"'}:
124
+ quote = character
125
+ index += 1
126
+ while index < len(expression):
127
+ if expression[index] == "\\":
128
+ index += 2
129
+ continue
130
+ if expression[index] == quote:
131
+ index += 1
132
+ break
133
+ index += 1
134
+ continue
135
+ match = _FORMULA_IDENTIFIER_RE.match(expression, index)
136
+ if match is None:
137
+ index += 1
138
+ continue
139
+ name = match.group(0)
140
+ cursor = match.end()
141
+ while cursor < len(expression) and expression[cursor].isspace():
142
+ cursor += 1
143
+ if cursor < len(expression) and expression[cursor] in {"(", "("}:
144
+ calls.add(name)
145
+ index = match.end()
146
+ return calls
147
+
148
+
149
+ def _iter_formula_expressions(payload: dict[str, Any]) -> Iterable[tuple[str, str]]:
150
+ spec = payload.get("spec")
151
+ body = spec.get("body") if isinstance(spec, dict) else None
152
+ if not isinstance(body, list):
153
+ return
154
+
155
+ def visit(field: Any, path: str) -> Iterable[tuple[str, str]]:
156
+ if not isinstance(field, dict):
157
+ return
158
+ if field.get("defaultType") == 3 and isinstance(field.get("defaultValue"), str):
159
+ yield f"{path}/defaultValue", field["defaultValue"]
160
+ config = field.get("config") if isinstance(field.get("config"), dict) else {}
161
+ default_value_formula = config.get("defaultValueFormula")
162
+ if isinstance(default_value_formula, str):
163
+ yield f"{path}/config/defaultValueFormula", default_value_formula
164
+ filters = config.get("filters") if isinstance(config.get("filters"), dict) else {}
165
+ if filters.get("mode") == "advanced" and isinstance(filters.get("formula"), str):
166
+ yield f"{path}/config/filters/formula", filters["formula"]
167
+ for child_index, child in enumerate(field.get("subfields") or []):
168
+ yield from visit(child, f"{path}/subfields/{child_index}")
169
+
170
+ for block_index, block in enumerate(body):
171
+ if not isinstance(block, dict):
172
+ continue
173
+ rows = [(block, f"/spec/body/{block_index}")] if block.get("kind") == "row" else [
174
+ (row, f"/spec/body/{block_index}/rows/{row_index}")
175
+ for row_index, row in enumerate(block.get("rows") or [])
176
+ ]
177
+ for row, row_path in rows:
178
+ if not isinstance(row, dict):
179
+ continue
180
+ for field_index, field in enumerate(row.get("fields") or []):
181
+ yield from visit(field, f"{row_path}/fields/{field_index}")
182
+
183
+
184
+ def _formula_errors(payload: dict[str, Any], supported_functions: frozenset[str]) -> list[tuple[str, str]]:
185
+ errors: list[tuple[str, str]] = []
186
+ for pointer, expression in _iter_formula_expressions(payload):
187
+ unknown = sorted(name for name in _formula_function_calls(expression) if name.upper() not in supported_functions)
188
+ for name in unknown:
189
+ errors.append(
190
+ (
191
+ pointer,
192
+ f"unsupported formula function '{name}'; use a function declared by builder app-form schema --schema-kind formula",
193
+ )
194
+ )
195
+ return errors
196
+
197
+
104
198
  def _static_errors(payload: dict[str, Any]) -> list[tuple[str, str]]:
105
199
  errors: list[tuple[str, str]] = []
106
200
  spec = payload.get("spec")
@@ -346,6 +440,7 @@ def validate_declaration(
346
440
  ) from error
347
441
  if declaration.get("apiVersion") != api_version:
348
442
  raise ValueError(f"/apiVersion: expected {api_version!r} for schemaVersion {pinned!r}")
443
+ supported_formula_functions = _supported_formula_functions(pinned)
349
444
  types = _field_types(declaration)
350
445
  details = [schema_call(pinned, field_type) for field_type in (types or ("text",))]
351
446
  for detail in details:
@@ -359,6 +454,8 @@ def validate_declaration(
359
454
  found[(_pointer(error.absolute_path), f"{error.validator}: {schema_error_message(error)}")] = None
360
455
  for pointer, message in _static_errors(declaration):
361
456
  found[(pointer, message)] = None
457
+ for pointer, message in _formula_errors(declaration, supported_formula_functions):
458
+ found[(pointer, message)] = None
362
459
  if found:
363
460
  issues = [{"pointer": pointer, "message": message} for pointer, message in sorted(found)]
364
461
  raise AppFormValidationError(issues, schema_version=pinned, api_version=api_version)
@@ -2834,9 +2834,22 @@ class AppFormReconciler:
2834
2834
  self.normalizer = AppFormNormalizer()
2835
2835
  self.compiler = AppFormCompiler(apps)
2836
2836
 
2837
- def schema(self, *, schema_version: str | None, field_type: str | None) -> JSONObject:
2837
+ def schema(
2838
+ self,
2839
+ *,
2840
+ schema_version: str | None,
2841
+ field_type: str | None,
2842
+ schema_kind: str | None = None,
2843
+ ) -> JSONObject:
2838
2844
  try:
2839
- return {"status": "success", **self.registry.schema(schema_version=schema_version, field_type=field_type)}
2845
+ return {
2846
+ "status": "success",
2847
+ **self.registry.schema(
2848
+ schema_version=schema_version,
2849
+ field_type=field_type,
2850
+ schema_kind=schema_kind,
2851
+ ),
2852
+ }
2840
2853
  except AppFormContractError as error:
2841
2854
  return self._failure(error.error_code, error.message, "SCHEMA", details=error.details)
2842
2855
 
@@ -139,6 +139,7 @@ class AppFormSchemaContext:
139
139
  api_version: str
140
140
  complete_schema: JSONObject
141
141
  supported_types: tuple[str, ...]
142
+ supported_schema_kinds: tuple[str, ...]
142
143
  contract_source: str
143
144
  contract_version: str
144
145
  normalizer_version: str
@@ -242,6 +243,9 @@ class AppFormSchemaRegistry:
242
243
  api_version = str(metadata.get("apiVersion") or "")
243
244
  complete = self._read_json(root / version / "complete.json")
244
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}")
245
249
  if complete.get("properties", {}).get("apiVersion", {}).get("const") != api_version:
246
250
  raise RuntimeError(f"AppForm schema apiVersion mismatch for {version}")
247
251
  schema_id = str(complete.get("$id") or "")
@@ -254,6 +258,11 @@ class AppFormSchemaRegistry:
254
258
  raise RuntimeError(f"AppForm summary apiVersion mismatch for {version}")
255
259
  Draft202012Validator.check_schema(summary)
256
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)
257
266
  for field_type in supported:
258
267
  field_path = root / version / "fields" / f"{field_type}.json"
259
268
  field_schema = self._read_json(field_path)
@@ -264,6 +273,7 @@ class AppFormSchemaRegistry:
264
273
  api_version,
265
274
  complete,
266
275
  supported,
276
+ supported_schema_kinds,
267
277
  str(metadata.get("contractSource") or "app_form"),
268
278
  str(metadata.get("contractVersion") or version),
269
279
  str(metadata.get("normalizerVersion") or version),
@@ -272,12 +282,46 @@ class AppFormSchemaRegistry:
272
282
  self._contexts[version] = context
273
283
  self._api_to_version[api_version] = version
274
284
 
275
- 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:
276
292
  context = self.resolve_schema_version(schema_version)
277
- 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
+ )
278
316
  schema = self._read_json(self.root / context.schema_version / "summary.json")
279
- schema_kind = "summary"
317
+ resolved_kind = "summary"
280
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
+ )
281
325
  if field_type not in context.supported_types:
282
326
  raise AppFormContractError(
283
327
  "UNSUPPORTED_SCHEMA_FIELD_TYPE",
@@ -285,14 +329,15 @@ class AppFormSchemaRegistry:
285
329
  details={"supportedTypes": list(context.supported_types)},
286
330
  )
287
331
  schema = self._read_json(self.root / context.schema_version / "fields" / f"{field_type}.json")
288
- schema_kind = "field"
332
+ resolved_kind = "field"
289
333
  return {
290
334
  "schemaVersion": context.schema_version,
291
335
  "apiVersion": context.api_version,
292
336
  "latestVersion": self.latest_version,
293
337
  "storedVersions": list(self.stored_versions),
294
338
  "enabledVersions": list(self.enabled_versions),
295
- "schemaKind": schema_kind,
339
+ "supportedSchemaKinds": list(context.supported_schema_kinds),
340
+ "schemaKind": resolved_kind,
296
341
  "fieldType": field_type,
297
342
  "schema": schema,
298
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
  }
@@ -1593,10 +1593,12 @@
1593
1593
  ]
1594
1594
  },
1595
1595
  "currencyType": {
1596
- "default": 1,
1597
- "description": "货币类型 ID;后端发布要求为非空正整数,即使 currencyStatus 为 false 也不得传 null。无需指定时省略并使用默认值 1。",
1596
+ "description": "货币类型 ID。",
1598
1597
  "minimum": 1,
1599
- "type": "integer"
1598
+ "type": [
1599
+ "integer",
1600
+ "null"
1601
+ ]
1600
1602
  },
1601
1603
  "decimalDigit": {
1602
1604
  "description": "小数位数。",
@@ -2083,7 +2085,7 @@
2083
2085
  "type": "object"
2084
2086
  },
2085
2087
  "ScalarDefaultValue": {
2086
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
2088
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
2087
2089
  "type": [
2088
2090
  "null",
2089
2091
  "string"
@@ -2624,14 +2626,7 @@
2624
2626
  ]
2625
2627
  },
2626
2628
  "tableRowHeight": {
2627
- "description": "子表行高;只允许 compact(紧凑)、moderate(适中)、easy(宽松)或 highest(最高)。省略表示保留现有值,null 表示清除并使用后端默认的紧凑行高。",
2628
- "enum": [
2629
- "compact",
2630
- "moderate",
2631
- "easy",
2632
- "highest",
2633
- null
2634
- ],
2629
+ "minLength": 1,
2635
2630
  "type": [
2636
2631
  "string",
2637
2632
  "null"
@@ -359,7 +359,7 @@
359
359
  "type": "object"
360
360
  },
361
361
  "ScalarDefaultValue": {
362
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
362
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
363
363
  "type": [
364
364
  "null",
365
365
  "string"
@@ -137,10 +137,12 @@
137
137
  ]
138
138
  },
139
139
  "currencyType": {
140
- "default": 1,
141
- "description": "货币类型 ID;后端发布要求为非空正整数,即使 currencyStatus 为 false 也不得传 null。无需指定时省略并使用默认值 1。",
140
+ "description": "货币类型 ID。",
142
141
  "minimum": 1,
143
- "type": "integer"
142
+ "type": [
143
+ "integer",
144
+ "null"
145
+ ]
144
146
  },
145
147
  "decimalDigit": {
146
148
  "description": "小数位数。",
@@ -201,7 +203,7 @@
201
203
  "type": "object"
202
204
  },
203
205
  "ScalarDefaultValue": {
204
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
206
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
205
207
  "type": [
206
208
  "null",
207
209
  "string"
@@ -392,7 +392,7 @@
392
392
  "type": "object"
393
393
  },
394
394
  "ScalarDefaultValue": {
395
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
395
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
396
396
  "type": [
397
397
  "null",
398
398
  "string"
@@ -284,7 +284,7 @@
284
284
  "type": "object"
285
285
  },
286
286
  "ScalarDefaultValue": {
287
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
287
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
288
288
  "type": [
289
289
  "null",
290
290
  "string"
@@ -546,7 +546,7 @@
546
546
  "type": "object"
547
547
  },
548
548
  "ScalarDefaultValue": {
549
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
549
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
550
550
  "type": [
551
551
  "null",
552
552
  "string"
@@ -117,7 +117,7 @@
117
117
  "type": "object"
118
118
  },
119
119
  "ScalarDefaultValue": {
120
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
120
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
121
121
  "type": [
122
122
  "null",
123
123
  "string"
@@ -117,7 +117,7 @@
117
117
  "type": "object"
118
118
  },
119
119
  "ScalarDefaultValue": {
120
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
120
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
121
121
  "type": [
122
122
  "null",
123
123
  "string"
@@ -264,7 +264,7 @@
264
264
  "type": "object"
265
265
  },
266
266
  "ScalarDefaultValue": {
267
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
267
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
268
268
  "type": [
269
269
  "null",
270
270
  "string"
@@ -112,7 +112,7 @@
112
112
  "type": "object"
113
113
  },
114
114
  "ScalarDefaultValue": {
115
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
115
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
116
116
  "type": [
117
117
  "null",
118
118
  "string"
@@ -112,7 +112,7 @@
112
112
  "type": "object"
113
113
  },
114
114
  "ScalarDefaultValue": {
115
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
115
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
116
116
  "type": [
117
117
  "null",
118
118
  "string"
@@ -112,7 +112,7 @@
112
112
  "type": "object"
113
113
  },
114
114
  "ScalarDefaultValue": {
115
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
115
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
116
116
  "type": [
117
117
  "null",
118
118
  "string"
@@ -404,7 +404,7 @@
404
404
  "type": "object"
405
405
  },
406
406
  "ScalarDefaultValue": {
407
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
407
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
408
408
  "type": [
409
409
  "null",
410
410
  "string"
@@ -180,7 +180,7 @@
180
180
  "type": "object"
181
181
  },
182
182
  "ScalarDefaultValue": {
183
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
183
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
184
184
  "type": [
185
185
  "null",
186
186
  "string"
@@ -137,10 +137,12 @@
137
137
  ]
138
138
  },
139
139
  "currencyType": {
140
- "default": 1,
141
- "description": "货币类型 ID;后端发布要求为非空正整数,即使 currencyStatus 为 false 也不得传 null。无需指定时省略并使用默认值 1。",
140
+ "description": "货币类型 ID。",
142
141
  "minimum": 1,
143
- "type": "integer"
142
+ "type": [
143
+ "integer",
144
+ "null"
145
+ ]
144
146
  },
145
147
  "decimalDigit": {
146
148
  "description": "小数位数。",
@@ -201,7 +203,7 @@
201
203
  "type": "object"
202
204
  },
203
205
  "ScalarDefaultValue": {
204
- "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。需要配置结构化默认值时,通过 AppForm Schema 查询能力传入当前 schema_version,并将字段的 questionType 作为 field_type 获取该字段类型的详细 Schema,再按其中的字段专用 config 定义填写。",
206
+ "description": "后端 queDefaultValue 支持的普通字段默认值;非空值必须是字符串,显式 null 表示清除默认值。数字和布尔值应先转换为字符串,对象和数组必须使用字段专用默认值配置。defaultType 为 3 时,通过 AppForm Schema 查询能力传入当前 schema_version 并设置 schema_kind=formula 获取公式语法、字段引用、运算符和全部函数说明;CLI 使用 builder app-form schema --schema-kind formula。需要配置结构化默认值时,传入当前 schema_version,并将字段的 questionType 作为 field_type 获取字段详细 Schema。",
205
207
  "type": [
206
208
  "null",
207
209
  "string"