@qingflow-tech/qingflow-cli 1.1.51 → 1.1.54

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 (106) hide show
  1. package/README.md +4 -4
  2. package/npm/lib/runtime.mjs +29 -13
  3. package/package.json +1 -1
  4. package/pyproject.toml +8 -2
  5. package/skills/qingflow-cli/SKILL.md +93 -322
  6. package/skills/qingflow-cli/manifest.yaml +1 -1
  7. package/skills/qingflow-cli/reference/00-INDEX.md +4 -4
  8. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  9. package/skills/qingflow-cli/reference/QINGFLOW_CLI_BUILDER_VIEWS_WORKFLOW.md +3 -9
  10. package/skills/qingflow-cli/reference/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +3 -137
  11. package/skills/qingflow-cli/reference/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  12. package/skills/qingflow-cli/reference/QINGFLOW_CLI_FIELD_DATA_TYPES.md +14 -14
  13. package/skills/qingflow-cli/reference/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  14. package/skills/qingflow-cli/reference/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  15. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_CREATE_WORKFLOW.md +7 -12
  16. package/skills/qingflow-cli/reference/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  17. package/skills/qingflow-cli/reference/builder/20-build-complete-system.md +3 -2
  18. package/skills/qingflow-cli/reference/builder/30-schema-fields.md +9 -5
  19. package/skills/qingflow-cli/reference/builder/50-views.md +192 -191
  20. package/skills/qingflow-cli/reference/builder/80-buttons-associated-resources.md +2 -0
  21. package/skills/qingflow-cli/reference/builder/README.md +1 -1
  22. package/skills/qingflow-cli/reference/builder/reference/app-delivery-sop.md +6 -6
  23. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ADMIN_CHEATSHEET.md +1 -1
  24. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_DATA_RETRIEVAL_WORKFLOW.md +76 -95
  25. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_EXPLORATION_REPORT.md +6 -4
  26. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_FIELD_DATA_TYPES.md +13 -13
  27. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_MEMBER_CHEATSHEET.md +20 -19
  28. package/skills/qingflow-cli/reference/core/QINGFLOW_CLI_ONE_SHOT_CHEATSHEET.md +18 -14
  29. package/skills/qingflow-cli/reference/data/README.md +32 -0
  30. package/skills/qingflow-cli/reference/examples/README.md +1 -1
  31. package/skills/qingflow-cli/reference/examples/views/views_apply_v1alpha1.example.json +14 -0
  32. package/skills/qingflow-cli/reference/examples/views/views_upsert_table_minimal.example.json +30 -36
  33. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_SPECIALIZED_WORKFLOW.md +50 -0
  34. package/skills/qingflow-cli/reference/record/QINGFLOW_CLI_RECORD_UPDATE_WORKFLOW.md +2 -1
  35. package/skills/qingflow-cli/reference/record/analysis/README.md +21 -113
  36. package/skills/qingflow-cli/reference/record/analysis/analysis-gotchas.md +10 -10
  37. package/skills/qingflow-cli/reference/record/analysis/analysis-patterns.md +9 -9
  38. package/skills/qingflow-cli/reference/record/analysis/business-context.md +1 -1
  39. package/skills/qingflow-cli/reference/record/analysis/confidence-reporting.md +10 -10
  40. package/skills/qingflow-cli/reference/record/analysis/data-access-playbook.md +52 -80
  41. package/skills/qingflow-cli/reference/record/analysis/pandas-recipes.md +2 -2
  42. package/skills/qingflow-common/SKILL.md +7 -4
  43. package/src/qingflow_mcp/app_form_compat.py +3 -0
  44. package/src/qingflow_mcp/app_form_validator.py +69 -17
  45. package/src/qingflow_mcp/builder_facade/app_form.py +497 -100
  46. package/src/qingflow_mcp/builder_facade/models.py +227 -17
  47. package/src/qingflow_mcp/builder_facade/schemas/app_form/manifest.json +1 -0
  48. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/complete.json +64 -30
  49. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/address.json +4 -8
  50. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/amount.json +4 -8
  51. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/attachment.json +6 -10
  52. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/code_block.json +6 -10
  53. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/data_relation.json +11 -18
  54. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/date.json +4 -8
  55. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/datetime.json +4 -8
  56. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/department.json +6 -10
  57. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/email.json +4 -8
  58. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/link.json +4 -8
  59. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/long_text.json +4 -8
  60. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/member.json +6 -10
  61. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/multi_select.json +4 -8
  62. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/number.json +4 -8
  63. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/phone.json +4 -8
  64. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/q_linker.json +6 -10
  65. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/relation.json +46 -15
  66. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/rich_text.json +146 -0
  67. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_choice.json +4 -8
  68. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/single_select.json +4 -8
  69. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/subtable.json +61 -27
  70. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/fields/text.json +4 -8
  71. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/formula.json +43 -0
  72. package/src/qingflow_mcp/builder_facade/schemas/app_form/v1alpha1/summary.json +7 -12
  73. package/src/qingflow_mcp/builder_facade/schemas/views/manifest.json +42 -0
  74. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/complete.json +1245 -0
  75. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/button-config.json +53 -0
  76. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/data-permission.json +60 -0
  77. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/display.json +73 -0
  78. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/operation-permission.json +48 -0
  79. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/details/query-condition.json +47 -0
  80. package/src/qingflow_mcp/builder_facade/schemas/views/v1alpha1/summary.json +319 -0
  81. package/src/qingflow_mcp/builder_facade/service.py +1709 -518
  82. package/src/qingflow_mcp/builder_facade/views_extract.py +595 -0
  83. package/src/qingflow_mcp/builder_facade/views_schema.py +400 -0
  84. package/src/qingflow_mcp/cli/commands/__init__.py +2 -1
  85. package/src/qingflow_mcp/cli/commands/builder.py +76 -55
  86. package/src/qingflow_mcp/cli/commands/data.py +134 -0
  87. package/src/qingflow_mcp/cli/commands/record.py +1 -1
  88. package/src/qingflow_mcp/cli/context.py +3 -0
  89. package/src/qingflow_mcp/cli/formatters.py +97 -1
  90. package/src/qingflow_mcp/public_surface.py +8 -0
  91. package/src/qingflow_mcp/response_trim.py +11 -2
  92. package/src/qingflow_mcp/server.py +2 -0
  93. package/src/qingflow_mcp/server_app_builder.py +34 -10
  94. package/src/qingflow_mcp/server_app_user.py +4 -0
  95. package/src/qingflow_mcp/solution/compiler/view_compiler.py +2 -1
  96. package/src/qingflow_mcp/solution/executor.py +96 -8
  97. package/src/qingflow_mcp/solution/spec_models.py +1 -0
  98. package/src/qingflow_mcp/tools/ai_builder_tools.py +218 -398
  99. package/src/qingflow_mcp/tools/csv_export.py +168 -0
  100. package/src/qingflow_mcp/tools/data_tools.py +284 -0
  101. package/src/qingflow_mcp/tools/record_tools.py +11 -46
  102. package/src/qingflow_mcp/tools/solution_tools.py +2 -0
  103. package/src/qingflow_mcp/tools/view_tools.py +0 -6
  104. package/src/qingflow_mcp/tools/view_writer.py +44 -0
  105. package/skills/qingflow-cli/reference/examples/views/views_batch_full.example.json +0 -159
  106. package/skills/qingflow-cli/reference/views_upsert_table_minimal.example.json +0 -42
@@ -0,0 +1,400 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from copy import deepcopy
6
+ from dataclasses import dataclass
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from jsonschema import Draft202012Validator, FormatChecker
11
+ from referencing import Registry, Resource
12
+
13
+ from .app_form_schema import compact_schema_errors, schema_error_message
14
+
15
+ JSONObject = dict[str, Any]
16
+ VIEWS_SCHEMA_ROOT = Path(__file__).with_name("schemas") / "views"
17
+ VIEWS_SUPPORTED_SCHEMA_KINDS = (
18
+ "summary",
19
+ "complete",
20
+ "display",
21
+ "data-permission",
22
+ "operation-permission",
23
+ "query-condition",
24
+ "button-config",
25
+ )
26
+
27
+
28
+ class ViewsContractError(ValueError):
29
+ """Raised when a views declaration violates the versioned schema contract."""
30
+
31
+ def __init__(self, error_code: str, message: str, *, path: str | None = None, details: JSONObject | None = None) -> None:
32
+ super().__init__(message)
33
+ self.error_code = error_code
34
+ self.message = message
35
+ self.path = path
36
+ self.details = details or {}
37
+
38
+
39
+ def compact_none_values(value: Any) -> Any:
40
+ """Recursively drop None values so pydantic dumps match the JSON Schema contract."""
41
+ if isinstance(value, dict):
42
+ return {key: compact_none_values(item) for key, item in value.items() if item is not None}
43
+ if isinstance(value, list):
44
+ return [compact_none_values(item) for item in value]
45
+ return value
46
+
47
+
48
+ @dataclass(frozen=True, slots=True)
49
+ class ViewsSchemaContext:
50
+ """Resolved schema context for one views schema version."""
51
+
52
+ schema_version: str
53
+ api_version: str
54
+ schemas: dict[str, JSONObject]
55
+ schema_files: dict[str, str]
56
+ ref_registry: Registry
57
+ supported_schema_kinds: tuple[str, ...]
58
+ contract_source: str
59
+ contract_version: str
60
+
61
+ @property
62
+ def summary_schema(self) -> JSONObject:
63
+ return self.schemas["summary"]
64
+
65
+ @property
66
+ def complete_schema(self) -> JSONObject:
67
+ return self.schemas["complete"]
68
+
69
+ def schema_for_kind(self, schema_kind: str) -> JSONObject:
70
+ requested = str(schema_kind or "").strip()
71
+ schema = self.schemas.get(requested)
72
+ if schema is not None:
73
+ return schema
74
+ raise ViewsContractError(
75
+ "UNSUPPORTED_SCHEMA_KIND",
76
+ f"unsupported views schema kind: {requested}",
77
+ details={"supportedKinds": list(self.supported_schema_kinds)},
78
+ )
79
+
80
+ def _validate(self, payload: JSONObject, *, schema_kind: str) -> None:
81
+ schema = self.schema_for_kind(schema_kind)
82
+ validator = Draft202012Validator(schema, format_checker=FormatChecker(), registry=self.ref_registry)
83
+ errors = sorted(validator.iter_errors(payload), key=lambda item: (list(item.absolute_path), item.message))
84
+ if not errors:
85
+ return
86
+ compact_errors = compact_schema_errors(errors, schema)
87
+ error = compact_errors[0] if compact_errors else errors[0]
88
+ pointer = "".join(f"/{str(part).replace('~', '~0').replace('/', '~1')}" for part in error.absolute_path) or "/"
89
+ raise ViewsContractError(
90
+ "INVALID_SPEC",
91
+ schema_error_message(error),
92
+ path=pointer,
93
+ details={
94
+ "validator": error.validator,
95
+ "schemaPath": "/".join(str(part) for part in error.absolute_schema_path),
96
+ "schemaKind": schema_kind,
97
+ },
98
+ )
99
+
100
+ def validate_upsert(self, payload: JSONObject, *, schema_kind: str = "complete") -> None:
101
+ """Validate a single (alias-normalized) view declaration."""
102
+ self._validate(payload, schema_kind=schema_kind)
103
+
104
+ def validate_declaration(self, payload: JSONObject, *, schema_kind: str = "complete") -> None:
105
+ """Validate one view declaration used by ``app_views_apply.upsert_view``."""
106
+ if not isinstance(payload, dict):
107
+ raise ViewsContractError("INVALID_SPEC", "views declaration must be a JSON object", path="/")
108
+ batch_keys = ("views", "upsert_views", "upsertViews", "apps")
109
+ if any(key in payload for key in batch_keys):
110
+ raise ViewsContractError(
111
+ "VIEW_BATCH_CREATE_UNSUPPORTED",
112
+ "views schema validates one view declaration; batch envelopes are unsupported",
113
+ path="/",
114
+ details={"unsupportedKeys": [key for key in batch_keys if key in payload], "schemaKind": schema_kind},
115
+ )
116
+ self._validate(payload, schema_kind=schema_kind)
117
+
118
+ class ViewsSchemaRegistry:
119
+ """Registry for versioned views declaration schemas (mirrors AppFormSchemaRegistry)."""
120
+
121
+ def __init__(self, root: Path = VIEWS_SCHEMA_ROOT, *, enabled_versions: tuple[str, ...] | None = None) -> None:
122
+ self.root = root
123
+ self.manifest = self._read_json(root / "manifest.json")
124
+ versions = self.manifest.get("versions")
125
+ if not isinstance(versions, dict) or not versions:
126
+ raise RuntimeError("views schema manifest must define versions")
127
+ env_versions = self._configured_versions()
128
+ defaults = tuple(
129
+ str(version)
130
+ for version, metadata in versions.items()
131
+ if isinstance(metadata, dict) and bool(metadata.get("enabledByDefault"))
132
+ )
133
+ configured = tuple(enabled_versions) if enabled_versions is not None else (env_versions or defaults)
134
+ self.stored_versions = tuple(str(version) for version in versions)
135
+ self.latest_version = str(self.manifest.get("latestVersion") or "")
136
+ self._stored_api_to_version: dict[str, str] = {}
137
+ for version, metadata in versions.items():
138
+ if not isinstance(metadata, dict):
139
+ raise RuntimeError(f"views schema metadata must be an object: {version}")
140
+ api_version = str(metadata.get("apiVersion") or "")
141
+ if not api_version:
142
+ raise RuntimeError(f"views schema version has no apiVersion: {version}")
143
+ if api_version in self._stored_api_to_version:
144
+ raise RuntimeError(f"duplicate views apiVersion: {api_version}")
145
+ self._stored_api_to_version[api_version] = str(version)
146
+ self.enabled_versions = configured
147
+ if self.latest_version not in versions:
148
+ raise RuntimeError("views latestVersion is not stored")
149
+ if self.latest_version not in self.enabled_versions:
150
+ raise RuntimeError("views latestVersion must remain enabled")
151
+ self._contexts: dict[str, ViewsSchemaContext] = {}
152
+ self._api_to_version: dict[str, str] = {}
153
+ for version in self.enabled_versions:
154
+ metadata = versions.get(version)
155
+ if not isinstance(metadata, dict):
156
+ raise RuntimeError(f"enabled views schema version is not stored: {version}")
157
+ api_version = str(metadata.get("apiVersion") or "")
158
+ schema_files = metadata.get("schemaFiles")
159
+ if not isinstance(schema_files, dict):
160
+ schema_files = {kind: f"{version}/{kind}.json" for kind in ("summary", "complete")}
161
+ supported_schema_kinds = tuple(str(item) for item in metadata.get("schemaKinds") or schema_files)
162
+ if not {"summary", "complete"}.issubset(supported_schema_kinds):
163
+ raise RuntimeError(f"views schema version must support summary and complete kinds: {version}")
164
+ schemas: dict[str, JSONObject] = {}
165
+ paths: dict[str, Path] = {}
166
+ for name in supported_schema_kinds:
167
+ relative_path = schema_files.get(name)
168
+ if not isinstance(relative_path, str) or not relative_path:
169
+ raise RuntimeError(f"views schema kind has no schema file: {version}/{name}")
170
+ path = root / relative_path
171
+ schema = self._read_json(path)
172
+ schemas[name] = schema
173
+ paths[name] = path
174
+ ref_registry = Registry()
175
+ for name, schema in schemas.items():
176
+ schema_id = str(schema.get("$id") or "")
177
+ if version not in schema_id:
178
+ raise RuntimeError(f"views {name} schema $id mismatch for {version}: {schema_id}")
179
+ Draft202012Validator.check_schema(schema)
180
+ resource = Resource.from_contents(schema)
181
+ if schema_id:
182
+ ref_registry = ref_registry.with_resource(schema_id, resource)
183
+ ref_registry = ref_registry.with_resource(paths[name].resolve().as_uri(), resource)
184
+ for name, schema in schemas.items():
185
+ self._validate_local_refs(schema, paths[name], schemas, paths)
186
+ context = ViewsSchemaContext(
187
+ version,
188
+ api_version,
189
+ schemas,
190
+ {name: str(schema_files[name]) for name in supported_schema_kinds},
191
+ ref_registry,
192
+ supported_schema_kinds,
193
+ str(metadata.get("contractSource") or "app_views_apply"),
194
+ str(metadata.get("contractVersion") or version),
195
+ )
196
+ self._contexts[version] = context
197
+ self._api_to_version[api_version] = version
198
+
199
+ def schema(
200
+ self,
201
+ *,
202
+ schema_version: str | None = None,
203
+ schema_kind: str | None = None,
204
+ ) -> JSONObject:
205
+ context = self.resolve_schema_version(schema_version)
206
+ requested_kind = str(schema_kind or "summary").strip()
207
+ if requested_kind not in context.supported_schema_kinds:
208
+ raise ViewsContractError(
209
+ "UNSUPPORTED_SCHEMA_KIND",
210
+ f"unsupported views schema kind: {requested_kind}",
211
+ details={"supportedKinds": list(context.supported_schema_kinds)},
212
+ )
213
+ schema, dependencies = self._bundle_schema(
214
+ context.schema_for_kind(requested_kind),
215
+ schema_path=(self.root / context.schema_files[requested_kind]),
216
+ schemas=context.schemas,
217
+ schema_files=context.schema_files,
218
+ )
219
+ return {
220
+ "schemaVersion": context.schema_version,
221
+ "apiVersion": context.api_version,
222
+ "latestVersion": self.latest_version,
223
+ "storedVersions": list(self.stored_versions),
224
+ "enabledVersions": list(self.enabled_versions),
225
+ "supportedSchemaKinds": list(context.supported_schema_kinds),
226
+ "schemaKind": requested_kind,
227
+ "schemaFile": context.schema_files[requested_kind],
228
+ "schemaBundled": bool(dependencies),
229
+ "schemaDependencies": dependencies,
230
+ "schema": schema,
231
+ }
232
+
233
+ @classmethod
234
+ def _bundle_schema(
235
+ cls,
236
+ schema: JSONObject,
237
+ *,
238
+ schema_path: Path,
239
+ schemas: dict[str, JSONObject],
240
+ schema_files: dict[str, str],
241
+ ) -> tuple[JSONObject, list[str]]:
242
+ """Inline reachable cross-file definitions for standalone consumers."""
243
+ bundled = deepcopy(schema)
244
+ definitions = bundled.get("$defs")
245
+ if not isinstance(definitions, dict):
246
+ definitions = {}
247
+ dependencies: set[str] = set()
248
+ # schema_path is <root>/<version>/details/<file> or <root>/<version>/<file>.
249
+ # Find the manifest root from the configured path instead of relying on
250
+ # a fixed details depth.
251
+ cls_root = schema_path.parent
252
+ for candidate in schema_path.parents:
253
+ if (candidate / "manifest.json").is_file():
254
+ cls_root = candidate
255
+ break
256
+ paths = {name: cls_root / relative for name, relative in schema_files.items()}
257
+
258
+ def resolve_pointer(document: JSONObject, fragment: str, *, ref: str) -> Any:
259
+ target: Any = document
260
+ for raw_part in fragment[1:].split("/") if fragment.startswith("/") else ():
261
+ part = raw_part.replace("~1", "/").replace("~0", "~")
262
+ if not isinstance(target, dict) or part not in target:
263
+ raise RuntimeError(f"views schema contains a dangling $ref in {schema_path}: {ref}")
264
+ target = target[part]
265
+ return target
266
+
267
+ pending: list[tuple[str, JSONObject, Path]] = []
268
+
269
+ def add_definition(name: str, source: JSONObject, source_path: Path) -> None:
270
+ if name in definitions:
271
+ return
272
+ source_definitions = source.get("$defs")
273
+ if not isinstance(source_definitions, dict) or name not in source_definitions:
274
+ raise RuntimeError(f"views schema references an unknown definition: {name}")
275
+ definitions[name] = deepcopy(source_definitions[name])
276
+ pending.append((name, source, source_path))
277
+
278
+ def rewrite(value: Any, source: JSONObject, source_path: Path) -> None:
279
+ if isinstance(value, dict):
280
+ ref = value.get("$ref")
281
+ if isinstance(ref, str):
282
+ ref_file, _, fragment = ref.partition("#")
283
+ target_source = source
284
+ target_path = source_path
285
+ if ref_file:
286
+ target_path = (source_path.parent / ref_file).resolve()
287
+ target_name = next(
288
+ (name for name, path in paths.items() if path.resolve() == target_path),
289
+ None,
290
+ )
291
+ if target_name is None:
292
+ raise RuntimeError(f"views schema contains an external $ref in {source_path}: {ref}")
293
+ target_source = schemas[target_name]
294
+ dependencies.add(schema_files[target_name])
295
+ value["$ref"] = f"#{fragment}"
296
+ local_ref = str(value["$ref"])
297
+ if local_ref.startswith("#/$defs/"):
298
+ name = local_ref.removeprefix("#/$defs/").split("/", 1)[0]
299
+ add_definition(name, target_source, target_path)
300
+ else:
301
+ resolve_pointer(target_source, fragment, ref=ref)
302
+ for child in value.values():
303
+ rewrite(child, source, source_path)
304
+ elif isinstance(value, list):
305
+ for child in value:
306
+ rewrite(child, source, source_path)
307
+
308
+ for key, value in bundled.items():
309
+ if key != "$defs":
310
+ rewrite(value, schema, schema_path)
311
+ while pending:
312
+ name, source, source_path = pending.pop()
313
+ rewrite(definitions[name], source, source_path)
314
+ if definitions:
315
+ bundled["$defs"] = definitions
316
+ return bundled, sorted(dependencies)
317
+
318
+ def resolve_schema_version(self, schema_version: str | None) -> ViewsSchemaContext:
319
+ version = str(schema_version or self.latest_version).strip()
320
+ if version not in self.stored_versions:
321
+ raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", version)
322
+ if version not in self.enabled_versions:
323
+ raise self._version_error("SCHEMA_VERSION_DISABLED", version)
324
+ return self._contexts[version]
325
+
326
+ def resolve_api_version(self, api_version: str) -> ViewsSchemaContext:
327
+ version = self._api_to_version.get(api_version)
328
+ if version is not None:
329
+ return self._contexts[version]
330
+ stored = self._stored_api_to_version.get(api_version)
331
+ if stored is not None:
332
+ raise self._version_error("SCHEMA_VERSION_DISABLED", stored)
333
+ raise self._version_error("UNSUPPORTED_SCHEMA_VERSION", api_version)
334
+
335
+ def _version_error(self, error_code: str, requested: str) -> ViewsContractError:
336
+ return ViewsContractError(
337
+ error_code,
338
+ f"unsupported views schema selection: {requested}",
339
+ details={
340
+ "requestedVersion": requested,
341
+ "latestVersion": self.latest_version,
342
+ "storedVersions": list(self.stored_versions),
343
+ "enabledVersions": list(self.enabled_versions),
344
+ },
345
+ )
346
+
347
+ @staticmethod
348
+ def _read_json(path: Path) -> JSONObject:
349
+ value = json.loads(path.read_text(encoding="utf-8"))
350
+ if not isinstance(value, dict):
351
+ raise RuntimeError(f"views schema artifact must be an object: {path}")
352
+ return value
353
+
354
+ @staticmethod
355
+ def _validate_local_refs(
356
+ document: JSONObject,
357
+ path: Path,
358
+ documents: dict[str, JSONObject],
359
+ paths: dict[str, Path],
360
+ ) -> None:
361
+ stack: list[Any] = [document]
362
+ while stack:
363
+ value = stack.pop()
364
+ if isinstance(value, dict):
365
+ ref = value.get("$ref")
366
+ if ref is not None:
367
+ if not isinstance(ref, str):
368
+ raise RuntimeError(f"views schema contains an invalid $ref in {path}: {ref}")
369
+ ref_file, _, fragment = ref.partition("#")
370
+ target_document = document
371
+ if ref_file:
372
+ target_path = (path.parent / ref_file).resolve()
373
+ if target_path not in {item.resolve() for item in paths.values()}:
374
+ raise RuntimeError(f"views schema contains an external $ref in {path}: {ref}")
375
+ target_name = next(name for name, item in paths.items() if item.resolve() == target_path)
376
+ target_document = documents[target_name]
377
+ target: Any = target_document
378
+ for raw_part in fragment[1:].split("/") if fragment.startswith("/") else ():
379
+ part = raw_part.replace("~1", "/").replace("~0", "~")
380
+ if not isinstance(target, dict) or part not in target:
381
+ raise RuntimeError(f"views schema contains a dangling $ref in {path}: {ref}")
382
+ target = target[part]
383
+ stack.extend(value.values())
384
+ elif isinstance(value, list):
385
+ stack.extend(value)
386
+
387
+ @staticmethod
388
+ def _configured_versions() -> tuple[str, ...]:
389
+ raw = os.environ.get("QINGFLOW_VIEWS_SCHEMA_VERSIONS", "")
390
+ return tuple(part.strip() for part in raw.split(",") if part.strip())
391
+
392
+
393
+ _REGISTRY: ViewsSchemaRegistry | None = None
394
+
395
+
396
+ def views_schema_registry() -> ViewsSchemaRegistry:
397
+ global _REGISTRY
398
+ if _REGISTRY is None:
399
+ _REGISTRY = ViewsSchemaRegistry()
400
+ return _REGISTRY
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
 
5
- from . import app, auth, builder, chart, exports, imports, portal, record, task, view, workspace
5
+ from . import app, auth, builder, chart, data, exports, imports, portal, record, task, view, workspace
6
6
 
7
7
 
8
8
  def register_all_commands(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
@@ -13,6 +13,7 @@ def register_all_commands(subparsers: argparse._SubParsersAction[argparse.Argume
13
13
  view.register(subparsers)
14
14
  chart.register(subparsers)
15
15
  record.register(subparsers)
16
+ data.register(subparsers)
16
17
  imports.register(subparsers)
17
18
  exports.register(subparsers)
18
19
  task.register(subparsers)
@@ -4,24 +4,30 @@ import argparse
4
4
  from copy import deepcopy
5
5
  from pathlib import Path
6
6
 
7
- from ..context import CliContext
8
7
  from ...app_form_validator import (
9
8
  AppFormValidationError,
10
9
  build_encoded_id_map,
11
- referenced_formula_field_ids,
12
- translate_formula_test_case_values,
13
10
  encode_code_block_field_ids,
14
11
  referenced_code_block_field_ids,
12
+ referenced_formula_field_ids,
15
13
  restore_decimal_ids_in_text,
16
14
  strip_placeholder_quotes,
15
+ translate_formula_test_case_values,
17
16
  translate_select_test_case_values,
18
17
  validate_code_block_static,
19
18
  validate_declaration,
20
19
  validate_formula_expression,
21
20
  )
21
+ from ...builder_facade.views_schema import VIEWS_SUPPORTED_SCHEMA_KINDS
22
+ from ..context import CliContext
22
23
  from ..json_io import load_json_value
23
- from .common import load_list_arg, load_object_arg, parse_bool_text, raise_config_error, require_list_arg
24
-
24
+ from .common import (
25
+ load_list_arg,
26
+ load_object_arg,
27
+ parse_bool_text,
28
+ raise_config_error,
29
+ require_list_arg,
30
+ )
25
31
 
26
32
  def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
27
33
  parser = subparsers.add_parser("builder", aliases=["build"], help="稳定 builder 命令")
@@ -219,6 +225,7 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
219
225
  button_get.add_argument("--app-key", default="")
220
226
  button_get.add_argument("--app-keys", default="", help="逗号分隔的多应用 app_key;用于批量读取按钮配置")
221
227
  button_get.add_argument("--app-keys-file", help="JSON 字符串数组;用于批量读取按钮配置")
228
+ button_get.add_argument("--button-text", default="", help="按按钮文案精确查重;命中复用其 button_id,未命中再创建")
222
229
  button_get.set_defaults(handler=_handle_button_get, format_hint="builder_summary")
223
230
 
224
231
  button_apply = button_subparsers.add_parser("apply", help="声明式创建、更新或删除自定义按钮")
@@ -282,14 +289,32 @@ def register(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) ->
282
289
 
283
290
  views_apply = builder_subparsers.add_parser("views", help="视图")
284
291
  views_apply_subparsers = views_apply.add_subparsers(dest="builder_views_command", required=True)
292
+ views_schema = views_apply_subparsers.add_parser("schema", help="读取视图声明 Schema")
293
+ views_schema.add_argument("--schema-version")
294
+ views_schema.add_argument(
295
+ "--schema-kind",
296
+ choices=VIEWS_SUPPORTED_SCHEMA_KINDS,
297
+ )
298
+ views_schema.set_defaults(handler=_handle_views_schema, format_hint="builder_summary", force_json_output=True)
299
+
300
+ views_get = views_apply_subparsers.add_parser("get", help="读取 canonical 视图声明")
301
+ views_get.add_argument("--app-key", required=True)
302
+ views_get.add_argument("--schema-version")
303
+ views_get.add_argument("--schema-kind", choices=("summary", "complete"), help="summary=投影字段;complete=逐视图读 viewConfig 反向编译(含覆盖度 gaps)")
304
+ views_get.set_defaults(handler=_handle_views_get, format_hint="builder_summary", force_json_output=True)
305
+
306
+ views_validate = views_apply_subparsers.add_parser("validate", help="只校验视图声明,不访问后端")
307
+ views_validate.add_argument("--file", required=True, help="视图声明 JSON 文件,- 表示 stdin")
308
+ views_validate.add_argument("--schema-version", help="固定使用的视图 Schema 版本;缺省读文件内 schema_version,再缺省 latest")
309
+ views_validate.set_defaults(handler=_handle_views_validate, format_hint="builder_summary", force_json_output=True)
310
+
285
311
  views_apply_apply = views_apply_subparsers.add_parser("apply", help="执行视图变更")
286
- views_apply_apply.add_argument("--app-key", default="")
287
- views_apply_apply.add_argument("--views-file", help='推荐批量视图 JSON 对象:{"views":[...]};每项包含 app_key、operation、视图配置')
288
- views_apply_apply.add_argument("--apps-file", help="多应用视图 JSON 数组;每项包含 app_key 和 upsert/patch/remove payload")
312
+ views_apply_apply.add_argument("--app-key", required=True)
289
313
  views_apply_apply.add_argument("--publish", action=argparse.BooleanOptionalAction, default=True)
290
- views_apply_apply.add_argument("--upsert-views-file")
314
+ views_apply_apply.add_argument("--view-file", help="单条新建或完整更新视图的 JSON 对象")
291
315
  views_apply_apply.add_argument("--patch-views-file")
292
316
  views_apply_apply.add_argument("--remove-views-file")
317
+ views_apply_apply.add_argument("--schema-version", help="声明使用的 Schema 版本;缺省 latest,信封回显 schemaVersion")
293
318
  views_apply_apply.set_defaults(handler=_handle_views_apply, format_hint="builder_summary", force_json_output=True)
294
319
 
295
320
  flow_apply = builder_subparsers.add_parser("flow", help="流程")
@@ -874,12 +899,13 @@ def _handle_associated_resource_apply(args: argparse.Namespace, context: CliCont
874
899
 
875
900
 
876
901
  def _handle_button_get(args: argparse.Namespace, context: CliContext) -> dict:
902
+ button_text = str(getattr(args, "button_text", "") or "").strip() or None
877
903
  app_keys = _app_keys_from_args(args)
878
904
  if app_keys is not None:
879
- return context.builder.app_get_buttons(profile=args.profile, app_key="", app_keys=app_keys)
905
+ return context.builder.app_get_buttons(profile=args.profile, app_key="", app_keys=app_keys, button_text=button_text)
880
906
  if not (args.app_key or "").strip():
881
907
  raise_config_error("builder button get requires --app-key unless --app-keys or --app-keys-file is provided")
882
- return context.builder.app_get_buttons(profile=args.profile, app_key=args.app_key)
908
+ return context.builder.app_get_buttons(profile=args.profile, app_key=args.app_key, button_text=button_text)
883
909
 
884
910
 
885
911
  def _handle_associated_resource_get(args: argparse.Namespace, context: CliContext) -> dict:
@@ -1187,30 +1213,6 @@ def _coerce_apps_file_bool(value: object, *, field_name: str, default: bool) ->
1187
1213
  )
1188
1214
 
1189
1215
 
1190
- def _load_views_file_arg(path: str | None) -> list[object] | None:
1191
- if not path:
1192
- return None
1193
- payload = load_json_value(path, option_name="--views-file")
1194
- if isinstance(payload, dict):
1195
- views = payload.get("views")
1196
- if not isinstance(views, list):
1197
- raise_config_error(
1198
- "--views-file JSON object must contain a views array.",
1199
- fix_hint='Use {"views": [{"operation": "upsert", "app_key": "...", "name": "...", "type": "table", "columns": [...]}]}.',
1200
- error_code="VIEWS_FILE_SHAPE_INVALID",
1201
- details={"expected": '{"views": [...]}', "actual_type": type(views).__name__},
1202
- )
1203
- return views
1204
- if isinstance(payload, list):
1205
- return payload
1206
- raise_config_error(
1207
- "--views-file must be a JSON object with views[] or a JSON array.",
1208
- fix_hint='Prefer {"views": [...]} for new automation. A bare array is accepted only for compatibility.',
1209
- error_code="VIEWS_FILE_SHAPE_INVALID",
1210
- details={"actual_type": type(payload).__name__},
1211
- )
1212
-
1213
-
1214
1216
  def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
1215
1217
  apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
1216
1218
  if apps is None and not (args.app_key or "").strip():
@@ -1235,34 +1237,53 @@ def _handle_layout_apply(args: argparse.Namespace, context: CliContext) -> dict:
1235
1237
  )
1236
1238
 
1237
1239
 
1240
+ def _handle_views_schema(args: argparse.Namespace, context: CliContext) -> dict:
1241
+ return context.builder.app_views_schema(
1242
+ schema_version=args.schema_version,
1243
+ schema_kind=args.schema_kind,
1244
+ )
1245
+
1246
+
1247
+ def _handle_views_get(args: argparse.Namespace, context: CliContext) -> dict:
1248
+ return context.builder.app_views_get(
1249
+ profile=args.profile,
1250
+ app_key=args.app_key,
1251
+ schema_version=args.schema_version,
1252
+ schema_kind=getattr(args, "schema_kind", None) or "summary",
1253
+ )
1254
+
1255
+
1256
+ def _handle_views_validate(args: argparse.Namespace, context: CliContext) -> dict:
1257
+ payload = load_json_value(
1258
+ args.file,
1259
+ option_name="--file",
1260
+ max_bytes=2 * 1024 * 1024,
1261
+ max_depth=64,
1262
+ )
1263
+ if not isinstance(payload, dict):
1264
+ raise_config_error("builder views validate --file must contain a JSON object")
1265
+ return context.builder.app_views_validate(
1266
+ declaration=payload,
1267
+ schema_version=args.schema_version,
1268
+ )
1269
+
1270
+
1238
1271
  def _handle_views_apply(args: argparse.Namespace, context: CliContext) -> dict:
1239
- views = _load_views_file_arg(args.views_file) if getattr(args, "views_file", None) else None
1240
- apps = load_list_arg(args.apps_file, option_name="--apps-file") if getattr(args, "apps_file", None) else None
1241
- if views is not None:
1242
- if apps is not None or (args.app_key or "").strip() or getattr(args, "upsert_views_file", None) or getattr(args, "patch_views_file", None) or getattr(args, "remove_views_file", None):
1243
- raise_config_error(
1244
- "builder views apply --views-file cannot be mixed with --app-key, --apps-file, --upsert-views-file, --patch-views-file, or --remove-views-file.",
1245
- fix_hint="Use one shape: preferred --views-file for flat batch jobs, or legacy --app-key/--apps-file for grouped payloads.",
1246
- )
1247
- return context.builder.app_views_apply(
1248
- profile=args.profile,
1249
- app_key="",
1250
- publish=bool(args.publish),
1251
- upsert_views=[],
1252
- patch_views=[],
1253
- remove_views=[],
1254
- views=views,
1272
+ view = load_object_arg(args.view_file, option_name="--view-file") if getattr(args, "view_file", None) else None
1273
+ if view is None and not (getattr(args, "patch_views_file", None) or getattr(args, "remove_views_file", None)):
1274
+ raise_config_error(
1275
+ "builder views apply requires --view-file, --patch-views-file, or --remove-views-file.",
1276
+ fix_hint="Pass one JSON object through --view-file to create or fully update one view.",
1277
+ error_code="ARGUMENT_ERROR",
1255
1278
  )
1256
- if apps is None and not (args.app_key or "").strip():
1257
- raise_config_error("builder views apply requires --views-file, --apps-file, or --app-key")
1258
1279
  return context.builder.app_views_apply(
1259
1280
  profile=args.profile,
1260
1281
  app_key=args.app_key,
1261
1282
  publish=bool(args.publish),
1262
- upsert_views=load_list_arg(args.upsert_views_file, option_name="--upsert-views-file"),
1283
+ upsert_view=view,
1263
1284
  patch_views=load_list_arg(args.patch_views_file, option_name="--patch-views-file"),
1264
1285
  remove_views=load_list_arg(args.remove_views_file, option_name="--remove-views-file"),
1265
- apps=apps,
1286
+ schema_version=getattr(args, "schema_version", None),
1266
1287
  )
1267
1288
 
1268
1289