@josephyan/qingflow-cli 0.2.0-beta.55
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -0
- package/docs/local-agent-install.md +235 -0
- package/entry_point.py +13 -0
- package/npm/bin/qingflow.mjs +5 -0
- package/npm/lib/runtime.mjs +204 -0
- package/npm/scripts/postinstall.mjs +16 -0
- package/package.json +34 -0
- package/pyproject.toml +67 -0
- package/qingflow +15 -0
- package/src/qingflow_mcp/__init__.py +5 -0
- package/src/qingflow_mcp/__main__.py +5 -0
- package/src/qingflow_mcp/backend_client.py +547 -0
- package/src/qingflow_mcp/builder_facade/__init__.py +3 -0
- package/src/qingflow_mcp/builder_facade/models.py +985 -0
- package/src/qingflow_mcp/builder_facade/service.py +8243 -0
- package/src/qingflow_mcp/cli/__init__.py +1 -0
- package/src/qingflow_mcp/cli/commands/__init__.py +15 -0
- package/src/qingflow_mcp/cli/commands/app.py +40 -0
- package/src/qingflow_mcp/cli/commands/auth.py +78 -0
- package/src/qingflow_mcp/cli/commands/builder.py +184 -0
- package/src/qingflow_mcp/cli/commands/common.py +47 -0
- package/src/qingflow_mcp/cli/commands/imports.py +86 -0
- package/src/qingflow_mcp/cli/commands/record.py +202 -0
- package/src/qingflow_mcp/cli/commands/task.py +87 -0
- package/src/qingflow_mcp/cli/commands/workspace.py +33 -0
- package/src/qingflow_mcp/cli/context.py +48 -0
- package/src/qingflow_mcp/cli/formatters.py +269 -0
- package/src/qingflow_mcp/cli/json_io.py +50 -0
- package/src/qingflow_mcp/cli/main.py +147 -0
- package/src/qingflow_mcp/config.py +221 -0
- package/src/qingflow_mcp/errors.py +66 -0
- package/src/qingflow_mcp/import_store.py +121 -0
- package/src/qingflow_mcp/json_types.py +18 -0
- package/src/qingflow_mcp/list_type_labels.py +76 -0
- package/src/qingflow_mcp/server.py +211 -0
- package/src/qingflow_mcp/server_app_builder.py +387 -0
- package/src/qingflow_mcp/server_app_user.py +317 -0
- package/src/qingflow_mcp/session_store.py +289 -0
- package/src/qingflow_mcp/solution/__init__.py +6 -0
- package/src/qingflow_mcp/solution/build_assembly_store.py +181 -0
- package/src/qingflow_mcp/solution/compiler/__init__.py +282 -0
- package/src/qingflow_mcp/solution/compiler/chart_compiler.py +96 -0
- package/src/qingflow_mcp/solution/compiler/form_compiler.py +466 -0
- package/src/qingflow_mcp/solution/compiler/icon_utils.py +113 -0
- package/src/qingflow_mcp/solution/compiler/navigation_compiler.py +57 -0
- package/src/qingflow_mcp/solution/compiler/package_compiler.py +19 -0
- package/src/qingflow_mcp/solution/compiler/portal_compiler.py +60 -0
- package/src/qingflow_mcp/solution/compiler/view_compiler.py +51 -0
- package/src/qingflow_mcp/solution/compiler/workflow_compiler.py +173 -0
- package/src/qingflow_mcp/solution/design_session.py +222 -0
- package/src/qingflow_mcp/solution/design_store.py +100 -0
- package/src/qingflow_mcp/solution/executor.py +2339 -0
- package/src/qingflow_mcp/solution/normalizer.py +23 -0
- package/src/qingflow_mcp/solution/requirements_builder.py +536 -0
- package/src/qingflow_mcp/solution/run_store.py +244 -0
- package/src/qingflow_mcp/solution/spec_models.py +853 -0
- package/src/qingflow_mcp/tools/__init__.py +1 -0
- package/src/qingflow_mcp/tools/ai_builder_tools.py +2063 -0
- package/src/qingflow_mcp/tools/app_tools.py +850 -0
- package/src/qingflow_mcp/tools/approval_tools.py +833 -0
- package/src/qingflow_mcp/tools/auth_tools.py +697 -0
- package/src/qingflow_mcp/tools/base.py +81 -0
- package/src/qingflow_mcp/tools/code_block_tools.py +679 -0
- package/src/qingflow_mcp/tools/directory_tools.py +648 -0
- package/src/qingflow_mcp/tools/feedback_tools.py +230 -0
- package/src/qingflow_mcp/tools/file_tools.py +385 -0
- package/src/qingflow_mcp/tools/import_tools.py +1971 -0
- package/src/qingflow_mcp/tools/navigation_tools.py +177 -0
- package/src/qingflow_mcp/tools/package_tools.py +240 -0
- package/src/qingflow_mcp/tools/portal_tools.py +131 -0
- package/src/qingflow_mcp/tools/qingbi_report_tools.py +269 -0
- package/src/qingflow_mcp/tools/record_tools.py +12739 -0
- package/src/qingflow_mcp/tools/role_tools.py +94 -0
- package/src/qingflow_mcp/tools/solution_tools.py +3887 -0
- package/src/qingflow_mcp/tools/task_context_tools.py +1423 -0
- package/src/qingflow_mcp/tools/task_tools.py +843 -0
- package/src/qingflow_mcp/tools/view_tools.py +280 -0
- package/src/qingflow_mcp/tools/workflow_tools.py +312 -0
- package/src/qingflow_mcp/tools/workspace_tools.py +219 -0
|
@@ -0,0 +1,3887 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from copy import deepcopy
|
|
4
|
+
from typing import Any
|
|
5
|
+
from uuid import uuid4
|
|
6
|
+
|
|
7
|
+
from mcp.server.fastmcp import FastMCP
|
|
8
|
+
from pydantic import BaseModel, ValidationError
|
|
9
|
+
|
|
10
|
+
from ..config import DEFAULT_PROFILE
|
|
11
|
+
from ..list_type_labels import get_record_list_type_label
|
|
12
|
+
from ..solution.build_assembly_store import BuildAssemblyStore, default_manifest
|
|
13
|
+
from ..solution.compiler import CompiledSolution, ExecutionPlan, ExecutionStep, build_execution_plan, compile_solution
|
|
14
|
+
from ..solution.design_session import DesignStage, evaluate_design_session, finalize_design_session, merge_design_payload
|
|
15
|
+
from ..solution.design_store import DesignSessionStore
|
|
16
|
+
from ..solution.executor import SolutionExecutor
|
|
17
|
+
from ..solution.normalizer import normalize_solution_spec
|
|
18
|
+
from ..solution.requirements_builder import GeneratedAppBuild, RequirementsBuildError, build_app_spec_from_requirements
|
|
19
|
+
from ..solution.run_store import RunArtifactStore, fingerprint_payload
|
|
20
|
+
from ..solution.spec_models import (
|
|
21
|
+
AnalyticsPortalBuildSpec,
|
|
22
|
+
AppBuildSpec,
|
|
23
|
+
AppFlowBuildSpec,
|
|
24
|
+
FlowBuildSpec,
|
|
25
|
+
BuildManifest,
|
|
26
|
+
NavigationBuildSpec,
|
|
27
|
+
SolutionSpec,
|
|
28
|
+
ViewsBuildSpec,
|
|
29
|
+
)
|
|
30
|
+
from .app_tools import AppTools
|
|
31
|
+
from .base import ToolBase
|
|
32
|
+
from .navigation_tools import NavigationTools
|
|
33
|
+
from .package_tools import PackageTools
|
|
34
|
+
from .portal_tools import PortalTools
|
|
35
|
+
from .qingbi_report_tools import QingbiReportTools
|
|
36
|
+
from .record_tools import RecordTools
|
|
37
|
+
from .role_tools import RoleTools
|
|
38
|
+
from .view_tools import ViewTools
|
|
39
|
+
from .workflow_tools import WorkflowTools
|
|
40
|
+
from .workspace_tools import WorkspaceTools
|
|
41
|
+
|
|
42
|
+
STAGED_BUILD_MODES = {"preflight", "plan", "apply", "repair"}
|
|
43
|
+
STAGED_BUILD_MODE_ALIASES = {
|
|
44
|
+
"create": "plan",
|
|
45
|
+
"new": "plan",
|
|
46
|
+
"start": "plan",
|
|
47
|
+
"draft": "plan",
|
|
48
|
+
"update": "plan",
|
|
49
|
+
"modify": "plan",
|
|
50
|
+
"preview": "preflight",
|
|
51
|
+
"prepare": "preflight",
|
|
52
|
+
"run": "apply",
|
|
53
|
+
"execute": "apply",
|
|
54
|
+
"fix": "repair",
|
|
55
|
+
"resume": "repair",
|
|
56
|
+
"retry": "repair",
|
|
57
|
+
}
|
|
58
|
+
STAGE_ORDER = ["app_flow", "views", "analytics_portal", "navigation"]
|
|
59
|
+
STAGE_TOOL_NAMES = {
|
|
60
|
+
"app_flow": "solution_build_app_flow",
|
|
61
|
+
"views": "solution_build_views",
|
|
62
|
+
"analytics_portal": "solution_build_analytics_portal",
|
|
63
|
+
"navigation": "solution_build_navigation",
|
|
64
|
+
}
|
|
65
|
+
SOLUTION_SCHEMA_STAGES = {"app", "app_update", "flow", "views", "analytics_portal", "navigation", "app_flow", "all"}
|
|
66
|
+
SOLUTION_SCHEMA_INTENTS = {"minimal", "full", "demo"}
|
|
67
|
+
SOLUTION_SCHEMA_STAGE_ALIASES = {
|
|
68
|
+
"all": "all",
|
|
69
|
+
"solution": "all",
|
|
70
|
+
"full_solution": "all",
|
|
71
|
+
"app": "app",
|
|
72
|
+
"application": "app",
|
|
73
|
+
"form": "app",
|
|
74
|
+
"schema": "app",
|
|
75
|
+
"app_spec": "app",
|
|
76
|
+
"app_update": "app_update",
|
|
77
|
+
"update_app": "app_update",
|
|
78
|
+
"update": "app_update",
|
|
79
|
+
"更新应用": "app_update",
|
|
80
|
+
"更新表单": "app_update",
|
|
81
|
+
"更新schema": "app_update",
|
|
82
|
+
"应用": "app",
|
|
83
|
+
"表单": "app",
|
|
84
|
+
"flow": "flow",
|
|
85
|
+
"workflow": "flow",
|
|
86
|
+
"流程": "flow",
|
|
87
|
+
"flow_spec": "flow",
|
|
88
|
+
"views": "views",
|
|
89
|
+
"view": "views",
|
|
90
|
+
"视图": "views",
|
|
91
|
+
"analytics_portal": "analytics_portal",
|
|
92
|
+
"analytics": "analytics_portal",
|
|
93
|
+
"portal": "analytics_portal",
|
|
94
|
+
"report": "analytics_portal",
|
|
95
|
+
"dashboard": "analytics_portal",
|
|
96
|
+
"报表": "analytics_portal",
|
|
97
|
+
"门户": "analytics_portal",
|
|
98
|
+
"navigation": "navigation",
|
|
99
|
+
"nav": "navigation",
|
|
100
|
+
"menu": "navigation",
|
|
101
|
+
"导航": "navigation",
|
|
102
|
+
"app_flow": "app_flow",
|
|
103
|
+
"应用流程": "app_flow",
|
|
104
|
+
}
|
|
105
|
+
SOLUTION_SCHEMA_INTENT_ALIASES = {
|
|
106
|
+
"minimal": "minimal",
|
|
107
|
+
"min": "minimal",
|
|
108
|
+
"最小": "minimal",
|
|
109
|
+
"最简": "minimal",
|
|
110
|
+
"骨架": "minimal",
|
|
111
|
+
"基础": "minimal",
|
|
112
|
+
"full": "full",
|
|
113
|
+
"complete": "full",
|
|
114
|
+
"完整": "full",
|
|
115
|
+
"全字段": "full",
|
|
116
|
+
"完整布局": "full",
|
|
117
|
+
"all_fields": "full",
|
|
118
|
+
"demo": "demo",
|
|
119
|
+
"example": "demo",
|
|
120
|
+
"sample": "demo",
|
|
121
|
+
"示例": "demo",
|
|
122
|
+
"演示": "demo",
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
class SolutionTools(ToolBase):
|
|
127
|
+
def register(self, mcp: FastMCP) -> None:
|
|
128
|
+
@mcp.tool()
|
|
129
|
+
def solution_design_session(
|
|
130
|
+
action: str = "get",
|
|
131
|
+
session_id: str = "",
|
|
132
|
+
stage: str | None = None,
|
|
133
|
+
design_patch: dict[str, Any] | None = None,
|
|
134
|
+
metadata: dict[str, Any] | None = None,
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
return self.solution_design_session(
|
|
137
|
+
action=action,
|
|
138
|
+
session_id=session_id,
|
|
139
|
+
stage=stage,
|
|
140
|
+
design_patch=design_patch or {},
|
|
141
|
+
metadata=metadata or {},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
@mcp.tool()
|
|
145
|
+
def solution_build_all(
|
|
146
|
+
profile: str = DEFAULT_PROFILE,
|
|
147
|
+
mode: str = "plan",
|
|
148
|
+
build_id: str = "",
|
|
149
|
+
solution_spec: dict[str, Any] | None = None,
|
|
150
|
+
publish: bool = True,
|
|
151
|
+
run_label: str | None = None,
|
|
152
|
+
target: dict[str, Any] | None = None,
|
|
153
|
+
repair_patch: dict[str, Any] | None = None,
|
|
154
|
+
verify: bool = False,
|
|
155
|
+
) -> dict[str, Any]:
|
|
156
|
+
return self.solution_build_all(
|
|
157
|
+
profile=profile,
|
|
158
|
+
mode=mode,
|
|
159
|
+
build_id=build_id,
|
|
160
|
+
solution_spec=solution_spec or {},
|
|
161
|
+
publish=publish,
|
|
162
|
+
run_label=run_label,
|
|
163
|
+
target=target or {},
|
|
164
|
+
repair_patch=repair_patch or {},
|
|
165
|
+
verify=verify,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
@mcp.tool()
|
|
169
|
+
def solution_build_app(
|
|
170
|
+
profile: str = DEFAULT_PROFILE,
|
|
171
|
+
mode: str = "plan",
|
|
172
|
+
build_id: str = "",
|
|
173
|
+
app_spec: dict[str, Any] | None = None,
|
|
174
|
+
app_key: str = "",
|
|
175
|
+
package_tag_id: int = 0,
|
|
176
|
+
update_only: bool = False,
|
|
177
|
+
publish: bool = True,
|
|
178
|
+
run_label: str | None = None,
|
|
179
|
+
target: dict[str, Any] | None = None,
|
|
180
|
+
repair_patch: dict[str, Any] | None = None,
|
|
181
|
+
) -> dict[str, Any]:
|
|
182
|
+
return self.solution_build_app(
|
|
183
|
+
profile=profile,
|
|
184
|
+
mode=mode,
|
|
185
|
+
build_id=build_id,
|
|
186
|
+
app_spec=app_spec or {},
|
|
187
|
+
app_key=app_key,
|
|
188
|
+
package_tag_id=package_tag_id,
|
|
189
|
+
update_only=update_only,
|
|
190
|
+
publish=publish,
|
|
191
|
+
run_label=run_label,
|
|
192
|
+
target=target or {},
|
|
193
|
+
repair_patch=repair_patch or {},
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
@mcp.tool()
|
|
197
|
+
def solution_build_app_from_requirements(
|
|
198
|
+
profile: str = DEFAULT_PROFILE,
|
|
199
|
+
mode: str = "plan",
|
|
200
|
+
build_id: str = "",
|
|
201
|
+
title: str = "",
|
|
202
|
+
requirement_text: str = "",
|
|
203
|
+
package_tag_id: int = 0,
|
|
204
|
+
package_name: str = "",
|
|
205
|
+
app_key: str = "",
|
|
206
|
+
update_only: bool = False,
|
|
207
|
+
layout_style: str = "auto",
|
|
208
|
+
publish: bool = True,
|
|
209
|
+
run_label: str | None = None,
|
|
210
|
+
include_generated_spec: bool = False,
|
|
211
|
+
) -> dict[str, Any]:
|
|
212
|
+
return self.solution_build_app_from_requirements(
|
|
213
|
+
profile=profile,
|
|
214
|
+
mode=mode,
|
|
215
|
+
build_id=build_id,
|
|
216
|
+
title=title,
|
|
217
|
+
requirement_text=requirement_text,
|
|
218
|
+
package_tag_id=package_tag_id,
|
|
219
|
+
package_name=package_name,
|
|
220
|
+
app_key=app_key,
|
|
221
|
+
update_only=update_only,
|
|
222
|
+
layout_style=layout_style,
|
|
223
|
+
publish=publish,
|
|
224
|
+
run_label=run_label,
|
|
225
|
+
include_generated_spec=include_generated_spec,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
@mcp.tool()
|
|
229
|
+
def solution_build_flow(
|
|
230
|
+
profile: str = DEFAULT_PROFILE,
|
|
231
|
+
mode: str = "plan",
|
|
232
|
+
build_id: str = "",
|
|
233
|
+
flow_spec: dict[str, Any] | None = None,
|
|
234
|
+
publish: bool = True,
|
|
235
|
+
run_label: str | None = None,
|
|
236
|
+
repair_patch: dict[str, Any] | None = None,
|
|
237
|
+
) -> dict[str, Any]:
|
|
238
|
+
return self.solution_build_flow(
|
|
239
|
+
profile=profile,
|
|
240
|
+
mode=mode,
|
|
241
|
+
build_id=build_id,
|
|
242
|
+
flow_spec=flow_spec or {},
|
|
243
|
+
publish=publish,
|
|
244
|
+
run_label=run_label,
|
|
245
|
+
repair_patch=repair_patch or {},
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
@mcp.tool()
|
|
249
|
+
def solution_build_app_flow(
|
|
250
|
+
profile: str = DEFAULT_PROFILE,
|
|
251
|
+
mode: str = "plan",
|
|
252
|
+
build_id: str = "",
|
|
253
|
+
app_flow_spec: dict[str, Any] | None = None,
|
|
254
|
+
publish: bool = True,
|
|
255
|
+
run_label: str | None = None,
|
|
256
|
+
target: dict[str, Any] | None = None,
|
|
257
|
+
repair_patch: dict[str, Any] | None = None,
|
|
258
|
+
) -> dict[str, Any]:
|
|
259
|
+
return self.solution_build_app_flow(
|
|
260
|
+
profile=profile,
|
|
261
|
+
mode=mode,
|
|
262
|
+
build_id=build_id,
|
|
263
|
+
app_flow_spec=app_flow_spec or {},
|
|
264
|
+
publish=publish,
|
|
265
|
+
run_label=run_label,
|
|
266
|
+
target=target or {},
|
|
267
|
+
repair_patch=repair_patch or {},
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
@mcp.tool()
|
|
271
|
+
def solution_schema_example(
|
|
272
|
+
stage: str = "app",
|
|
273
|
+
intent: str = "minimal",
|
|
274
|
+
include_examples: bool = False,
|
|
275
|
+
include_selected_example: bool = False,
|
|
276
|
+
) -> dict[str, Any]:
|
|
277
|
+
return self.solution_schema_example(
|
|
278
|
+
stage=stage,
|
|
279
|
+
intent=intent,
|
|
280
|
+
include_examples=include_examples,
|
|
281
|
+
include_selected_example=include_selected_example,
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
@mcp.tool()
|
|
285
|
+
def solution_build_views(
|
|
286
|
+
profile: str = DEFAULT_PROFILE,
|
|
287
|
+
mode: str = "plan",
|
|
288
|
+
build_id: str = "",
|
|
289
|
+
views_spec: dict[str, Any] | None = None,
|
|
290
|
+
publish: bool = True,
|
|
291
|
+
run_label: str | None = None,
|
|
292
|
+
repair_patch: dict[str, Any] | None = None,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
return self.solution_build_views(
|
|
295
|
+
profile=profile,
|
|
296
|
+
mode=mode,
|
|
297
|
+
build_id=build_id,
|
|
298
|
+
views_spec=views_spec or {},
|
|
299
|
+
publish=publish,
|
|
300
|
+
run_label=run_label,
|
|
301
|
+
repair_patch=repair_patch or {},
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
@mcp.tool()
|
|
305
|
+
def solution_build_analytics_portal(
|
|
306
|
+
profile: str = DEFAULT_PROFILE,
|
|
307
|
+
mode: str = "plan",
|
|
308
|
+
build_id: str = "",
|
|
309
|
+
analytics_portal_spec: dict[str, Any] | None = None,
|
|
310
|
+
publish: bool = True,
|
|
311
|
+
run_label: str | None = None,
|
|
312
|
+
repair_patch: dict[str, Any] | None = None,
|
|
313
|
+
) -> dict[str, Any]:
|
|
314
|
+
return self.solution_build_analytics_portal(
|
|
315
|
+
profile=profile,
|
|
316
|
+
mode=mode,
|
|
317
|
+
build_id=build_id,
|
|
318
|
+
analytics_portal_spec=analytics_portal_spec or {},
|
|
319
|
+
publish=publish,
|
|
320
|
+
run_label=run_label,
|
|
321
|
+
repair_patch=repair_patch or {},
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
@mcp.tool()
|
|
325
|
+
def solution_build_navigation(
|
|
326
|
+
profile: str = DEFAULT_PROFILE,
|
|
327
|
+
mode: str = "plan",
|
|
328
|
+
build_id: str = "",
|
|
329
|
+
navigation_spec: dict[str, Any] | None = None,
|
|
330
|
+
publish: bool = True,
|
|
331
|
+
run_label: str | None = None,
|
|
332
|
+
repair_patch: dict[str, Any] | None = None,
|
|
333
|
+
) -> dict[str, Any]:
|
|
334
|
+
return self.solution_build_navigation(
|
|
335
|
+
profile=profile,
|
|
336
|
+
mode=mode,
|
|
337
|
+
build_id=build_id,
|
|
338
|
+
navigation_spec=navigation_spec or {},
|
|
339
|
+
publish=publish,
|
|
340
|
+
run_label=run_label,
|
|
341
|
+
repair_patch=repair_patch or {},
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
@mcp.tool()
|
|
345
|
+
def solution_build_status(build_id: str = "") -> dict[str, Any]:
|
|
346
|
+
return self.solution_build_status(build_id=build_id)
|
|
347
|
+
|
|
348
|
+
def solution_design_session(
|
|
349
|
+
self,
|
|
350
|
+
*,
|
|
351
|
+
action: str,
|
|
352
|
+
session_id: str,
|
|
353
|
+
stage: str | None,
|
|
354
|
+
design_patch: dict[str, Any],
|
|
355
|
+
metadata: dict[str, Any],
|
|
356
|
+
) -> dict[str, Any]:
|
|
357
|
+
try:
|
|
358
|
+
if action == "start" and not session_id:
|
|
359
|
+
session_id = _generate_session_id(metadata=metadata, stage=stage)
|
|
360
|
+
if action not in {"start", "get", "submit_stage", "finalize"}:
|
|
361
|
+
return self._design_failure_response(session_id, action, "config", "action must be one of: start, get, submit_stage, finalize")
|
|
362
|
+
if not session_id:
|
|
363
|
+
return self._design_failure_response(session_id, action, "config", "session_id is required")
|
|
364
|
+
store = DesignSessionStore.open(session_id=session_id, metadata=metadata)
|
|
365
|
+
if metadata:
|
|
366
|
+
store.set_metadata(metadata)
|
|
367
|
+
|
|
368
|
+
if action == "start":
|
|
369
|
+
evaluation = evaluate_design_session(store.data.get("stage_payloads", {}))
|
|
370
|
+
store.update_progress(
|
|
371
|
+
status=evaluation["status"],
|
|
372
|
+
current_stage=evaluation["current_stage"],
|
|
373
|
+
stage_results=evaluation["stage_results"],
|
|
374
|
+
merged_design_spec=evaluation["merged_design_spec"],
|
|
375
|
+
)
|
|
376
|
+
return self._design_summary(store)
|
|
377
|
+
|
|
378
|
+
if action == "get":
|
|
379
|
+
return self._design_summary(store)
|
|
380
|
+
|
|
381
|
+
if action == "submit_stage":
|
|
382
|
+
if stage not in {item.value for item in DesignStage if item != DesignStage.finalize}:
|
|
383
|
+
return self._design_failure_response(session_id, action, "config", "stage must be one of: discover, design, experience")
|
|
384
|
+
if not isinstance(design_patch, dict) or not design_patch:
|
|
385
|
+
return self._design_failure_response(session_id, action, "config", "design_patch must be a non-empty object")
|
|
386
|
+
existing_payload = store.get_stage_payload(stage)
|
|
387
|
+
merged_payload = merge_design_payload(existing_payload, design_patch)
|
|
388
|
+
store.set_stage_payload(stage, merged_payload)
|
|
389
|
+
evaluation = evaluate_design_session(store.data.get("stage_payloads", {}))
|
|
390
|
+
store.update_progress(
|
|
391
|
+
status=evaluation["status"],
|
|
392
|
+
current_stage=evaluation["current_stage"],
|
|
393
|
+
stage_results=evaluation["stage_results"],
|
|
394
|
+
merged_design_spec=evaluation["merged_design_spec"],
|
|
395
|
+
)
|
|
396
|
+
return self._design_summary(store)
|
|
397
|
+
|
|
398
|
+
finalized = finalize_design_session(store.data.get("stage_payloads", {}))
|
|
399
|
+
store.update_progress(
|
|
400
|
+
status=finalized["status"],
|
|
401
|
+
current_stage=finalized["current_stage"],
|
|
402
|
+
stage_results=finalized["stage_results"],
|
|
403
|
+
merged_design_spec=finalized["merged_design_spec"],
|
|
404
|
+
)
|
|
405
|
+
store.mark_finalized(
|
|
406
|
+
normalized_solution_spec=finalized["normalized_solution_spec"],
|
|
407
|
+
execution_plan=finalized["execution_plan"],
|
|
408
|
+
)
|
|
409
|
+
return self._design_summary(store)
|
|
410
|
+
except ValidationError as exc:
|
|
411
|
+
return self._design_failure_response(session_id, action, "validation", exc.errors())
|
|
412
|
+
except ValueError as exc:
|
|
413
|
+
return self._design_failure_response(session_id, action, "config", str(exc))
|
|
414
|
+
except Exception as exc: # noqa: BLE001
|
|
415
|
+
return self._design_failure_response(session_id, action, "runtime", str(exc))
|
|
416
|
+
|
|
417
|
+
def solution_bootstrap(
|
|
418
|
+
self,
|
|
419
|
+
*,
|
|
420
|
+
profile: str,
|
|
421
|
+
mode: str,
|
|
422
|
+
solution_spec: dict[str, Any],
|
|
423
|
+
idempotency_key: str,
|
|
424
|
+
publish: bool,
|
|
425
|
+
run_label: str | None,
|
|
426
|
+
target: dict[str, Any],
|
|
427
|
+
repair_patch: dict[str, Any],
|
|
428
|
+
) -> dict[str, Any]:
|
|
429
|
+
mode = _normalize_staged_build_mode(mode)
|
|
430
|
+
if mode not in {"preflight", "plan", "apply", "repair"}:
|
|
431
|
+
return self._failure_response(mode, idempotency_key, "config", "mode must be one of: preflight, plan, apply, repair")
|
|
432
|
+
if not idempotency_key:
|
|
433
|
+
return self._failure_response(mode, idempotency_key, "config", "idempotency_key is required")
|
|
434
|
+
if not isinstance(solution_spec, dict) or not solution_spec:
|
|
435
|
+
return self._failure_response(mode, idempotency_key, "config", "solution_spec must be a non-empty object")
|
|
436
|
+
if mode != "repair" and repair_patch:
|
|
437
|
+
return self._failure_response(mode, idempotency_key, "config", "repair_patch is only supported in repair mode")
|
|
438
|
+
|
|
439
|
+
def runner() -> dict[str, Any]:
|
|
440
|
+
try:
|
|
441
|
+
base_spec_payload = deepcopy(solution_spec)
|
|
442
|
+
if mode == "repair" and repair_patch:
|
|
443
|
+
base_spec_payload = deep_merge(base_spec_payload, repair_patch)
|
|
444
|
+
parsed = SolutionSpec.model_validate(base_spec_payload)
|
|
445
|
+
normalized = normalize_solution_spec(parsed)
|
|
446
|
+
compiled = compile_solution(normalized)
|
|
447
|
+
package_tag_id = _resolve_target_package_tag_id(target)
|
|
448
|
+
if package_tag_id is not None:
|
|
449
|
+
compiled = _bind_existing_package_target(compiled, package_tag_id)
|
|
450
|
+
request_fingerprint = fingerprint_payload(
|
|
451
|
+
{
|
|
452
|
+
"mode": "apply" if mode == "repair" else mode,
|
|
453
|
+
"publish": publish,
|
|
454
|
+
"solution_spec": normalized.model_dump(mode="json"),
|
|
455
|
+
"target": target,
|
|
456
|
+
}
|
|
457
|
+
)
|
|
458
|
+
if mode in {"preflight", "plan"}:
|
|
459
|
+
store = BuildAssemblyStore.open(build_id=idempotency_key)
|
|
460
|
+
store.set_manifest(normalized.model_dump(mode="json"))
|
|
461
|
+
return {
|
|
462
|
+
"build_id": idempotency_key,
|
|
463
|
+
"mode": mode,
|
|
464
|
+
"idempotency_key": idempotency_key,
|
|
465
|
+
"normalized_solution_spec": normalized.model_dump(mode="json"),
|
|
466
|
+
"execution_plan": compiled.execution_plan.as_dict(),
|
|
467
|
+
"artifacts": {},
|
|
468
|
+
"step_results": {},
|
|
469
|
+
"publish_results": {},
|
|
470
|
+
"status": "preflighted" if mode == "preflight" else "planned",
|
|
471
|
+
"errors": [],
|
|
472
|
+
"run_path": None,
|
|
473
|
+
"build_path": str(store.path),
|
|
474
|
+
"suggested_next_call": _solution_build_next_call(
|
|
475
|
+
tool_name="solution_build_all",
|
|
476
|
+
mode=mode,
|
|
477
|
+
build_id=idempotency_key,
|
|
478
|
+
),
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
store = RunArtifactStore.open(
|
|
482
|
+
idempotency_key=idempotency_key,
|
|
483
|
+
normalized_solution_spec=normalized.model_dump(mode="json"),
|
|
484
|
+
request_fingerprint=request_fingerprint,
|
|
485
|
+
run_label=run_label,
|
|
486
|
+
)
|
|
487
|
+
if mode == "apply":
|
|
488
|
+
store.ensure_apply_fingerprint(request_fingerprint)
|
|
489
|
+
if package_tag_id is not None:
|
|
490
|
+
store.set_artifact("package", "tag_id", package_tag_id)
|
|
491
|
+
store.set_artifact("package", "reused", True)
|
|
492
|
+
result = self._build_executor().execute(profile=profile, compiled=compiled, store=store, publish=publish, mode=mode)
|
|
493
|
+
return {
|
|
494
|
+
"mode": mode,
|
|
495
|
+
"idempotency_key": idempotency_key,
|
|
496
|
+
"normalized_solution_spec": normalized.model_dump(mode="json"),
|
|
497
|
+
"execution_plan": compiled.execution_plan.as_dict(),
|
|
498
|
+
"artifacts": result["artifacts"],
|
|
499
|
+
"step_results": result["step_results"],
|
|
500
|
+
"publish_results": _extract_publish_results(result["step_results"]),
|
|
501
|
+
"status": result["status"],
|
|
502
|
+
"errors": result["errors"],
|
|
503
|
+
"run_path": result["run_path"],
|
|
504
|
+
}
|
|
505
|
+
except ValidationError as exc:
|
|
506
|
+
return self._failure_response(mode, idempotency_key, "validation", exc.errors())
|
|
507
|
+
except ValueError as exc:
|
|
508
|
+
return self._failure_response(mode, idempotency_key, "config", str(exc))
|
|
509
|
+
except Exception as exc: # noqa: BLE001
|
|
510
|
+
return self._failure_response(mode, idempotency_key, "runtime", str(exc))
|
|
511
|
+
|
|
512
|
+
if mode in {"preflight", "plan"}:
|
|
513
|
+
return runner()
|
|
514
|
+
return self._run(profile, lambda _session_profile, _context: runner())
|
|
515
|
+
|
|
516
|
+
def solution_build_app_flow(
|
|
517
|
+
self,
|
|
518
|
+
*,
|
|
519
|
+
profile: str,
|
|
520
|
+
mode: str,
|
|
521
|
+
build_id: str,
|
|
522
|
+
app_flow_spec: dict[str, Any],
|
|
523
|
+
publish: bool,
|
|
524
|
+
run_label: str | None,
|
|
525
|
+
target: dict[str, Any],
|
|
526
|
+
repair_patch: dict[str, Any],
|
|
527
|
+
) -> dict[str, Any]:
|
|
528
|
+
mode = _normalize_staged_build_mode(mode)
|
|
529
|
+
return self._stage_build(
|
|
530
|
+
profile=profile,
|
|
531
|
+
mode=mode,
|
|
532
|
+
build_id=build_id,
|
|
533
|
+
stage_name="app_flow",
|
|
534
|
+
stage_payload=app_flow_spec,
|
|
535
|
+
stage_model=AppFlowBuildSpec,
|
|
536
|
+
publish=publish,
|
|
537
|
+
run_label=run_label,
|
|
538
|
+
target=target,
|
|
539
|
+
repair_patch=repair_patch,
|
|
540
|
+
)
|
|
541
|
+
|
|
542
|
+
def solution_build_app(
|
|
543
|
+
self,
|
|
544
|
+
*,
|
|
545
|
+
profile: str,
|
|
546
|
+
mode: str,
|
|
547
|
+
build_id: str,
|
|
548
|
+
app_spec: dict[str, Any],
|
|
549
|
+
app_key: str = "",
|
|
550
|
+
package_tag_id: int = 0,
|
|
551
|
+
update_only: bool = False,
|
|
552
|
+
publish: bool,
|
|
553
|
+
run_label: str | None,
|
|
554
|
+
target: dict[str, Any],
|
|
555
|
+
repair_patch: dict[str, Any],
|
|
556
|
+
) -> dict[str, Any]:
|
|
557
|
+
mode = _normalize_staged_build_mode(mode)
|
|
558
|
+
resolved_app_spec = deepcopy(app_spec) if isinstance(app_spec, dict) else {}
|
|
559
|
+
if not resolved_app_spec and build_id:
|
|
560
|
+
resolved_app_spec = self._load_generated_app_spec(build_id) or {}
|
|
561
|
+
resolved_target = _merge_app_target(
|
|
562
|
+
target,
|
|
563
|
+
app_key=app_key,
|
|
564
|
+
package_tag_id=package_tag_id,
|
|
565
|
+
update_only=update_only,
|
|
566
|
+
)
|
|
567
|
+
return self._stage_build(
|
|
568
|
+
profile=profile,
|
|
569
|
+
mode=mode,
|
|
570
|
+
build_id=build_id,
|
|
571
|
+
stage_name="app_flow",
|
|
572
|
+
stage_payload=resolved_app_spec,
|
|
573
|
+
stage_model=AppBuildSpec,
|
|
574
|
+
publish=publish,
|
|
575
|
+
run_label=run_label,
|
|
576
|
+
target=resolved_target,
|
|
577
|
+
repair_patch=repair_patch,
|
|
578
|
+
public_stage_name="app",
|
|
579
|
+
tool_name="solution_build_app",
|
|
580
|
+
projection_name="app",
|
|
581
|
+
)
|
|
582
|
+
|
|
583
|
+
def solution_build_app_from_requirements(
|
|
584
|
+
self,
|
|
585
|
+
*,
|
|
586
|
+
profile: str,
|
|
587
|
+
mode: str,
|
|
588
|
+
build_id: str,
|
|
589
|
+
title: str,
|
|
590
|
+
requirement_text: str,
|
|
591
|
+
package_tag_id: int,
|
|
592
|
+
package_name: str,
|
|
593
|
+
app_key: str = "",
|
|
594
|
+
update_only: bool = False,
|
|
595
|
+
layout_style: str,
|
|
596
|
+
publish: bool,
|
|
597
|
+
run_label: str | None,
|
|
598
|
+
include_generated_spec: bool,
|
|
599
|
+
) -> dict[str, Any]:
|
|
600
|
+
mode = _normalize_staged_build_mode(mode)
|
|
601
|
+
if mode in {"preflight", "plan"} and not build_id:
|
|
602
|
+
build_id = _generate_build_id(run_label=run_label, stage_name="app")
|
|
603
|
+
resolved_target = _merge_app_target(
|
|
604
|
+
{},
|
|
605
|
+
app_key=app_key,
|
|
606
|
+
package_tag_id=package_tag_id,
|
|
607
|
+
update_only=update_only,
|
|
608
|
+
)
|
|
609
|
+
resolved_package = self._resolve_builder_package_reference(
|
|
610
|
+
profile=profile,
|
|
611
|
+
package_tag_id=package_tag_id,
|
|
612
|
+
package_name=package_name,
|
|
613
|
+
)
|
|
614
|
+
if resolved_package.get("status") == "failed":
|
|
615
|
+
return resolved_package["response"]
|
|
616
|
+
existing_app = self._resolve_builder_existing_app_reference(
|
|
617
|
+
profile=profile,
|
|
618
|
+
app_key=app_key,
|
|
619
|
+
package_resolution=resolved_package,
|
|
620
|
+
update_only=_target_is_update_only(resolved_target),
|
|
621
|
+
)
|
|
622
|
+
if existing_app.get("status") == "failed":
|
|
623
|
+
return existing_app["response"]
|
|
624
|
+
effective_package_resolution = existing_app.get("package_resolution") if existing_app.get("package_resolution") else resolved_package
|
|
625
|
+
try:
|
|
626
|
+
generated = build_app_spec_from_requirements(
|
|
627
|
+
title=title,
|
|
628
|
+
requirement_text=requirement_text,
|
|
629
|
+
package_name=effective_package_resolution.get("tag_name"),
|
|
630
|
+
layout_style=layout_style,
|
|
631
|
+
)
|
|
632
|
+
except RequirementsBuildError as exc:
|
|
633
|
+
return self._requirements_failure_response(
|
|
634
|
+
mode=mode,
|
|
635
|
+
build_id=build_id,
|
|
636
|
+
title=title,
|
|
637
|
+
requirement_text=requirement_text,
|
|
638
|
+
package_resolution=effective_package_resolution,
|
|
639
|
+
app_key=app_key,
|
|
640
|
+
update_only=_target_is_update_only(resolved_target),
|
|
641
|
+
layout_style=layout_style,
|
|
642
|
+
publish=publish,
|
|
643
|
+
run_label=run_label,
|
|
644
|
+
detail=str(exc),
|
|
645
|
+
error_code=exc.error_code,
|
|
646
|
+
recoverable=exc.recoverable,
|
|
647
|
+
missing_required_fields=exc.missing_required_fields,
|
|
648
|
+
invalid_field_types=exc.invalid_field_types,
|
|
649
|
+
error_details=exc.details,
|
|
650
|
+
)
|
|
651
|
+
except ValueError as exc:
|
|
652
|
+
return self._requirements_failure_response(
|
|
653
|
+
mode=mode,
|
|
654
|
+
build_id=build_id,
|
|
655
|
+
title=title,
|
|
656
|
+
requirement_text=requirement_text,
|
|
657
|
+
package_resolution=effective_package_resolution,
|
|
658
|
+
app_key=app_key,
|
|
659
|
+
update_only=_target_is_update_only(resolved_target),
|
|
660
|
+
layout_style=layout_style,
|
|
661
|
+
publish=publish,
|
|
662
|
+
run_label=run_label,
|
|
663
|
+
detail=str(exc),
|
|
664
|
+
)
|
|
665
|
+
if build_id:
|
|
666
|
+
self._persist_generated_app_spec(
|
|
667
|
+
build_id=build_id,
|
|
668
|
+
generated=generated,
|
|
669
|
+
package_resolution=effective_package_resolution,
|
|
670
|
+
title=title,
|
|
671
|
+
requirement_text=requirement_text,
|
|
672
|
+
layout_style=layout_style,
|
|
673
|
+
)
|
|
674
|
+
stage_result = self.solution_build_app(
|
|
675
|
+
profile=profile,
|
|
676
|
+
mode=mode,
|
|
677
|
+
build_id=build_id,
|
|
678
|
+
app_spec=generated.app_spec,
|
|
679
|
+
app_key=app_key,
|
|
680
|
+
package_tag_id=int(effective_package_resolution.get("tag_id") or 0),
|
|
681
|
+
update_only=_target_is_update_only(resolved_target),
|
|
682
|
+
publish=publish,
|
|
683
|
+
run_label=run_label,
|
|
684
|
+
target=_requirements_target(
|
|
685
|
+
effective_package_resolution,
|
|
686
|
+
app_key=app_key if isinstance(app_key, str) else "",
|
|
687
|
+
update_only=_target_is_update_only(resolved_target),
|
|
688
|
+
),
|
|
689
|
+
repair_patch={},
|
|
690
|
+
)
|
|
691
|
+
return self._requirements_success_response(
|
|
692
|
+
mode=mode,
|
|
693
|
+
title=title,
|
|
694
|
+
requirement_text=requirement_text,
|
|
695
|
+
package_resolution=effective_package_resolution,
|
|
696
|
+
layout_style=layout_style,
|
|
697
|
+
publish=publish,
|
|
698
|
+
run_label=run_label,
|
|
699
|
+
generated=generated,
|
|
700
|
+
stage_result=stage_result,
|
|
701
|
+
include_generated_spec=include_generated_spec,
|
|
702
|
+
)
|
|
703
|
+
|
|
704
|
+
def solution_build_flow(
|
|
705
|
+
self,
|
|
706
|
+
*,
|
|
707
|
+
profile: str,
|
|
708
|
+
mode: str,
|
|
709
|
+
build_id: str,
|
|
710
|
+
flow_spec: dict[str, Any],
|
|
711
|
+
publish: bool,
|
|
712
|
+
run_label: str | None,
|
|
713
|
+
repair_patch: dict[str, Any],
|
|
714
|
+
) -> dict[str, Any]:
|
|
715
|
+
mode = _normalize_staged_build_mode(mode)
|
|
716
|
+
return self._stage_build(
|
|
717
|
+
profile=profile,
|
|
718
|
+
mode=mode,
|
|
719
|
+
build_id=build_id,
|
|
720
|
+
stage_name="app_flow",
|
|
721
|
+
stage_payload=flow_spec,
|
|
722
|
+
stage_model=FlowBuildSpec,
|
|
723
|
+
publish=publish,
|
|
724
|
+
run_label=run_label,
|
|
725
|
+
target={},
|
|
726
|
+
repair_patch=repair_patch,
|
|
727
|
+
public_stage_name="flow",
|
|
728
|
+
tool_name="solution_build_flow",
|
|
729
|
+
projection_name="flow",
|
|
730
|
+
)
|
|
731
|
+
|
|
732
|
+
def solution_build_all(
|
|
733
|
+
self,
|
|
734
|
+
*,
|
|
735
|
+
profile: str,
|
|
736
|
+
mode: str,
|
|
737
|
+
build_id: str,
|
|
738
|
+
solution_spec: dict[str, Any],
|
|
739
|
+
publish: bool,
|
|
740
|
+
run_label: str | None,
|
|
741
|
+
target: dict[str, Any],
|
|
742
|
+
repair_patch: dict[str, Any],
|
|
743
|
+
verify: bool,
|
|
744
|
+
) -> dict[str, Any]:
|
|
745
|
+
mode = _normalize_staged_build_mode(mode)
|
|
746
|
+
if mode in {"preflight", "plan"} and not build_id:
|
|
747
|
+
build_id = _generate_build_id(run_label=run_label, stage_name="all")
|
|
748
|
+
if mode not in STAGED_BUILD_MODES:
|
|
749
|
+
return self._all_failure_response(build_id, mode, "config", "mode must be one of: preflight, plan, apply, repair")
|
|
750
|
+
if not build_id:
|
|
751
|
+
return self._all_failure_response(build_id, mode, "config", "build_id is required")
|
|
752
|
+
if not isinstance(solution_spec, dict):
|
|
753
|
+
return self._all_failure_response(build_id, mode, "config", "solution_spec must be an object")
|
|
754
|
+
if mode != "repair" and not solution_spec:
|
|
755
|
+
return self._all_failure_response(build_id, mode, "config", "solution_spec must be a non-empty object")
|
|
756
|
+
if mode != "repair" and repair_patch:
|
|
757
|
+
return self._all_failure_response(build_id, mode, "config", "repair_patch is only supported in repair mode")
|
|
758
|
+
|
|
759
|
+
def runner() -> dict[str, Any]:
|
|
760
|
+
try:
|
|
761
|
+
base_solution_spec = deepcopy(solution_spec)
|
|
762
|
+
if mode == "repair" and not base_solution_spec:
|
|
763
|
+
assembly = BuildAssemblyStore.open(build_id=build_id, create=False)
|
|
764
|
+
base_solution_spec = assembly.get_manifest()
|
|
765
|
+
if not base_solution_spec.get("entities"):
|
|
766
|
+
return self._all_failure_response(
|
|
767
|
+
build_id,
|
|
768
|
+
mode,
|
|
769
|
+
"config",
|
|
770
|
+
"repair requires a stored build manifest or a non-empty solution_spec",
|
|
771
|
+
)
|
|
772
|
+
resolved_solution_spec = self._resolve_full_solution_spec(
|
|
773
|
+
solution_spec=base_solution_spec,
|
|
774
|
+
mode=mode,
|
|
775
|
+
repair_patch=repair_patch,
|
|
776
|
+
)
|
|
777
|
+
stage_payloads = _split_solution_spec_into_stage_payloads(resolved_solution_spec)
|
|
778
|
+
stage_results: dict[str, Any] = {}
|
|
779
|
+
overall_errors: list[dict[str, Any]] = []
|
|
780
|
+
if mode == "preflight":
|
|
781
|
+
overall_status = "preflighted"
|
|
782
|
+
elif mode == "plan":
|
|
783
|
+
overall_status = "planned"
|
|
784
|
+
else:
|
|
785
|
+
overall_status = "success"
|
|
786
|
+
|
|
787
|
+
for stage_name in STAGE_ORDER:
|
|
788
|
+
stage_payload = stage_payloads[stage_name]
|
|
789
|
+
if _stage_payload_is_empty(stage_name, stage_payload):
|
|
790
|
+
assembly = BuildAssemblyStore.open(build_id=build_id)
|
|
791
|
+
assembly.add_stage_history(
|
|
792
|
+
{
|
|
793
|
+
"stage": stage_name,
|
|
794
|
+
"mode": mode,
|
|
795
|
+
"status": "skipped",
|
|
796
|
+
"reason": _stage_skip_reason(stage_name),
|
|
797
|
+
"execution_step_count": 0,
|
|
798
|
+
}
|
|
799
|
+
)
|
|
800
|
+
stage_results[stage_name] = self._stage_skip_response(
|
|
801
|
+
build_id=build_id,
|
|
802
|
+
stage_name=stage_name,
|
|
803
|
+
mode=mode,
|
|
804
|
+
assembly=assembly,
|
|
805
|
+
tool_name=STAGE_TOOL_NAMES.get(stage_name, "solution_build_app_flow"),
|
|
806
|
+
)
|
|
807
|
+
continue
|
|
808
|
+
|
|
809
|
+
if stage_name == "app_flow":
|
|
810
|
+
result = self._stage_build(
|
|
811
|
+
profile=profile,
|
|
812
|
+
mode=mode,
|
|
813
|
+
build_id=build_id,
|
|
814
|
+
stage_name=stage_name,
|
|
815
|
+
stage_payload=stage_payload,
|
|
816
|
+
stage_model=AppFlowBuildSpec,
|
|
817
|
+
publish=publish,
|
|
818
|
+
run_label=run_label,
|
|
819
|
+
target=target,
|
|
820
|
+
repair_patch={},
|
|
821
|
+
)
|
|
822
|
+
elif stage_name == "views":
|
|
823
|
+
result = self._stage_build(
|
|
824
|
+
profile=profile,
|
|
825
|
+
mode=mode,
|
|
826
|
+
build_id=build_id,
|
|
827
|
+
stage_name=stage_name,
|
|
828
|
+
stage_payload=stage_payload,
|
|
829
|
+
stage_model=ViewsBuildSpec,
|
|
830
|
+
publish=publish,
|
|
831
|
+
run_label=run_label,
|
|
832
|
+
target={},
|
|
833
|
+
repair_patch={},
|
|
834
|
+
)
|
|
835
|
+
elif stage_name == "analytics_portal":
|
|
836
|
+
result = self._stage_build(
|
|
837
|
+
profile=profile,
|
|
838
|
+
mode=mode,
|
|
839
|
+
build_id=build_id,
|
|
840
|
+
stage_name=stage_name,
|
|
841
|
+
stage_payload=stage_payload,
|
|
842
|
+
stage_model=AnalyticsPortalBuildSpec,
|
|
843
|
+
publish=publish,
|
|
844
|
+
run_label=run_label,
|
|
845
|
+
target={},
|
|
846
|
+
repair_patch={},
|
|
847
|
+
)
|
|
848
|
+
else:
|
|
849
|
+
result = self._stage_build(
|
|
850
|
+
profile=profile,
|
|
851
|
+
mode=mode,
|
|
852
|
+
build_id=build_id,
|
|
853
|
+
stage_name=stage_name,
|
|
854
|
+
stage_payload=stage_payload,
|
|
855
|
+
stage_model=NavigationBuildSpec,
|
|
856
|
+
publish=publish,
|
|
857
|
+
run_label=run_label,
|
|
858
|
+
target={},
|
|
859
|
+
repair_patch={},
|
|
860
|
+
)
|
|
861
|
+
stage_results[stage_name] = result
|
|
862
|
+
if result.get("status") == "failed":
|
|
863
|
+
overall_status = "failed"
|
|
864
|
+
overall_errors = _sanitize_errors(result.get("errors", []))
|
|
865
|
+
break
|
|
866
|
+
if mode in {"apply", "repair"} and result.get("status") != "success":
|
|
867
|
+
overall_status = str(result.get("status"))
|
|
868
|
+
elif mode == "plan" and result.get("status") != "planned":
|
|
869
|
+
overall_status = str(result.get("status"))
|
|
870
|
+
elif mode == "preflight" and result.get("status") != "preflighted":
|
|
871
|
+
overall_status = str(result.get("status"))
|
|
872
|
+
|
|
873
|
+
assembly = BuildAssemblyStore.open(build_id=build_id)
|
|
874
|
+
if overall_status != "failed" and mode in {"apply", "repair"}:
|
|
875
|
+
assembly.mark_status("success")
|
|
876
|
+
build_summary = _build_summary(assembly)
|
|
877
|
+
artifacts = assembly.get_artifacts()
|
|
878
|
+
verification = None
|
|
879
|
+
if verify and overall_status == "success":
|
|
880
|
+
verification = self._verify_solution_build(
|
|
881
|
+
profile=profile,
|
|
882
|
+
build_id=build_id,
|
|
883
|
+
artifacts=artifacts,
|
|
884
|
+
solution_spec=resolved_solution_spec,
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
return {
|
|
888
|
+
"build_id": build_id,
|
|
889
|
+
"mode": mode,
|
|
890
|
+
"normalized_solution_spec": resolved_solution_spec,
|
|
891
|
+
"stage_results": stage_results,
|
|
892
|
+
"stage_statuses": {stage: str(result.get("status")) for stage, result in stage_results.items()},
|
|
893
|
+
"artifacts": artifacts,
|
|
894
|
+
"status": overall_status,
|
|
895
|
+
"errors": overall_errors,
|
|
896
|
+
"verification": verification,
|
|
897
|
+
"build_summary": build_summary,
|
|
898
|
+
"build_path": build_summary.get("build_path"),
|
|
899
|
+
"suggested_next_call": _solution_build_next_call(tool_name="solution_build_all", mode=mode, build_id=build_id),
|
|
900
|
+
}
|
|
901
|
+
except ValidationError as exc:
|
|
902
|
+
return self._all_failure_response(build_id, mode, "validation", exc.errors())
|
|
903
|
+
except FileNotFoundError:
|
|
904
|
+
return self._all_failure_response(
|
|
905
|
+
build_id,
|
|
906
|
+
mode,
|
|
907
|
+
"config",
|
|
908
|
+
"repair requires an existing build_id with a stored build manifest",
|
|
909
|
+
)
|
|
910
|
+
except ValueError as exc:
|
|
911
|
+
return self._all_failure_response(build_id, mode, "config", str(exc))
|
|
912
|
+
except Exception as exc: # noqa: BLE001
|
|
913
|
+
return self._all_failure_response(build_id, mode, "runtime", str(exc))
|
|
914
|
+
|
|
915
|
+
if mode in {"preflight", "plan"}:
|
|
916
|
+
return runner()
|
|
917
|
+
return self._run(profile, lambda _session_profile, _context: runner())
|
|
918
|
+
|
|
919
|
+
def solution_build_views(
|
|
920
|
+
self,
|
|
921
|
+
*,
|
|
922
|
+
profile: str,
|
|
923
|
+
mode: str,
|
|
924
|
+
build_id: str,
|
|
925
|
+
views_spec: dict[str, Any],
|
|
926
|
+
publish: bool,
|
|
927
|
+
run_label: str | None,
|
|
928
|
+
repair_patch: dict[str, Any],
|
|
929
|
+
) -> dict[str, Any]:
|
|
930
|
+
mode = _normalize_staged_build_mode(mode)
|
|
931
|
+
return self._stage_build(
|
|
932
|
+
profile=profile,
|
|
933
|
+
mode=mode,
|
|
934
|
+
build_id=build_id,
|
|
935
|
+
stage_name="views",
|
|
936
|
+
stage_payload=views_spec,
|
|
937
|
+
stage_model=ViewsBuildSpec,
|
|
938
|
+
publish=publish,
|
|
939
|
+
run_label=run_label,
|
|
940
|
+
target={},
|
|
941
|
+
repair_patch=repair_patch,
|
|
942
|
+
)
|
|
943
|
+
|
|
944
|
+
def solution_schema_example(
|
|
945
|
+
self,
|
|
946
|
+
*,
|
|
947
|
+
stage: str,
|
|
948
|
+
intent: str,
|
|
949
|
+
include_examples: bool = False,
|
|
950
|
+
include_selected_example: bool = False,
|
|
951
|
+
) -> dict[str, Any]:
|
|
952
|
+
resolved_stage = _normalize_solution_schema_stage(stage)
|
|
953
|
+
resolved_intent = _normalize_solution_schema_intent(intent)
|
|
954
|
+
if resolved_stage is None:
|
|
955
|
+
return {
|
|
956
|
+
"stage": stage,
|
|
957
|
+
"intent": intent,
|
|
958
|
+
"status": "failed",
|
|
959
|
+
"errors": [{"category": "config", "detail": f"stage must be one of: {', '.join(sorted(SOLUTION_SCHEMA_STAGES))}"}],
|
|
960
|
+
"available_stages": sorted(SOLUTION_SCHEMA_STAGES),
|
|
961
|
+
**_solution_error_fields(
|
|
962
|
+
category="config",
|
|
963
|
+
detail=f"stage must be one of: {', '.join(sorted(SOLUTION_SCHEMA_STAGES))}",
|
|
964
|
+
suggested_next_call={"tool_name": "solution_schema_example", "arguments": {"stage": "app", "intent": "minimal"}},
|
|
965
|
+
),
|
|
966
|
+
}
|
|
967
|
+
if resolved_intent is None:
|
|
968
|
+
return {
|
|
969
|
+
"stage": resolved_stage,
|
|
970
|
+
"intent": intent,
|
|
971
|
+
"status": "failed",
|
|
972
|
+
"errors": [{"category": "config", "detail": f"intent must be one of: {', '.join(sorted(SOLUTION_SCHEMA_INTENTS))}"}],
|
|
973
|
+
"available_intents": sorted(SOLUTION_SCHEMA_INTENTS),
|
|
974
|
+
**_solution_error_fields(
|
|
975
|
+
category="config",
|
|
976
|
+
detail=f"intent must be one of: {', '.join(sorted(SOLUTION_SCHEMA_INTENTS))}",
|
|
977
|
+
suggested_next_call={"tool_name": "solution_schema_example", "arguments": {"stage": resolved_stage, "intent": "minimal"}},
|
|
978
|
+
),
|
|
979
|
+
}
|
|
980
|
+
response = _solution_schema_example_payload(
|
|
981
|
+
stage=resolved_stage,
|
|
982
|
+
intent=resolved_intent,
|
|
983
|
+
include_examples=include_examples,
|
|
984
|
+
include_selected_example=include_selected_example,
|
|
985
|
+
)
|
|
986
|
+
response["requested_stage"] = stage
|
|
987
|
+
response["requested_intent"] = intent
|
|
988
|
+
response["resolved_stage"] = resolved_stage
|
|
989
|
+
response["resolved_intent"] = resolved_intent
|
|
990
|
+
return response
|
|
991
|
+
|
|
992
|
+
def solution_build_analytics_portal(
|
|
993
|
+
self,
|
|
994
|
+
*,
|
|
995
|
+
profile: str,
|
|
996
|
+
mode: str,
|
|
997
|
+
build_id: str,
|
|
998
|
+
analytics_portal_spec: dict[str, Any],
|
|
999
|
+
publish: bool,
|
|
1000
|
+
run_label: str | None,
|
|
1001
|
+
repair_patch: dict[str, Any],
|
|
1002
|
+
) -> dict[str, Any]:
|
|
1003
|
+
mode = _normalize_staged_build_mode(mode)
|
|
1004
|
+
return self._stage_build(
|
|
1005
|
+
profile=profile,
|
|
1006
|
+
mode=mode,
|
|
1007
|
+
build_id=build_id,
|
|
1008
|
+
stage_name="analytics_portal",
|
|
1009
|
+
stage_payload=analytics_portal_spec,
|
|
1010
|
+
stage_model=AnalyticsPortalBuildSpec,
|
|
1011
|
+
publish=publish,
|
|
1012
|
+
run_label=run_label,
|
|
1013
|
+
target={},
|
|
1014
|
+
repair_patch=repair_patch,
|
|
1015
|
+
)
|
|
1016
|
+
|
|
1017
|
+
def solution_build_navigation(
|
|
1018
|
+
self,
|
|
1019
|
+
*,
|
|
1020
|
+
profile: str,
|
|
1021
|
+
mode: str,
|
|
1022
|
+
build_id: str,
|
|
1023
|
+
navigation_spec: dict[str, Any],
|
|
1024
|
+
publish: bool,
|
|
1025
|
+
run_label: str | None,
|
|
1026
|
+
repair_patch: dict[str, Any],
|
|
1027
|
+
) -> dict[str, Any]:
|
|
1028
|
+
mode = _normalize_staged_build_mode(mode)
|
|
1029
|
+
return self._stage_build(
|
|
1030
|
+
profile=profile,
|
|
1031
|
+
mode=mode,
|
|
1032
|
+
build_id=build_id,
|
|
1033
|
+
stage_name="navigation",
|
|
1034
|
+
stage_payload=navigation_spec,
|
|
1035
|
+
stage_model=NavigationBuildSpec,
|
|
1036
|
+
publish=publish,
|
|
1037
|
+
run_label=run_label,
|
|
1038
|
+
target={},
|
|
1039
|
+
repair_patch=repair_patch,
|
|
1040
|
+
)
|
|
1041
|
+
|
|
1042
|
+
def solution_build_status(self, *, build_id: str) -> dict[str, Any]:
|
|
1043
|
+
if not build_id:
|
|
1044
|
+
error_fields = _solution_error_fields(
|
|
1045
|
+
category="config",
|
|
1046
|
+
detail="build_id is required",
|
|
1047
|
+
suggested_next_call=None,
|
|
1048
|
+
)
|
|
1049
|
+
return {
|
|
1050
|
+
"build_id": build_id,
|
|
1051
|
+
"status": "failed",
|
|
1052
|
+
"errors": [{"category": "config", "detail": "build_id is required"}],
|
|
1053
|
+
"build_path": None,
|
|
1054
|
+
**error_fields,
|
|
1055
|
+
}
|
|
1056
|
+
assembly = BuildAssemblyStore.open(build_id=build_id)
|
|
1057
|
+
summary = _build_summary(assembly)
|
|
1058
|
+
summary["errors"] = []
|
|
1059
|
+
return summary
|
|
1060
|
+
|
|
1061
|
+
def _stage_build(
|
|
1062
|
+
self,
|
|
1063
|
+
*,
|
|
1064
|
+
profile: str,
|
|
1065
|
+
mode: str,
|
|
1066
|
+
build_id: str,
|
|
1067
|
+
stage_name: str,
|
|
1068
|
+
stage_payload: dict[str, Any],
|
|
1069
|
+
stage_model: type[BaseModel],
|
|
1070
|
+
publish: bool,
|
|
1071
|
+
run_label: str | None,
|
|
1072
|
+
target: dict[str, Any],
|
|
1073
|
+
repair_patch: dict[str, Any],
|
|
1074
|
+
public_stage_name: str | None = None,
|
|
1075
|
+
tool_name: str | None = None,
|
|
1076
|
+
projection_name: str | None = None,
|
|
1077
|
+
) -> dict[str, Any]:
|
|
1078
|
+
mode = _normalize_staged_build_mode(mode)
|
|
1079
|
+
public_stage_name = public_stage_name or stage_name
|
|
1080
|
+
tool_name = tool_name or STAGE_TOOL_NAMES.get(stage_name, "solution_build_app_flow")
|
|
1081
|
+
projection_name = projection_name or public_stage_name
|
|
1082
|
+
if mode in {"preflight", "plan"} and not build_id:
|
|
1083
|
+
build_id = _generate_build_id(run_label=run_label, stage_name=stage_name)
|
|
1084
|
+
assembly = BuildAssemblyStore.open(build_id=build_id) if build_id else None
|
|
1085
|
+
if mode not in STAGED_BUILD_MODES:
|
|
1086
|
+
return self._stage_failure_response(build_id, public_stage_name, mode, "config", "mode must be one of: preflight, plan, apply, repair", assembly, tool_name=tool_name)
|
|
1087
|
+
if not build_id:
|
|
1088
|
+
return self._stage_failure_response(build_id, public_stage_name, mode, "config", "build_id is required", assembly, tool_name=tool_name)
|
|
1089
|
+
if mode != "repair" and repair_patch:
|
|
1090
|
+
return self._stage_failure_response(build_id, public_stage_name, mode, "config", "repair_patch is only supported in repair mode", assembly, tool_name=tool_name)
|
|
1091
|
+
if stage_name != "app_flow" and target:
|
|
1092
|
+
return self._stage_failure_response(build_id, public_stage_name, mode, "config", "target is only supported for the app_flow stage", assembly, tool_name=tool_name)
|
|
1093
|
+
|
|
1094
|
+
def runner() -> dict[str, Any]:
|
|
1095
|
+
return self._execute_stage_build(
|
|
1096
|
+
profile=profile,
|
|
1097
|
+
build_id=build_id,
|
|
1098
|
+
stage_name=stage_name,
|
|
1099
|
+
public_stage_name=public_stage_name,
|
|
1100
|
+
mode=mode,
|
|
1101
|
+
assembly=assembly,
|
|
1102
|
+
stage_payload=stage_payload,
|
|
1103
|
+
stage_model=stage_model,
|
|
1104
|
+
publish=publish,
|
|
1105
|
+
run_label=run_label,
|
|
1106
|
+
target=target,
|
|
1107
|
+
repair_patch=repair_patch,
|
|
1108
|
+
tool_name=tool_name,
|
|
1109
|
+
projection_name=projection_name,
|
|
1110
|
+
)
|
|
1111
|
+
|
|
1112
|
+
if mode in {"preflight", "plan"}:
|
|
1113
|
+
return runner()
|
|
1114
|
+
return self._run(profile, lambda _session_profile, _context: runner())
|
|
1115
|
+
|
|
1116
|
+
def _execute_stage_build(
|
|
1117
|
+
self,
|
|
1118
|
+
*,
|
|
1119
|
+
profile: str,
|
|
1120
|
+
build_id: str,
|
|
1121
|
+
stage_name: str,
|
|
1122
|
+
public_stage_name: str,
|
|
1123
|
+
mode: str,
|
|
1124
|
+
assembly: BuildAssemblyStore,
|
|
1125
|
+
stage_payload: dict[str, Any],
|
|
1126
|
+
stage_model: type[BaseModel],
|
|
1127
|
+
publish: bool,
|
|
1128
|
+
run_label: str | None,
|
|
1129
|
+
target: dict[str, Any],
|
|
1130
|
+
repair_patch: dict[str, Any],
|
|
1131
|
+
tool_name: str,
|
|
1132
|
+
projection_name: str,
|
|
1133
|
+
) -> dict[str, Any]:
|
|
1134
|
+
try:
|
|
1135
|
+
resolved_stage_payload = self._resolve_stage_payload(
|
|
1136
|
+
assembly=assembly,
|
|
1137
|
+
stage_name=stage_name,
|
|
1138
|
+
mode=mode,
|
|
1139
|
+
incoming_stage_payload=stage_payload,
|
|
1140
|
+
repair_patch=repair_patch,
|
|
1141
|
+
stage_model=stage_model,
|
|
1142
|
+
projection_name=projection_name,
|
|
1143
|
+
)
|
|
1144
|
+
missing_prerequisites = _missing_stage_prerequisites(
|
|
1145
|
+
stage_name=public_stage_name,
|
|
1146
|
+
assembly=assembly,
|
|
1147
|
+
stage_payload=resolved_stage_payload,
|
|
1148
|
+
)
|
|
1149
|
+
if missing_prerequisites:
|
|
1150
|
+
raise ValueError("; ".join(missing_prerequisites))
|
|
1151
|
+
merged_manifest = _merge_stage_manifest(
|
|
1152
|
+
stage_name=stage_name,
|
|
1153
|
+
manifest=assembly.get_manifest(),
|
|
1154
|
+
stage_payload=resolved_stage_payload,
|
|
1155
|
+
)
|
|
1156
|
+
parsed_manifest = BuildManifest.model_validate(merged_manifest)
|
|
1157
|
+
normalized_manifest = normalize_solution_spec(parsed_manifest)
|
|
1158
|
+
compiled = compile_solution(normalized_manifest)
|
|
1159
|
+
existing_app_targets = _resolve_target_app_keys(
|
|
1160
|
+
target=target,
|
|
1161
|
+
entity_ids=[entity.entity_id for entity in compiled.entities],
|
|
1162
|
+
)
|
|
1163
|
+
update_only = _target_is_update_only(target) or bool(existing_app_targets)
|
|
1164
|
+
if update_only and not existing_app_targets:
|
|
1165
|
+
raise ValueError("update target requires app_key or target.app_keys")
|
|
1166
|
+
package_tag_id = _resolve_target_package_tag_id(target)
|
|
1167
|
+
if package_tag_id is not None:
|
|
1168
|
+
compiled = _bind_existing_package_target(compiled, package_tag_id)
|
|
1169
|
+
elif update_only:
|
|
1170
|
+
compiled = _bind_existing_app_target(compiled)
|
|
1171
|
+
filtered_compiled = _filter_compiled_solution(
|
|
1172
|
+
compiled,
|
|
1173
|
+
stage_name=stage_name,
|
|
1174
|
+
stage_payload=resolved_stage_payload,
|
|
1175
|
+
stage_variant=public_stage_name,
|
|
1176
|
+
)
|
|
1177
|
+
assembly.set_stage_spec(stage_name, resolved_stage_payload)
|
|
1178
|
+
assembly.set_manifest(normalized_manifest.model_dump(mode="json"))
|
|
1179
|
+
if existing_app_targets:
|
|
1180
|
+
_seed_existing_app_artifacts(assembly=assembly, existing_app_targets=existing_app_targets)
|
|
1181
|
+
|
|
1182
|
+
if mode in {"preflight", "plan"}:
|
|
1183
|
+
status = "preflighted" if mode == "preflight" else "planned"
|
|
1184
|
+
assembly.add_stage_history(
|
|
1185
|
+
{
|
|
1186
|
+
"stage": stage_name,
|
|
1187
|
+
"mode": mode,
|
|
1188
|
+
"status": status,
|
|
1189
|
+
"execution_step_count": len(filtered_compiled.execution_plan.steps),
|
|
1190
|
+
}
|
|
1191
|
+
)
|
|
1192
|
+
assembly.mark_status("draft")
|
|
1193
|
+
return self._stage_success_response(
|
|
1194
|
+
build_id=build_id,
|
|
1195
|
+
stage_name=public_stage_name,
|
|
1196
|
+
mode=mode,
|
|
1197
|
+
normalized_manifest=normalized_manifest,
|
|
1198
|
+
compiled=filtered_compiled,
|
|
1199
|
+
artifacts=assembly.get_artifacts(),
|
|
1200
|
+
step_results={},
|
|
1201
|
+
errors=[],
|
|
1202
|
+
run_path=None,
|
|
1203
|
+
build_path=str(assembly.path),
|
|
1204
|
+
status=status,
|
|
1205
|
+
build_summary=_build_summary(assembly),
|
|
1206
|
+
tool_name=tool_name,
|
|
1207
|
+
)
|
|
1208
|
+
|
|
1209
|
+
request_fingerprint = fingerprint_payload(
|
|
1210
|
+
{
|
|
1211
|
+
"stage": stage_name,
|
|
1212
|
+
"mode": "apply" if mode == "repair" else mode,
|
|
1213
|
+
"publish": publish,
|
|
1214
|
+
"build_manifest": normalized_manifest.model_dump(mode="json"),
|
|
1215
|
+
"stage_payload": resolved_stage_payload,
|
|
1216
|
+
"target": target,
|
|
1217
|
+
}
|
|
1218
|
+
)
|
|
1219
|
+
store = RunArtifactStore.open(
|
|
1220
|
+
idempotency_key=_stage_run_id(build_id, stage_name),
|
|
1221
|
+
normalized_solution_spec=normalized_manifest.model_dump(mode="json"),
|
|
1222
|
+
request_fingerprint=request_fingerprint,
|
|
1223
|
+
run_label=run_label,
|
|
1224
|
+
initial_artifacts=assembly.get_artifacts(),
|
|
1225
|
+
)
|
|
1226
|
+
if mode == "apply":
|
|
1227
|
+
store.ensure_apply_fingerprint(request_fingerprint)
|
|
1228
|
+
if package_tag_id is not None:
|
|
1229
|
+
store.set_artifact("package", "tag_id", package_tag_id)
|
|
1230
|
+
store.set_artifact("package", "reused", True)
|
|
1231
|
+
result = self._build_executor().execute(
|
|
1232
|
+
profile=profile,
|
|
1233
|
+
compiled=filtered_compiled,
|
|
1234
|
+
store=store,
|
|
1235
|
+
publish=publish,
|
|
1236
|
+
mode=mode,
|
|
1237
|
+
)
|
|
1238
|
+
assembly.set_artifacts(result["artifacts"])
|
|
1239
|
+
stage_verification = None
|
|
1240
|
+
effective_status = result["status"]
|
|
1241
|
+
if result["status"] == "success":
|
|
1242
|
+
stage_verification = self._verify_stage_write_result(
|
|
1243
|
+
profile=profile,
|
|
1244
|
+
build_id=build_id,
|
|
1245
|
+
public_stage_name=public_stage_name,
|
|
1246
|
+
artifacts=result["artifacts"],
|
|
1247
|
+
solution_spec=normalized_manifest.model_dump(mode="json"),
|
|
1248
|
+
target=target,
|
|
1249
|
+
)
|
|
1250
|
+
if isinstance(stage_verification, dict) and stage_verification.get("status") == "partial":
|
|
1251
|
+
effective_status = "partial_success"
|
|
1252
|
+
failure_signature = None
|
|
1253
|
+
stage_failure_count = 0
|
|
1254
|
+
next_strategy = None
|
|
1255
|
+
suggested_next_call_override = None
|
|
1256
|
+
if effective_status == "failed":
|
|
1257
|
+
failure_signature, stage_failure_count = self._record_failure_signature(
|
|
1258
|
+
assembly=assembly,
|
|
1259
|
+
stage_name=public_stage_name,
|
|
1260
|
+
stage_payload=resolved_stage_payload,
|
|
1261
|
+
errors=result.get("errors", []),
|
|
1262
|
+
)
|
|
1263
|
+
if failure_signature and stage_failure_count >= 2 and public_stage_name == "app":
|
|
1264
|
+
next_strategy = "switch_to_explicit_app_stage"
|
|
1265
|
+
suggested_next_call_override = {
|
|
1266
|
+
"tool_name": "solution_build_app",
|
|
1267
|
+
"arguments": {"mode": "plan", "build_id": build_id},
|
|
1268
|
+
}
|
|
1269
|
+
assembly.add_stage_history(
|
|
1270
|
+
{
|
|
1271
|
+
"stage": stage_name,
|
|
1272
|
+
"mode": mode,
|
|
1273
|
+
"status": effective_status,
|
|
1274
|
+
"run_path": result["run_path"],
|
|
1275
|
+
"execution_step_count": len(filtered_compiled.execution_plan.steps),
|
|
1276
|
+
}
|
|
1277
|
+
)
|
|
1278
|
+
assembly.mark_status(_build_status_for_stage(stage_name, effective_status))
|
|
1279
|
+
response = self._stage_success_response(
|
|
1280
|
+
build_id=build_id,
|
|
1281
|
+
stage_name=public_stage_name,
|
|
1282
|
+
mode=mode,
|
|
1283
|
+
normalized_manifest=normalized_manifest,
|
|
1284
|
+
compiled=filtered_compiled,
|
|
1285
|
+
artifacts=result["artifacts"],
|
|
1286
|
+
step_results=result["step_results"],
|
|
1287
|
+
errors=result["errors"],
|
|
1288
|
+
run_path=result["run_path"],
|
|
1289
|
+
build_path=str(assembly.path),
|
|
1290
|
+
status=effective_status,
|
|
1291
|
+
build_summary=_build_summary(assembly),
|
|
1292
|
+
tool_name=tool_name,
|
|
1293
|
+
verification=stage_verification,
|
|
1294
|
+
)
|
|
1295
|
+
if effective_status == "failed":
|
|
1296
|
+
response["failure_signature"] = failure_signature
|
|
1297
|
+
response["stage_failure_count"] = stage_failure_count or 1
|
|
1298
|
+
if next_strategy:
|
|
1299
|
+
response["next_strategy"] = next_strategy
|
|
1300
|
+
if suggested_next_call_override is not None:
|
|
1301
|
+
response["suggested_next_call"] = suggested_next_call_override
|
|
1302
|
+
return response
|
|
1303
|
+
except ValidationError as exc:
|
|
1304
|
+
return self._stage_failure_response(build_id, public_stage_name, mode, "validation", exc.errors(), assembly, tool_name=tool_name)
|
|
1305
|
+
except ValueError as exc:
|
|
1306
|
+
return self._stage_failure_response(build_id, public_stage_name, mode, "config", str(exc), assembly, tool_name=tool_name)
|
|
1307
|
+
except Exception as exc: # noqa: BLE001
|
|
1308
|
+
return self._stage_failure_response(build_id, public_stage_name, mode, "runtime", str(exc), assembly, tool_name=tool_name)
|
|
1309
|
+
|
|
1310
|
+
def _resolve_stage_payload(
|
|
1311
|
+
self,
|
|
1312
|
+
*,
|
|
1313
|
+
assembly: BuildAssemblyStore,
|
|
1314
|
+
stage_name: str,
|
|
1315
|
+
mode: str,
|
|
1316
|
+
incoming_stage_payload: dict[str, Any],
|
|
1317
|
+
repair_patch: dict[str, Any],
|
|
1318
|
+
stage_model: type[BaseModel],
|
|
1319
|
+
projection_name: str,
|
|
1320
|
+
) -> dict[str, Any]:
|
|
1321
|
+
existing_stage_payload = (assembly.data.get("stage_specs", {}) or {}).get(stage_name, {}) or {}
|
|
1322
|
+
base_payload: dict[str, Any] = {}
|
|
1323
|
+
if mode == "repair":
|
|
1324
|
+
base_payload = deepcopy(existing_stage_payload)
|
|
1325
|
+
if incoming_stage_payload:
|
|
1326
|
+
base_payload = deep_merge(base_payload, incoming_stage_payload)
|
|
1327
|
+
if repair_patch:
|
|
1328
|
+
base_payload = deep_merge(base_payload, repair_patch)
|
|
1329
|
+
if not base_payload:
|
|
1330
|
+
raise ValueError(f"{stage_name} stage does not have a stored spec to repair")
|
|
1331
|
+
else:
|
|
1332
|
+
if incoming_stage_payload:
|
|
1333
|
+
base_payload = deepcopy(incoming_stage_payload)
|
|
1334
|
+
elif existing_stage_payload:
|
|
1335
|
+
base_payload = deepcopy(existing_stage_payload)
|
|
1336
|
+
else:
|
|
1337
|
+
raise ValueError(f"{stage_name} stage spec is required")
|
|
1338
|
+
parsed: BaseModel | None = None
|
|
1339
|
+
try:
|
|
1340
|
+
parsed = stage_model.model_validate(base_payload)
|
|
1341
|
+
except ValidationError as original_error:
|
|
1342
|
+
if _looks_like_full_solution_spec(base_payload):
|
|
1343
|
+
parsed = stage_model.model_validate(_project_stage_payload(stage_name=projection_name, solution_spec=base_payload))
|
|
1344
|
+
else:
|
|
1345
|
+
raise original_error
|
|
1346
|
+
resolved = parsed.model_dump(mode="json", exclude_unset=True)
|
|
1347
|
+
if not resolved:
|
|
1348
|
+
raise ValueError(f"{stage_name} stage spec is required")
|
|
1349
|
+
return resolved
|
|
1350
|
+
|
|
1351
|
+
def _resolve_full_solution_spec(
|
|
1352
|
+
self,
|
|
1353
|
+
*,
|
|
1354
|
+
solution_spec: dict[str, Any],
|
|
1355
|
+
mode: str,
|
|
1356
|
+
repair_patch: dict[str, Any],
|
|
1357
|
+
) -> dict[str, Any]:
|
|
1358
|
+
base_payload = deepcopy(solution_spec)
|
|
1359
|
+
if mode == "repair" and repair_patch:
|
|
1360
|
+
base_payload = deep_merge(base_payload, repair_patch)
|
|
1361
|
+
parsed = SolutionSpec.model_validate(base_payload)
|
|
1362
|
+
return parsed.model_dump(mode="json", exclude_unset=True)
|
|
1363
|
+
|
|
1364
|
+
def _build_executor(self) -> SolutionExecutor:
|
|
1365
|
+
return SolutionExecutor(
|
|
1366
|
+
workspace_tools=WorkspaceTools(self.sessions, self.backend),
|
|
1367
|
+
package_tools=PackageTools(self.sessions, self.backend),
|
|
1368
|
+
role_tools=RoleTools(self.sessions, self.backend),
|
|
1369
|
+
app_tools=AppTools(self.sessions, self.backend),
|
|
1370
|
+
record_tools=RecordTools(self.sessions, self.backend),
|
|
1371
|
+
workflow_tools=WorkflowTools(self.sessions, self.backend),
|
|
1372
|
+
view_tools=ViewTools(self.sessions, self.backend),
|
|
1373
|
+
chart_tools=QingbiReportTools(self.sessions, self.backend),
|
|
1374
|
+
portal_tools=PortalTools(self.sessions, self.backend),
|
|
1375
|
+
navigation_tools=NavigationTools(self.sessions, self.backend),
|
|
1376
|
+
)
|
|
1377
|
+
|
|
1378
|
+
def _verify_solution_build(
|
|
1379
|
+
self,
|
|
1380
|
+
*,
|
|
1381
|
+
profile: str,
|
|
1382
|
+
build_id: str,
|
|
1383
|
+
artifacts: dict[str, Any],
|
|
1384
|
+
solution_spec: dict[str, Any],
|
|
1385
|
+
) -> dict[str, Any]:
|
|
1386
|
+
verification = {
|
|
1387
|
+
"build_id": build_id,
|
|
1388
|
+
"status": "success",
|
|
1389
|
+
"errors": [],
|
|
1390
|
+
"package": {},
|
|
1391
|
+
"apps": [],
|
|
1392
|
+
"portal": {},
|
|
1393
|
+
"navigation": {},
|
|
1394
|
+
}
|
|
1395
|
+
package_tools = PackageTools(self.sessions, self.backend)
|
|
1396
|
+
app_tools = AppTools(self.sessions, self.backend)
|
|
1397
|
+
portal_tools = PortalTools(self.sessions, self.backend)
|
|
1398
|
+
navigation_tools = NavigationTools(self.sessions, self.backend)
|
|
1399
|
+
record_tools = RecordTools(self.sessions, self.backend)
|
|
1400
|
+
entity_specs = {
|
|
1401
|
+
entity.get("entity_id"): entity
|
|
1402
|
+
for entity in solution_spec.get("entities", [])
|
|
1403
|
+
if isinstance(entity, dict) and isinstance(entity.get("entity_id"), str)
|
|
1404
|
+
}
|
|
1405
|
+
field_maps_artifact = artifacts.get("field_maps", {}) if isinstance(artifacts.get("field_maps"), dict) else {}
|
|
1406
|
+
|
|
1407
|
+
try:
|
|
1408
|
+
package_artifact = artifacts.get("package", {}) if isinstance(artifacts.get("package"), dict) else {}
|
|
1409
|
+
tag_id = package_artifact.get("tag_id")
|
|
1410
|
+
if isinstance(tag_id, int) and tag_id > 0:
|
|
1411
|
+
package_detail = package_tools.package_get(profile=profile, tag_id=tag_id, include_raw=True).get("result") or {}
|
|
1412
|
+
verification["package"] = {
|
|
1413
|
+
"tag_id": tag_id,
|
|
1414
|
+
"name": package_detail.get("tagName"),
|
|
1415
|
+
"item_count": len(package_detail.get("tagItems") or []),
|
|
1416
|
+
}
|
|
1417
|
+
except Exception as exc: # noqa: BLE001
|
|
1418
|
+
_append_verification_error(verification, "package", exc)
|
|
1419
|
+
|
|
1420
|
+
apps_artifact = artifacts.get("apps", {}) if isinstance(artifacts.get("apps"), dict) else {}
|
|
1421
|
+
records_artifact = artifacts.get("records", {}) if isinstance(artifacts.get("records"), dict) else {}
|
|
1422
|
+
for entity_id, app_info in apps_artifact.items():
|
|
1423
|
+
try:
|
|
1424
|
+
if not isinstance(app_info, dict):
|
|
1425
|
+
continue
|
|
1426
|
+
app_key = app_info.get("app_key")
|
|
1427
|
+
if not isinstance(app_key, str) or not app_key:
|
|
1428
|
+
continue
|
|
1429
|
+
entity_spec = entity_specs.get(entity_id)
|
|
1430
|
+
field_meta = field_maps_artifact.get(entity_id, {}) if isinstance(field_maps_artifact.get(entity_id), dict) else {}
|
|
1431
|
+
base_info = app_tools.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1432
|
+
record_page = _readback_record_page(
|
|
1433
|
+
record_tools=record_tools,
|
|
1434
|
+
profile=profile,
|
|
1435
|
+
app_key=app_key,
|
|
1436
|
+
entity_spec=entity_spec,
|
|
1437
|
+
field_meta=field_meta,
|
|
1438
|
+
)
|
|
1439
|
+
rows = record_page.get("list") or []
|
|
1440
|
+
returned_rows = _coerce_count(record_page.get("returned_rows"))
|
|
1441
|
+
if returned_rows is None:
|
|
1442
|
+
returned_rows = len(rows) if isinstance(rows, list) else 0
|
|
1443
|
+
backend_reported_total = _coerce_count(record_page.get("backend_reported_total"))
|
|
1444
|
+
effective_count = _coerce_count(record_page.get("effective_count"))
|
|
1445
|
+
if effective_count is None:
|
|
1446
|
+
effective_count = max(backend_reported_total or 0, returned_rows)
|
|
1447
|
+
seeded_records = records_artifact.get(entity_id, {}) if isinstance(records_artifact.get(entity_id), dict) else {}
|
|
1448
|
+
app_verification = {
|
|
1449
|
+
"entity_id": entity_id,
|
|
1450
|
+
"app_key": app_key,
|
|
1451
|
+
"app_name": base_info.get("formTitle") or app_info.get("display_name") or app_info.get("name"),
|
|
1452
|
+
"app_icon": base_info.get("appIcon"),
|
|
1453
|
+
"returned_rows": returned_rows,
|
|
1454
|
+
"effective_count": effective_count,
|
|
1455
|
+
"list_type": _coerce_count(record_page.get("list_type")),
|
|
1456
|
+
"list_type_label": record_page.get("list_type_label"),
|
|
1457
|
+
"seeded_records": len(seeded_records),
|
|
1458
|
+
"sample_titles": _extract_record_titles(rows, entity_spec=entity_spec, field_meta=field_meta),
|
|
1459
|
+
}
|
|
1460
|
+
if backend_reported_total is not None:
|
|
1461
|
+
app_verification["backend_reported_total"] = backend_reported_total
|
|
1462
|
+
verification["apps"].append(app_verification)
|
|
1463
|
+
except Exception as exc: # noqa: BLE001
|
|
1464
|
+
_append_verification_error(verification, f"app:{entity_id}", exc)
|
|
1465
|
+
|
|
1466
|
+
try:
|
|
1467
|
+
portal_artifact = artifacts.get("portal", {}) if isinstance(artifacts.get("portal"), dict) else {}
|
|
1468
|
+
dash_key = portal_artifact.get("dash_key")
|
|
1469
|
+
if isinstance(dash_key, str) and dash_key:
|
|
1470
|
+
published = portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=False).get("result") or {}
|
|
1471
|
+
draft = portal_tools.portal_get(profile=profile, dash_key=dash_key, being_draft=True).get("result") or {}
|
|
1472
|
+
verification["portal"] = {
|
|
1473
|
+
"dash_key": dash_key,
|
|
1474
|
+
"name": published.get("dashName") or draft.get("dashName") or published.get("name") or draft.get("name"),
|
|
1475
|
+
"published_component_count": _count_portal_components(published),
|
|
1476
|
+
"draft_component_count": _count_portal_components(draft),
|
|
1477
|
+
}
|
|
1478
|
+
except Exception as exc: # noqa: BLE001
|
|
1479
|
+
_append_verification_error(verification, "portal", exc)
|
|
1480
|
+
|
|
1481
|
+
try:
|
|
1482
|
+
navigation_artifact = artifacts.get("navigation", {}) if isinstance(artifacts.get("navigation"), dict) else {}
|
|
1483
|
+
status = navigation_tools.navigation_get_status(profile=profile).get("result") or {}
|
|
1484
|
+
items = navigation_artifact.get("items") or []
|
|
1485
|
+
verification["navigation"] = {
|
|
1486
|
+
"navigation_id": status.get("navigationId") or status.get("id"),
|
|
1487
|
+
"publish_status": status.get("publishStatus"),
|
|
1488
|
+
"item_count": len(items) if isinstance(items, list) else 0,
|
|
1489
|
+
"items": _extract_navigation_items(items),
|
|
1490
|
+
}
|
|
1491
|
+
except Exception as exc: # noqa: BLE001
|
|
1492
|
+
_append_verification_error(verification, "navigation", exc)
|
|
1493
|
+
|
|
1494
|
+
if verification["errors"]:
|
|
1495
|
+
verification["status"] = "partial"
|
|
1496
|
+
return verification
|
|
1497
|
+
|
|
1498
|
+
def _verify_stage_write_result(
|
|
1499
|
+
self,
|
|
1500
|
+
*,
|
|
1501
|
+
profile: str,
|
|
1502
|
+
build_id: str,
|
|
1503
|
+
public_stage_name: str,
|
|
1504
|
+
artifacts: dict[str, Any],
|
|
1505
|
+
solution_spec: dict[str, Any],
|
|
1506
|
+
target: dict[str, Any],
|
|
1507
|
+
) -> dict[str, Any] | None:
|
|
1508
|
+
if public_stage_name != "app":
|
|
1509
|
+
return None
|
|
1510
|
+
app_tools = AppTools(self.sessions, self.backend)
|
|
1511
|
+
expected_package_tag_id = _resolve_target_package_tag_id(target)
|
|
1512
|
+
verification: dict[str, Any] = {
|
|
1513
|
+
"stage": public_stage_name,
|
|
1514
|
+
"status": "success",
|
|
1515
|
+
"expected_package_tag_id": expected_package_tag_id,
|
|
1516
|
+
"package_attached": None if expected_package_tag_id is None else True,
|
|
1517
|
+
"apps": [],
|
|
1518
|
+
"views_created": [],
|
|
1519
|
+
"views_strategy": "not_included",
|
|
1520
|
+
"errors": [],
|
|
1521
|
+
}
|
|
1522
|
+
apps_artifact = artifacts.get("apps", {}) if isinstance(artifacts.get("apps"), dict) else {}
|
|
1523
|
+
views_artifact = artifacts.get("views", {}) if isinstance(artifacts.get("views"), dict) else {}
|
|
1524
|
+
entity_specs = {
|
|
1525
|
+
entity.get("entity_id"): entity
|
|
1526
|
+
for entity in solution_spec.get("entities", [])
|
|
1527
|
+
if isinstance(entity, dict) and isinstance(entity.get("entity_id"), str)
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
for entity_id, app_info in apps_artifact.items():
|
|
1531
|
+
if not isinstance(app_info, dict):
|
|
1532
|
+
continue
|
|
1533
|
+
app_key = app_info.get("app_key")
|
|
1534
|
+
if not isinstance(app_key, str) or not app_key:
|
|
1535
|
+
continue
|
|
1536
|
+
try:
|
|
1537
|
+
base_info = app_tools.app_get_base(profile=profile, app_key=app_key, include_raw=True).get("result") or {}
|
|
1538
|
+
raw_tag_ids = base_info.get("tagIds") if isinstance(base_info.get("tagIds"), list) else []
|
|
1539
|
+
tag_ids_after = [tag_id for tag_id in (_coerce_count(value) for value in raw_tag_ids) if tag_id is not None]
|
|
1540
|
+
package_attached = None if expected_package_tag_id is None else expected_package_tag_id in tag_ids_after
|
|
1541
|
+
if package_attached is False:
|
|
1542
|
+
verification["status"] = "partial"
|
|
1543
|
+
verification["package_attached"] = False
|
|
1544
|
+
verification["errors"].append(
|
|
1545
|
+
{
|
|
1546
|
+
"category": "verification",
|
|
1547
|
+
"detail": f"app '{app_key}' is not attached to expected package '{expected_package_tag_id}'",
|
|
1548
|
+
"entity_id": entity_id,
|
|
1549
|
+
"app_key": app_key,
|
|
1550
|
+
"tag_ids_after": tag_ids_after,
|
|
1551
|
+
}
|
|
1552
|
+
)
|
|
1553
|
+
entity_views = views_artifact.get(entity_id) if isinstance(views_artifact.get(entity_id), dict) else {}
|
|
1554
|
+
created_view_keys = sorted(
|
|
1555
|
+
{
|
|
1556
|
+
str(view_result.get("view_key"))
|
|
1557
|
+
for view_result in entity_views.values()
|
|
1558
|
+
if isinstance(view_result, dict) and isinstance(view_result.get("view_key"), str) and view_result.get("view_key")
|
|
1559
|
+
}
|
|
1560
|
+
)
|
|
1561
|
+
verification["views_created"].extend(created_view_keys)
|
|
1562
|
+
verification["apps"].append(
|
|
1563
|
+
{
|
|
1564
|
+
"entity_id": entity_id,
|
|
1565
|
+
"display_name": entity_specs.get(entity_id, {}).get("display_name"),
|
|
1566
|
+
"app_key": app_key,
|
|
1567
|
+
"tag_ids_after": tag_ids_after,
|
|
1568
|
+
"package_attached": package_attached,
|
|
1569
|
+
"views_created": created_view_keys,
|
|
1570
|
+
"publish_status": base_info.get("appPublishStatus"),
|
|
1571
|
+
}
|
|
1572
|
+
)
|
|
1573
|
+
except Exception as exc: # noqa: BLE001
|
|
1574
|
+
verification["status"] = "partial"
|
|
1575
|
+
verification["errors"].append(
|
|
1576
|
+
{
|
|
1577
|
+
"category": "verification",
|
|
1578
|
+
"detail": str(exc),
|
|
1579
|
+
"entity_id": entity_id,
|
|
1580
|
+
"app_key": app_key,
|
|
1581
|
+
}
|
|
1582
|
+
)
|
|
1583
|
+
if verification["views_created"]:
|
|
1584
|
+
verification["views_strategy"] = "created"
|
|
1585
|
+
else:
|
|
1586
|
+
verification["suggested_next_call"] = {
|
|
1587
|
+
"tool_name": "solution_build_views",
|
|
1588
|
+
"arguments": {"mode": "plan", "build_id": build_id},
|
|
1589
|
+
}
|
|
1590
|
+
return verification
|
|
1591
|
+
|
|
1592
|
+
def _failure_response(self, mode: str, idempotency_key: str, category: str, detail: Any) -> dict[str, Any]:
|
|
1593
|
+
error_fields = _solution_error_fields(
|
|
1594
|
+
category=category,
|
|
1595
|
+
detail=detail,
|
|
1596
|
+
suggested_next_call=None,
|
|
1597
|
+
stage="all",
|
|
1598
|
+
)
|
|
1599
|
+
return {
|
|
1600
|
+
"mode": mode,
|
|
1601
|
+
"idempotency_key": idempotency_key,
|
|
1602
|
+
"normalized_solution_spec": None,
|
|
1603
|
+
"execution_plan": {"steps": []},
|
|
1604
|
+
"artifacts": {},
|
|
1605
|
+
"step_results": {},
|
|
1606
|
+
"publish_results": {},
|
|
1607
|
+
"status": "failed",
|
|
1608
|
+
"errors": _sanitize_errors([{"category": category, "detail": detail}]),
|
|
1609
|
+
"run_path": None,
|
|
1610
|
+
"verification": None,
|
|
1611
|
+
**error_fields,
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1614
|
+
def _stage_failure_response(
|
|
1615
|
+
self,
|
|
1616
|
+
build_id: str,
|
|
1617
|
+
stage_name: str,
|
|
1618
|
+
mode: str,
|
|
1619
|
+
category: str,
|
|
1620
|
+
detail: Any,
|
|
1621
|
+
assembly: BuildAssemblyStore | None,
|
|
1622
|
+
*,
|
|
1623
|
+
tool_name: str,
|
|
1624
|
+
) -> dict[str, Any]:
|
|
1625
|
+
error_fields = _solution_error_fields(
|
|
1626
|
+
category=category,
|
|
1627
|
+
detail=detail,
|
|
1628
|
+
suggested_next_call=_solution_build_next_call(
|
|
1629
|
+
tool_name=tool_name,
|
|
1630
|
+
mode=mode,
|
|
1631
|
+
build_id=build_id or None,
|
|
1632
|
+
),
|
|
1633
|
+
stage=stage_name,
|
|
1634
|
+
)
|
|
1635
|
+
if build_id and stage_name == "app":
|
|
1636
|
+
error_fields["suggested_schema_call"] = None
|
|
1637
|
+
return {
|
|
1638
|
+
"build_id": build_id,
|
|
1639
|
+
"stage": stage_name,
|
|
1640
|
+
"mode": mode,
|
|
1641
|
+
"normalized_build_manifest": None,
|
|
1642
|
+
"execution_plan": {"steps": []},
|
|
1643
|
+
"artifacts": assembly.get_artifacts() if assembly is not None else {},
|
|
1644
|
+
"step_results": {},
|
|
1645
|
+
"publish_results": {},
|
|
1646
|
+
"status": "failed",
|
|
1647
|
+
"errors": _sanitize_errors([{"category": category, "detail": detail}]),
|
|
1648
|
+
"build_path": str(assembly.path) if assembly is not None else None,
|
|
1649
|
+
"run_path": None,
|
|
1650
|
+
"build_summary": _build_summary(assembly) if assembly is not None else None,
|
|
1651
|
+
**error_fields,
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
def _stage_skip_response(
|
|
1655
|
+
self,
|
|
1656
|
+
*,
|
|
1657
|
+
build_id: str,
|
|
1658
|
+
stage_name: str,
|
|
1659
|
+
mode: str,
|
|
1660
|
+
assembly: BuildAssemblyStore,
|
|
1661
|
+
tool_name: str,
|
|
1662
|
+
) -> dict[str, Any]:
|
|
1663
|
+
return {
|
|
1664
|
+
"build_id": build_id,
|
|
1665
|
+
"stage": stage_name,
|
|
1666
|
+
"mode": mode,
|
|
1667
|
+
"normalized_build_manifest": assembly.get_manifest(),
|
|
1668
|
+
"execution_plan": {"steps": []},
|
|
1669
|
+
"artifacts": assembly.get_artifacts(),
|
|
1670
|
+
"step_results": {},
|
|
1671
|
+
"publish_results": {},
|
|
1672
|
+
"status": "skipped",
|
|
1673
|
+
"errors": [],
|
|
1674
|
+
"build_path": str(assembly.path),
|
|
1675
|
+
"run_path": None,
|
|
1676
|
+
"build_summary": _build_summary(assembly),
|
|
1677
|
+
"suggested_next_call": _solution_build_next_call(
|
|
1678
|
+
tool_name=tool_name,
|
|
1679
|
+
mode=mode,
|
|
1680
|
+
build_id=build_id,
|
|
1681
|
+
),
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1684
|
+
def _stage_success_response(
|
|
1685
|
+
self,
|
|
1686
|
+
*,
|
|
1687
|
+
build_id: str,
|
|
1688
|
+
stage_name: str,
|
|
1689
|
+
mode: str,
|
|
1690
|
+
normalized_manifest: SolutionSpec,
|
|
1691
|
+
compiled: CompiledSolution,
|
|
1692
|
+
artifacts: dict[str, Any],
|
|
1693
|
+
step_results: dict[str, Any],
|
|
1694
|
+
errors: list[dict[str, Any]],
|
|
1695
|
+
run_path: str | None,
|
|
1696
|
+
build_path: str,
|
|
1697
|
+
status: str,
|
|
1698
|
+
build_summary: dict[str, Any],
|
|
1699
|
+
tool_name: str,
|
|
1700
|
+
verification: dict[str, Any] | None = None,
|
|
1701
|
+
) -> dict[str, Any]:
|
|
1702
|
+
response = {
|
|
1703
|
+
"build_id": build_id,
|
|
1704
|
+
"stage": stage_name,
|
|
1705
|
+
"mode": mode,
|
|
1706
|
+
"normalized_build_manifest": normalized_manifest.model_dump(mode="json"),
|
|
1707
|
+
"execution_plan": compiled.execution_plan.as_dict(),
|
|
1708
|
+
"artifacts": artifacts,
|
|
1709
|
+
"step_results": step_results,
|
|
1710
|
+
"publish_results": _extract_publish_results(step_results),
|
|
1711
|
+
"status": status,
|
|
1712
|
+
"errors": _sanitize_errors(errors),
|
|
1713
|
+
"build_path": build_path,
|
|
1714
|
+
"run_path": run_path,
|
|
1715
|
+
"verification": verification,
|
|
1716
|
+
"build_summary": build_summary,
|
|
1717
|
+
"suggested_next_call": _solution_build_next_call(
|
|
1718
|
+
tool_name=tool_name,
|
|
1719
|
+
mode=mode,
|
|
1720
|
+
build_id=build_id,
|
|
1721
|
+
),
|
|
1722
|
+
}
|
|
1723
|
+
if isinstance(verification, dict):
|
|
1724
|
+
for key in ("package_attached", "views_created", "views_strategy"):
|
|
1725
|
+
if key in verification:
|
|
1726
|
+
response[key] = verification.get(key)
|
|
1727
|
+
return response
|
|
1728
|
+
|
|
1729
|
+
def _design_failure_response(self, session_id: str, action: str, category: str, detail: Any) -> dict[str, Any]:
|
|
1730
|
+
error_fields = _solution_error_fields(
|
|
1731
|
+
category=category,
|
|
1732
|
+
detail=detail,
|
|
1733
|
+
suggested_next_call=_design_session_next_call(action=action, session_id=session_id or None),
|
|
1734
|
+
)
|
|
1735
|
+
return {
|
|
1736
|
+
"session_id": session_id,
|
|
1737
|
+
"status": "failed",
|
|
1738
|
+
"current_stage": None,
|
|
1739
|
+
"next_stage": None,
|
|
1740
|
+
"metadata": {},
|
|
1741
|
+
"stage_results": {},
|
|
1742
|
+
"merged_design_spec": {},
|
|
1743
|
+
"normalized_solution_spec": None,
|
|
1744
|
+
"execution_plan": None,
|
|
1745
|
+
"errors": _sanitize_errors([{"category": category, "detail": detail}]),
|
|
1746
|
+
"session_path": None,
|
|
1747
|
+
**error_fields,
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
def _design_summary(self, store: DesignSessionStore) -> dict[str, Any]:
|
|
1751
|
+
summary = store.summary()
|
|
1752
|
+
current_stage = summary["current_stage"]
|
|
1753
|
+
summary["next_stage"] = None if summary["status"] == "finalized" else current_stage
|
|
1754
|
+
summary["errors"] = []
|
|
1755
|
+
summary["suggested_next_call"] = _design_summary_next_call(summary)
|
|
1756
|
+
return summary
|
|
1757
|
+
|
|
1758
|
+
def _all_failure_response(self, build_id: str, mode: str, category: str, detail: Any) -> dict[str, Any]:
|
|
1759
|
+
error_fields = _solution_error_fields(
|
|
1760
|
+
category=category,
|
|
1761
|
+
detail=detail,
|
|
1762
|
+
suggested_next_call=_solution_build_next_call(tool_name="solution_build_all", mode=mode, build_id=build_id or None),
|
|
1763
|
+
stage="all",
|
|
1764
|
+
)
|
|
1765
|
+
return {
|
|
1766
|
+
"build_id": build_id,
|
|
1767
|
+
"mode": mode,
|
|
1768
|
+
"normalized_solution_spec": None,
|
|
1769
|
+
"stage_results": {},
|
|
1770
|
+
"stage_statuses": {},
|
|
1771
|
+
"artifacts": {},
|
|
1772
|
+
"status": "failed",
|
|
1773
|
+
"errors": _sanitize_errors([{"category": category, "detail": detail}]),
|
|
1774
|
+
"verification": None,
|
|
1775
|
+
"build_summary": None,
|
|
1776
|
+
"build_path": None,
|
|
1777
|
+
**error_fields,
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
def _load_generated_app_spec(self, build_id: str) -> dict[str, Any] | None:
|
|
1781
|
+
if not build_id:
|
|
1782
|
+
return None
|
|
1783
|
+
try:
|
|
1784
|
+
assembly = BuildAssemblyStore.open(build_id=build_id, create=False)
|
|
1785
|
+
except FileNotFoundError:
|
|
1786
|
+
return None
|
|
1787
|
+
generated_specs = assembly.get_builder_value("generated_specs", {})
|
|
1788
|
+
if not isinstance(generated_specs, dict):
|
|
1789
|
+
return None
|
|
1790
|
+
app_spec = generated_specs.get("app")
|
|
1791
|
+
return deepcopy(app_spec) if isinstance(app_spec, dict) else None
|
|
1792
|
+
|
|
1793
|
+
def _persist_generated_app_spec(
|
|
1794
|
+
self,
|
|
1795
|
+
*,
|
|
1796
|
+
build_id: str,
|
|
1797
|
+
generated: GeneratedAppBuild,
|
|
1798
|
+
package_resolution: dict[str, Any],
|
|
1799
|
+
title: str,
|
|
1800
|
+
requirement_text: str,
|
|
1801
|
+
layout_style: str,
|
|
1802
|
+
) -> None:
|
|
1803
|
+
assembly = BuildAssemblyStore.open(build_id=build_id)
|
|
1804
|
+
builder_state = assembly.get_builder_state()
|
|
1805
|
+
generated_specs = builder_state.get("generated_specs", {})
|
|
1806
|
+
if not isinstance(generated_specs, dict):
|
|
1807
|
+
generated_specs = {}
|
|
1808
|
+
generated_specs["app"] = deepcopy(generated.app_spec)
|
|
1809
|
+
generated_specs["app_summary"] = deepcopy(generated.summary)
|
|
1810
|
+
generated_specs["requirements_context"] = {
|
|
1811
|
+
"title": title,
|
|
1812
|
+
"requirement_text": requirement_text,
|
|
1813
|
+
"layout_style": layout_style,
|
|
1814
|
+
"package_resolution": deepcopy(package_resolution),
|
|
1815
|
+
}
|
|
1816
|
+
builder_state["generated_specs"] = generated_specs
|
|
1817
|
+
assembly.set_builder_state(builder_state)
|
|
1818
|
+
|
|
1819
|
+
def _record_failure_signature(
|
|
1820
|
+
self,
|
|
1821
|
+
*,
|
|
1822
|
+
assembly: BuildAssemblyStore,
|
|
1823
|
+
stage_name: str,
|
|
1824
|
+
stage_payload: dict[str, Any],
|
|
1825
|
+
errors: list[dict[str, Any]],
|
|
1826
|
+
) -> tuple[str | None, int]:
|
|
1827
|
+
signature = _failure_signature_for_stage(stage_name=stage_name, stage_payload=stage_payload, errors=errors)
|
|
1828
|
+
if signature is None:
|
|
1829
|
+
return None, 0
|
|
1830
|
+
builder_state = assembly.get_builder_state()
|
|
1831
|
+
signature_counts = builder_state.get("failure_signatures", {})
|
|
1832
|
+
if not isinstance(signature_counts, dict):
|
|
1833
|
+
signature_counts = {}
|
|
1834
|
+
count = int(signature_counts.get(signature, 0)) + 1
|
|
1835
|
+
signature_counts[signature] = count
|
|
1836
|
+
builder_state["failure_signatures"] = signature_counts
|
|
1837
|
+
builder_state["last_failure_signature"] = signature
|
|
1838
|
+
builder_state["last_failure_stage"] = stage_name
|
|
1839
|
+
builder_state["last_failure_count"] = count
|
|
1840
|
+
assembly.set_builder_state(builder_state)
|
|
1841
|
+
return signature, count
|
|
1842
|
+
|
|
1843
|
+
def _resolve_builder_package_reference(
|
|
1844
|
+
self,
|
|
1845
|
+
*,
|
|
1846
|
+
profile: str,
|
|
1847
|
+
package_tag_id: int,
|
|
1848
|
+
package_name: str,
|
|
1849
|
+
) -> dict[str, Any]:
|
|
1850
|
+
packages = PackageTools(self.sessions, self.backend)
|
|
1851
|
+
normalized_name = package_name.strip()
|
|
1852
|
+
if package_tag_id > 0:
|
|
1853
|
+
result = packages.package_get(profile=profile, tag_id=package_tag_id, include_raw=False)
|
|
1854
|
+
summary = result.get("result") if isinstance(result.get("result"), dict) else {}
|
|
1855
|
+
return {
|
|
1856
|
+
"status": "resolved",
|
|
1857
|
+
"matched_via": "tag_id",
|
|
1858
|
+
"tag_id": package_tag_id,
|
|
1859
|
+
"tag_name": summary.get("tagName"),
|
|
1860
|
+
"candidates": [],
|
|
1861
|
+
}
|
|
1862
|
+
if not normalized_name:
|
|
1863
|
+
return {
|
|
1864
|
+
"status": "new_package",
|
|
1865
|
+
"matched_via": "none",
|
|
1866
|
+
"tag_id": None,
|
|
1867
|
+
"tag_name": None,
|
|
1868
|
+
"candidates": [],
|
|
1869
|
+
"source_shape": None,
|
|
1870
|
+
"retried": False,
|
|
1871
|
+
}
|
|
1872
|
+
result = packages.package_list(profile=profile, trial_status="all", include_raw=False)
|
|
1873
|
+
items = result.get("items") if isinstance(result.get("items"), list) else []
|
|
1874
|
+
source_shape = result.get("source_shape")
|
|
1875
|
+
retried = False
|
|
1876
|
+
if not items:
|
|
1877
|
+
retry_result = packages.package_list(profile=profile, trial_status="all", include_raw=False)
|
|
1878
|
+
retry_items = retry_result.get("items") if isinstance(retry_result.get("items"), list) else []
|
|
1879
|
+
if retry_items:
|
|
1880
|
+
items = retry_items
|
|
1881
|
+
source_shape = retry_result.get("source_shape")
|
|
1882
|
+
retried = True
|
|
1883
|
+
exact = [
|
|
1884
|
+
item
|
|
1885
|
+
for item in items
|
|
1886
|
+
if isinstance(item, dict) and str(item.get("tagName") or "").strip().casefold() == normalized_name.casefold()
|
|
1887
|
+
]
|
|
1888
|
+
fuzzy_candidates = [
|
|
1889
|
+
item
|
|
1890
|
+
for item in items
|
|
1891
|
+
if isinstance(item, dict) and normalized_name.casefold() in str(item.get("tagName") or "").strip().casefold()
|
|
1892
|
+
]
|
|
1893
|
+
if len(exact) == 1:
|
|
1894
|
+
match = exact[0]
|
|
1895
|
+
return {
|
|
1896
|
+
"status": "resolved",
|
|
1897
|
+
"matched_via": "exact",
|
|
1898
|
+
"tag_id": match.get("tagId"),
|
|
1899
|
+
"tag_name": match.get("tagName"),
|
|
1900
|
+
"candidates": [],
|
|
1901
|
+
"source_shape": source_shape,
|
|
1902
|
+
"retried": retried,
|
|
1903
|
+
}
|
|
1904
|
+
candidates = [
|
|
1905
|
+
{"tag_id": item.get("tagId"), "tag_name": item.get("tagName")}
|
|
1906
|
+
for item in (exact or fuzzy_candidates)[:10]
|
|
1907
|
+
if isinstance(item, dict)
|
|
1908
|
+
]
|
|
1909
|
+
detail = f"package_name '{normalized_name}' did not resolve uniquely by exact name"
|
|
1910
|
+
error_code = "AMBIGUOUS_PACKAGE"
|
|
1911
|
+
if not exact and not fuzzy_candidates:
|
|
1912
|
+
detail = f"package_name '{normalized_name}' was not found"
|
|
1913
|
+
error_code = "PACKAGE_NOT_FOUND"
|
|
1914
|
+
error_fields = _solution_error_fields(
|
|
1915
|
+
category="config",
|
|
1916
|
+
detail=detail,
|
|
1917
|
+
suggested_next_call=None,
|
|
1918
|
+
stage="app",
|
|
1919
|
+
)
|
|
1920
|
+
return {
|
|
1921
|
+
"status": "failed",
|
|
1922
|
+
"response": {
|
|
1923
|
+
"status": "failed",
|
|
1924
|
+
"mode": "plan",
|
|
1925
|
+
"stage": "app",
|
|
1926
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
1927
|
+
"error_code": error_code,
|
|
1928
|
+
"recoverable": True,
|
|
1929
|
+
"missing_required_fields": error_fields["missing_required_fields"],
|
|
1930
|
+
"unknown_fields": error_fields["unknown_fields"],
|
|
1931
|
+
"invalid_field_types": error_fields["invalid_field_types"],
|
|
1932
|
+
"package_resolution": {
|
|
1933
|
+
"status": "failed",
|
|
1934
|
+
"requested_name": normalized_name,
|
|
1935
|
+
"resolution_policy": "exact_name_only",
|
|
1936
|
+
"candidates": candidates,
|
|
1937
|
+
"source_shape": source_shape,
|
|
1938
|
+
"retried": retried,
|
|
1939
|
+
},
|
|
1940
|
+
"suggested_next_call": {
|
|
1941
|
+
"tool_name": "solution_build_app_from_requirements",
|
|
1942
|
+
"arguments": {"mode": "plan", "title": "", "requirement_text": "", "package_name": normalized_name},
|
|
1943
|
+
},
|
|
1944
|
+
},
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
def _resolve_builder_existing_app_reference(
|
|
1948
|
+
self,
|
|
1949
|
+
*,
|
|
1950
|
+
profile: str,
|
|
1951
|
+
app_key: str,
|
|
1952
|
+
package_resolution: dict[str, Any],
|
|
1953
|
+
update_only: bool,
|
|
1954
|
+
) -> dict[str, Any]:
|
|
1955
|
+
normalized_app_key = str(app_key or "").strip()
|
|
1956
|
+
if not normalized_app_key:
|
|
1957
|
+
if update_only:
|
|
1958
|
+
detail = "update target requires app_key"
|
|
1959
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
1960
|
+
error_fields["missing_required_fields"] = sorted(
|
|
1961
|
+
dict.fromkeys(list(error_fields.get("missing_required_fields", [])) + ["app_key"])
|
|
1962
|
+
)
|
|
1963
|
+
error_fields["suggested_schema_call"] = None
|
|
1964
|
+
return {
|
|
1965
|
+
"status": "failed",
|
|
1966
|
+
"response": {
|
|
1967
|
+
"status": "failed",
|
|
1968
|
+
"stage": "app",
|
|
1969
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
1970
|
+
**error_fields,
|
|
1971
|
+
},
|
|
1972
|
+
}
|
|
1973
|
+
return {"status": "none"}
|
|
1974
|
+
|
|
1975
|
+
app_tools = AppTools(self.sessions, self.backend)
|
|
1976
|
+
try:
|
|
1977
|
+
base_info = app_tools.app_get_base(profile=profile, app_key=normalized_app_key, include_raw=True).get("result") or {}
|
|
1978
|
+
except Exception as exc: # noqa: BLE001
|
|
1979
|
+
detail = f"failed to resolve existing app '{normalized_app_key}': {exc}"
|
|
1980
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
1981
|
+
error_fields["suggested_schema_call"] = None
|
|
1982
|
+
return {
|
|
1983
|
+
"status": "failed",
|
|
1984
|
+
"response": {
|
|
1985
|
+
"status": "failed",
|
|
1986
|
+
"stage": "app",
|
|
1987
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
1988
|
+
**error_fields,
|
|
1989
|
+
},
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
raw_tag_ids = base_info.get("tagIds") if isinstance(base_info.get("tagIds"), list) else []
|
|
1993
|
+
tag_ids = [tag_id for tag_id in (_coerce_count(item) for item in raw_tag_ids) if tag_id is not None]
|
|
1994
|
+
effective_package_resolution = deepcopy(package_resolution)
|
|
1995
|
+
if package_resolution.get("status") == "resolved":
|
|
1996
|
+
resolved_tag_id = _coerce_count(package_resolution.get("tag_id"))
|
|
1997
|
+
if resolved_tag_id is not None and tag_ids and resolved_tag_id not in tag_ids:
|
|
1998
|
+
detail = (
|
|
1999
|
+
f"app_key '{normalized_app_key}' does not belong to requested package "
|
|
2000
|
+
f"'{package_resolution.get('tag_name') or resolved_tag_id}'"
|
|
2001
|
+
)
|
|
2002
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
2003
|
+
error_fields["error_code"] = "APP_PACKAGE_MISMATCH"
|
|
2004
|
+
error_fields["suggested_schema_call"] = None
|
|
2005
|
+
return {
|
|
2006
|
+
"status": "failed",
|
|
2007
|
+
"response": {
|
|
2008
|
+
"status": "failed",
|
|
2009
|
+
"stage": "app",
|
|
2010
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
2011
|
+
"existing_app": {
|
|
2012
|
+
"app_key": normalized_app_key,
|
|
2013
|
+
"app_name": base_info.get("formTitle"),
|
|
2014
|
+
"tag_ids": tag_ids,
|
|
2015
|
+
},
|
|
2016
|
+
**error_fields,
|
|
2017
|
+
},
|
|
2018
|
+
}
|
|
2019
|
+
elif package_resolution.get("status") == "new_package":
|
|
2020
|
+
if len(tag_ids) == 1:
|
|
2021
|
+
package_tools = PackageTools(self.sessions, self.backend)
|
|
2022
|
+
package_detail = package_tools.package_get(profile=profile, tag_id=tag_ids[0], include_raw=False).get("result") or {}
|
|
2023
|
+
effective_package_resolution = {
|
|
2024
|
+
"status": "resolved",
|
|
2025
|
+
"matched_via": "existing_app",
|
|
2026
|
+
"tag_id": tag_ids[0],
|
|
2027
|
+
"tag_name": package_detail.get("tagName"),
|
|
2028
|
+
"candidates": [],
|
|
2029
|
+
}
|
|
2030
|
+
elif len(tag_ids) > 1:
|
|
2031
|
+
detail = f"app_key '{normalized_app_key}' belongs to multiple packages; provide package_tag_id or package_name explicitly"
|
|
2032
|
+
error_fields = _solution_error_fields(category="config", detail=detail, suggested_next_call=None, stage="app")
|
|
2033
|
+
error_fields["error_code"] = "AMBIGUOUS_PACKAGE"
|
|
2034
|
+
error_fields["suggested_schema_call"] = None
|
|
2035
|
+
return {
|
|
2036
|
+
"status": "failed",
|
|
2037
|
+
"response": {
|
|
2038
|
+
"status": "failed",
|
|
2039
|
+
"stage": "app",
|
|
2040
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
2041
|
+
"existing_app": {
|
|
2042
|
+
"app_key": normalized_app_key,
|
|
2043
|
+
"app_name": base_info.get("formTitle"),
|
|
2044
|
+
"tag_ids": tag_ids,
|
|
2045
|
+
},
|
|
2046
|
+
**error_fields,
|
|
2047
|
+
},
|
|
2048
|
+
}
|
|
2049
|
+
return {
|
|
2050
|
+
"status": "resolved",
|
|
2051
|
+
"app_key": normalized_app_key,
|
|
2052
|
+
"app_name": base_info.get("formTitle"),
|
|
2053
|
+
"package_resolution": effective_package_resolution,
|
|
2054
|
+
"tag_ids": tag_ids,
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
def _requirements_failure_response(
|
|
2058
|
+
self,
|
|
2059
|
+
*,
|
|
2060
|
+
mode: str,
|
|
2061
|
+
build_id: str,
|
|
2062
|
+
title: str,
|
|
2063
|
+
requirement_text: str,
|
|
2064
|
+
package_resolution: dict[str, Any],
|
|
2065
|
+
app_key: str,
|
|
2066
|
+
update_only: bool,
|
|
2067
|
+
layout_style: str,
|
|
2068
|
+
publish: bool,
|
|
2069
|
+
run_label: str | None,
|
|
2070
|
+
detail: str,
|
|
2071
|
+
error_code: str | None = None,
|
|
2072
|
+
recoverable: bool | None = None,
|
|
2073
|
+
missing_required_fields: list[str] | None = None,
|
|
2074
|
+
invalid_field_types: list[str] | None = None,
|
|
2075
|
+
error_details: dict[str, Any] | None = None,
|
|
2076
|
+
) -> dict[str, Any]:
|
|
2077
|
+
suggested_next_call = (
|
|
2078
|
+
{"tool_name": "solution_build_app", "arguments": {"mode": "plan", "build_id": build_id}}
|
|
2079
|
+
if build_id and self._load_generated_app_spec(build_id)
|
|
2080
|
+
else _requirements_next_call(
|
|
2081
|
+
next_mode="plan" if mode in {"", "preflight"} else mode,
|
|
2082
|
+
build_id=build_id or None,
|
|
2083
|
+
title=title,
|
|
2084
|
+
requirement_text=requirement_text,
|
|
2085
|
+
package_resolution=package_resolution,
|
|
2086
|
+
app_key=app_key,
|
|
2087
|
+
update_only=update_only,
|
|
2088
|
+
layout_style=layout_style,
|
|
2089
|
+
publish=publish,
|
|
2090
|
+
run_label=run_label,
|
|
2091
|
+
)
|
|
2092
|
+
)
|
|
2093
|
+
error_fields = _solution_error_fields(
|
|
2094
|
+
category="config",
|
|
2095
|
+
detail=detail,
|
|
2096
|
+
suggested_next_call=suggested_next_call,
|
|
2097
|
+
stage="app",
|
|
2098
|
+
)
|
|
2099
|
+
if error_code:
|
|
2100
|
+
error_fields["error_code"] = error_code
|
|
2101
|
+
if recoverable is not None:
|
|
2102
|
+
error_fields["recoverable"] = recoverable
|
|
2103
|
+
if missing_required_fields:
|
|
2104
|
+
error_fields["missing_required_fields"] = sorted(
|
|
2105
|
+
dict.fromkeys(list(error_fields.get("missing_required_fields", [])) + list(missing_required_fields))
|
|
2106
|
+
)
|
|
2107
|
+
if invalid_field_types:
|
|
2108
|
+
error_fields["invalid_field_types"] = sorted(
|
|
2109
|
+
dict.fromkeys(list(error_fields.get("invalid_field_types", [])) + list(invalid_field_types))
|
|
2110
|
+
)
|
|
2111
|
+
error_fields["suggested_schema_call"] = None
|
|
2112
|
+
return {
|
|
2113
|
+
"status": "failed",
|
|
2114
|
+
"stage": "app",
|
|
2115
|
+
"mode": mode,
|
|
2116
|
+
"build_id": build_id,
|
|
2117
|
+
"errors": [{"category": "config", "detail": detail}],
|
|
2118
|
+
"package_resolution": package_resolution,
|
|
2119
|
+
"next_strategy": "switch_to_explicit_app_stage" if build_id and self._load_generated_app_spec(build_id) else "refine_requirements",
|
|
2120
|
+
"failure_signature": None,
|
|
2121
|
+
"stage_failure_count": 0,
|
|
2122
|
+
"details": deepcopy(error_details or {}),
|
|
2123
|
+
"suggested_requirement_hint": (error_details or {}).get("suggested_requirement_hint") if isinstance(error_details, dict) else None,
|
|
2124
|
+
"suggested_patch": deepcopy((error_details or {}).get("suggested_patch")) if isinstance(error_details, dict) else None,
|
|
2125
|
+
**error_fields,
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
def _requirements_success_response(
|
|
2129
|
+
self,
|
|
2130
|
+
*,
|
|
2131
|
+
mode: str,
|
|
2132
|
+
title: str,
|
|
2133
|
+
requirement_text: str,
|
|
2134
|
+
package_resolution: dict[str, Any],
|
|
2135
|
+
layout_style: str,
|
|
2136
|
+
publish: bool,
|
|
2137
|
+
run_label: str | None,
|
|
2138
|
+
generated: GeneratedAppBuild,
|
|
2139
|
+
stage_result: dict[str, Any],
|
|
2140
|
+
include_generated_spec: bool,
|
|
2141
|
+
) -> dict[str, Any]:
|
|
2142
|
+
next_call = stage_result.get("suggested_next_call")
|
|
2143
|
+
response = {
|
|
2144
|
+
"status": stage_result.get("status"),
|
|
2145
|
+
"stage": "app",
|
|
2146
|
+
"mode": mode,
|
|
2147
|
+
"build_id": stage_result.get("build_id"),
|
|
2148
|
+
"build_path": stage_result.get("build_path"),
|
|
2149
|
+
"run_path": stage_result.get("run_path"),
|
|
2150
|
+
"artifacts": stage_result.get("artifacts", {}),
|
|
2151
|
+
"errors": stage_result.get("errors", []),
|
|
2152
|
+
"package_resolution": package_resolution,
|
|
2153
|
+
"generated_app_summary": generated.summary,
|
|
2154
|
+
"resolved_layout_style": generated.summary.get("resolved_layout_style"),
|
|
2155
|
+
"field_selection_policy": generated.summary.get("field_selection_policy"),
|
|
2156
|
+
"excluded_advanced_fields": generated.summary.get("excluded_advanced_fields"),
|
|
2157
|
+
"execution_step_count": len((stage_result.get("execution_plan") or {}).get("steps", [])),
|
|
2158
|
+
"suggested_next_call": next_call,
|
|
2159
|
+
"views_strategy": _resolve_app_stage_views_strategy(stage_result),
|
|
2160
|
+
}
|
|
2161
|
+
if isinstance(stage_result.get("verification"), dict):
|
|
2162
|
+
verification = stage_result["verification"]
|
|
2163
|
+
response["verification"] = verification
|
|
2164
|
+
if "package_attached" in verification:
|
|
2165
|
+
response["package_attached"] = verification.get("package_attached")
|
|
2166
|
+
if isinstance(verification.get("apps"), list):
|
|
2167
|
+
response["tag_ids_after"] = {
|
|
2168
|
+
str(item.get("entity_id")): item.get("tag_ids_after")
|
|
2169
|
+
for item in verification["apps"]
|
|
2170
|
+
if isinstance(item, dict) and item.get("entity_id") is not None
|
|
2171
|
+
}
|
|
2172
|
+
if isinstance(verification.get("views_created"), list):
|
|
2173
|
+
response["views_created"] = verification.get("views_created")
|
|
2174
|
+
for key in (
|
|
2175
|
+
"error_code",
|
|
2176
|
+
"recoverable",
|
|
2177
|
+
"missing_required_fields",
|
|
2178
|
+
"unknown_fields",
|
|
2179
|
+
"invalid_field_types",
|
|
2180
|
+
"invalid_enum_values",
|
|
2181
|
+
"next_strategy",
|
|
2182
|
+
"failure_signature",
|
|
2183
|
+
"stage_failure_count",
|
|
2184
|
+
):
|
|
2185
|
+
if key in stage_result:
|
|
2186
|
+
response[key] = stage_result[key]
|
|
2187
|
+
if include_generated_spec:
|
|
2188
|
+
response["generated_app_spec"] = generated.app_spec
|
|
2189
|
+
return response
|
|
2190
|
+
|
|
2191
|
+
|
|
2192
|
+
def deep_merge(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
2193
|
+
merged = deepcopy(base)
|
|
2194
|
+
for key, value in patch.items():
|
|
2195
|
+
if isinstance(value, dict) and isinstance(merged.get(key), dict):
|
|
2196
|
+
merged[key] = deep_merge(merged[key], value)
|
|
2197
|
+
else:
|
|
2198
|
+
merged[key] = deepcopy(value)
|
|
2199
|
+
return merged
|
|
2200
|
+
|
|
2201
|
+
|
|
2202
|
+
def _extract_publish_results(step_results: dict[str, Any]) -> dict[str, Any]:
|
|
2203
|
+
return {
|
|
2204
|
+
step_name: step_result
|
|
2205
|
+
for step_name, step_result in step_results.items()
|
|
2206
|
+
if step_name.startswith("publish.")
|
|
2207
|
+
}
|
|
2208
|
+
|
|
2209
|
+
|
|
2210
|
+
def _sanitize_json_value(value: Any) -> Any:
|
|
2211
|
+
if value is None or isinstance(value, (str, int, float, bool)):
|
|
2212
|
+
return value
|
|
2213
|
+
if isinstance(value, BaseModel):
|
|
2214
|
+
return _sanitize_json_value(value.model_dump(mode="json"))
|
|
2215
|
+
if isinstance(value, dict):
|
|
2216
|
+
return {str(key): _sanitize_json_value(item) for key, item in value.items()}
|
|
2217
|
+
if isinstance(value, (list, tuple, set)):
|
|
2218
|
+
return [_sanitize_json_value(item) for item in value]
|
|
2219
|
+
if isinstance(value, Exception):
|
|
2220
|
+
return str(value)
|
|
2221
|
+
if hasattr(value, "__fspath__"):
|
|
2222
|
+
return str(value)
|
|
2223
|
+
return str(value)
|
|
2224
|
+
|
|
2225
|
+
|
|
2226
|
+
def _sanitize_errors(errors: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
2227
|
+
sanitized: list[dict[str, Any]] = []
|
|
2228
|
+
for error in errors:
|
|
2229
|
+
payload = _sanitize_json_value(error)
|
|
2230
|
+
if isinstance(payload, dict):
|
|
2231
|
+
sanitized.append(payload)
|
|
2232
|
+
continue
|
|
2233
|
+
sanitized.append({"category": "runtime", "detail": payload})
|
|
2234
|
+
return sanitized
|
|
2235
|
+
|
|
2236
|
+
|
|
2237
|
+
def _generate_build_id(*, run_label: str | None, stage_name: str) -> str:
|
|
2238
|
+
prefix = _slug_fragment(run_label) or stage_name
|
|
2239
|
+
return f"{prefix}-{uuid4().hex[:10]}"
|
|
2240
|
+
|
|
2241
|
+
|
|
2242
|
+
def _generate_session_id(*, metadata: dict[str, Any], stage: str | None) -> str:
|
|
2243
|
+
base = _slug_fragment(str(metadata.get("goal") or metadata.get("task") or metadata.get("run_label") or "")) or stage or "design"
|
|
2244
|
+
return f"{base}-{uuid4().hex[:10]}"
|
|
2245
|
+
|
|
2246
|
+
|
|
2247
|
+
def _slug_fragment(value: str | None) -> str:
|
|
2248
|
+
if not value:
|
|
2249
|
+
return ""
|
|
2250
|
+
text = "".join(ch.lower() if ch.isalnum() else "-" for ch in value).strip("-")
|
|
2251
|
+
compact = "-".join(part for part in text.split("-") if part)
|
|
2252
|
+
return compact[:32].rstrip("-")
|
|
2253
|
+
|
|
2254
|
+
|
|
2255
|
+
def _solution_build_next_call(*, tool_name: str, mode: str, build_id: str | None) -> dict[str, Any] | None:
|
|
2256
|
+
if not build_id:
|
|
2257
|
+
return None
|
|
2258
|
+
if mode not in STAGED_BUILD_MODES:
|
|
2259
|
+
return None
|
|
2260
|
+
if mode == "preflight":
|
|
2261
|
+
next_mode = "plan"
|
|
2262
|
+
elif mode == "plan":
|
|
2263
|
+
next_mode = "apply"
|
|
2264
|
+
else:
|
|
2265
|
+
next_mode = "repair"
|
|
2266
|
+
return {"tool_name": tool_name, "arguments": {"mode": next_mode, "build_id": build_id}}
|
|
2267
|
+
|
|
2268
|
+
|
|
2269
|
+
def _normalize_staged_build_mode(mode: str) -> str:
|
|
2270
|
+
normalized = str(mode or "").strip().lower()
|
|
2271
|
+
if not normalized:
|
|
2272
|
+
return normalized
|
|
2273
|
+
return STAGED_BUILD_MODE_ALIASES.get(normalized, normalized)
|
|
2274
|
+
|
|
2275
|
+
|
|
2276
|
+
def _requirements_next_call(
|
|
2277
|
+
*,
|
|
2278
|
+
next_mode: str,
|
|
2279
|
+
build_id: str | None,
|
|
2280
|
+
title: str,
|
|
2281
|
+
requirement_text: str,
|
|
2282
|
+
package_resolution: dict[str, Any],
|
|
2283
|
+
app_key: str,
|
|
2284
|
+
update_only: bool,
|
|
2285
|
+
layout_style: str,
|
|
2286
|
+
publish: bool,
|
|
2287
|
+
run_label: str | None,
|
|
2288
|
+
) -> dict[str, Any]:
|
|
2289
|
+
arguments: dict[str, Any] = {
|
|
2290
|
+
"mode": next_mode,
|
|
2291
|
+
"title": title,
|
|
2292
|
+
"requirement_text": requirement_text,
|
|
2293
|
+
"layout_style": layout_style,
|
|
2294
|
+
"publish": publish,
|
|
2295
|
+
}
|
|
2296
|
+
if build_id:
|
|
2297
|
+
arguments["build_id"] = build_id
|
|
2298
|
+
if run_label:
|
|
2299
|
+
arguments["run_label"] = run_label
|
|
2300
|
+
if isinstance(package_resolution.get("tag_id"), int) and package_resolution["tag_id"] > 0:
|
|
2301
|
+
arguments["package_tag_id"] = package_resolution["tag_id"]
|
|
2302
|
+
elif isinstance(package_resolution.get("tag_name"), str) and package_resolution["tag_name"]:
|
|
2303
|
+
arguments["package_name"] = package_resolution["tag_name"]
|
|
2304
|
+
if isinstance(app_key, str) and app_key:
|
|
2305
|
+
arguments["app_key"] = app_key
|
|
2306
|
+
if update_only or arguments.get("app_key"):
|
|
2307
|
+
arguments["update_only"] = True
|
|
2308
|
+
return {"tool_name": "solution_build_app_from_requirements", "arguments": arguments}
|
|
2309
|
+
|
|
2310
|
+
|
|
2311
|
+
def _rewrite_requirements_next_call(
|
|
2312
|
+
*,
|
|
2313
|
+
result_next_call: dict[str, Any] | None,
|
|
2314
|
+
title: str,
|
|
2315
|
+
requirement_text: str,
|
|
2316
|
+
package_resolution: dict[str, Any],
|
|
2317
|
+
app_key: str,
|
|
2318
|
+
update_only: bool,
|
|
2319
|
+
layout_style: str,
|
|
2320
|
+
publish: bool,
|
|
2321
|
+
run_label: str | None,
|
|
2322
|
+
) -> dict[str, Any] | None:
|
|
2323
|
+
if not isinstance(result_next_call, dict):
|
|
2324
|
+
return None
|
|
2325
|
+
arguments = result_next_call.get("arguments")
|
|
2326
|
+
if not isinstance(arguments, dict):
|
|
2327
|
+
return None
|
|
2328
|
+
next_mode = arguments.get("mode")
|
|
2329
|
+
if not isinstance(next_mode, str) or not next_mode:
|
|
2330
|
+
return None
|
|
2331
|
+
build_id = arguments.get("build_id")
|
|
2332
|
+
return _requirements_next_call(
|
|
2333
|
+
next_mode=next_mode,
|
|
2334
|
+
build_id=build_id if isinstance(build_id, str) else None,
|
|
2335
|
+
title=title,
|
|
2336
|
+
requirement_text=requirement_text,
|
|
2337
|
+
package_resolution=package_resolution,
|
|
2338
|
+
app_key=app_key,
|
|
2339
|
+
update_only=update_only,
|
|
2340
|
+
layout_style=layout_style,
|
|
2341
|
+
publish=publish,
|
|
2342
|
+
run_label=run_label,
|
|
2343
|
+
)
|
|
2344
|
+
|
|
2345
|
+
|
|
2346
|
+
def _requirements_target(
|
|
2347
|
+
package_resolution: dict[str, Any],
|
|
2348
|
+
*,
|
|
2349
|
+
app_key: str = "",
|
|
2350
|
+
update_only: bool = False,
|
|
2351
|
+
) -> dict[str, Any]:
|
|
2352
|
+
tag_id = package_resolution.get("tag_id")
|
|
2353
|
+
target: dict[str, Any] = {}
|
|
2354
|
+
if isinstance(tag_id, int) and tag_id > 0:
|
|
2355
|
+
target["package_tag_id"] = tag_id
|
|
2356
|
+
if isinstance(app_key, str) and app_key:
|
|
2357
|
+
target["app_key"] = app_key
|
|
2358
|
+
if update_only or target.get("app_key"):
|
|
2359
|
+
target["mode"] = "update"
|
|
2360
|
+
target["update_only"] = True
|
|
2361
|
+
return target
|
|
2362
|
+
|
|
2363
|
+
|
|
2364
|
+
def _merge_app_target(
|
|
2365
|
+
target: dict[str, Any] | None,
|
|
2366
|
+
*,
|
|
2367
|
+
app_key: str,
|
|
2368
|
+
package_tag_id: int,
|
|
2369
|
+
update_only: bool,
|
|
2370
|
+
) -> dict[str, Any]:
|
|
2371
|
+
merged = deepcopy(target) if isinstance(target, dict) else {}
|
|
2372
|
+
if isinstance(app_key, str) and app_key.strip():
|
|
2373
|
+
merged["app_key"] = app_key.strip()
|
|
2374
|
+
if isinstance(package_tag_id, int) and package_tag_id > 0:
|
|
2375
|
+
merged["package_tag_id"] = package_tag_id
|
|
2376
|
+
if update_only or merged.get("app_key"):
|
|
2377
|
+
merged["mode"] = "update"
|
|
2378
|
+
merged["update_only"] = True
|
|
2379
|
+
return merged
|
|
2380
|
+
|
|
2381
|
+
|
|
2382
|
+
def _target_is_update_only(target: dict[str, Any]) -> bool:
|
|
2383
|
+
if not target:
|
|
2384
|
+
return False
|
|
2385
|
+
if target.get("update_only") is True:
|
|
2386
|
+
return True
|
|
2387
|
+
mode = target.get("mode")
|
|
2388
|
+
return isinstance(mode, str) and mode.strip().lower() == "update"
|
|
2389
|
+
|
|
2390
|
+
|
|
2391
|
+
def _resolve_target_app_keys(*, target: dict[str, Any], entity_ids: list[str]) -> dict[str, str]:
|
|
2392
|
+
if not target:
|
|
2393
|
+
return {}
|
|
2394
|
+
resolved: dict[str, str] = {}
|
|
2395
|
+
direct_app_key = target.get("app_key")
|
|
2396
|
+
if isinstance(direct_app_key, str) and direct_app_key.strip():
|
|
2397
|
+
if len(entity_ids) != 1:
|
|
2398
|
+
raise ValueError("target.app_key requires a single-entity app stage; use target.app_keys for multiple entities")
|
|
2399
|
+
resolved[entity_ids[0]] = direct_app_key.strip()
|
|
2400
|
+
raw_mapping = target.get("app_keys")
|
|
2401
|
+
if isinstance(raw_mapping, dict):
|
|
2402
|
+
for entity_id, app_key in raw_mapping.items():
|
|
2403
|
+
if not isinstance(entity_id, str) or entity_id not in entity_ids:
|
|
2404
|
+
raise ValueError(f"target.app_keys contains unknown entity '{entity_id}'")
|
|
2405
|
+
if not isinstance(app_key, str) or not app_key.strip():
|
|
2406
|
+
raise ValueError(f"target.app_keys['{entity_id}'] must be a non-empty string")
|
|
2407
|
+
resolved[entity_id] = app_key.strip()
|
|
2408
|
+
return resolved
|
|
2409
|
+
|
|
2410
|
+
|
|
2411
|
+
def _seed_existing_app_artifacts(*, assembly: BuildAssemblyStore, existing_app_targets: dict[str, str]) -> None:
|
|
2412
|
+
if not existing_app_targets:
|
|
2413
|
+
return
|
|
2414
|
+
artifacts = assembly.get_artifacts()
|
|
2415
|
+
apps_artifact = artifacts.get("apps", {}) if isinstance(artifacts.get("apps"), dict) else {}
|
|
2416
|
+
for entity_id, app_key in existing_app_targets.items():
|
|
2417
|
+
existing = apps_artifact.get(entity_id, {}) if isinstance(apps_artifact.get(entity_id), dict) else {}
|
|
2418
|
+
next_artifact = deepcopy(existing)
|
|
2419
|
+
next_artifact["app_key"] = app_key
|
|
2420
|
+
next_artifact["reused"] = True
|
|
2421
|
+
next_artifact["target_mode"] = "update"
|
|
2422
|
+
apps_artifact[entity_id] = next_artifact
|
|
2423
|
+
artifacts["apps"] = apps_artifact
|
|
2424
|
+
assembly.set_artifacts(artifacts)
|
|
2425
|
+
|
|
2426
|
+
|
|
2427
|
+
def _design_session_next_call(*, action: str, session_id: str | None) -> dict[str, Any] | None:
|
|
2428
|
+
if action == "start" and not session_id:
|
|
2429
|
+
return {"tool_name": "solution_design_session", "arguments": {"action": "start"}}
|
|
2430
|
+
if session_id:
|
|
2431
|
+
return {"tool_name": "solution_design_session", "arguments": {"action": "get", "session_id": session_id}}
|
|
2432
|
+
return None
|
|
2433
|
+
|
|
2434
|
+
|
|
2435
|
+
def _design_summary_next_call(summary: dict[str, Any]) -> dict[str, Any] | None:
|
|
2436
|
+
session_id = summary.get("session_id")
|
|
2437
|
+
if not session_id:
|
|
2438
|
+
return None
|
|
2439
|
+
if summary.get("status") == "finalized":
|
|
2440
|
+
return None
|
|
2441
|
+
next_stage = summary.get("next_stage")
|
|
2442
|
+
if next_stage == "finalize":
|
|
2443
|
+
return {"tool_name": "solution_design_session", "arguments": {"action": "finalize", "session_id": session_id}}
|
|
2444
|
+
if next_stage:
|
|
2445
|
+
return {
|
|
2446
|
+
"tool_name": "solution_design_session",
|
|
2447
|
+
"arguments": {"action": "submit_stage", "session_id": session_id, "stage": next_stage},
|
|
2448
|
+
}
|
|
2449
|
+
return {"tool_name": "solution_design_session", "arguments": {"action": "get", "session_id": session_id}}
|
|
2450
|
+
|
|
2451
|
+
|
|
2452
|
+
def _normalize_solution_schema_stage(stage: str) -> str | None:
|
|
2453
|
+
normalized = str(stage or "").strip()
|
|
2454
|
+
if not normalized:
|
|
2455
|
+
return None
|
|
2456
|
+
return SOLUTION_SCHEMA_STAGE_ALIASES.get(normalized.lower()) or SOLUTION_SCHEMA_STAGE_ALIASES.get(normalized)
|
|
2457
|
+
|
|
2458
|
+
|
|
2459
|
+
def _normalize_solution_schema_intent(intent: str) -> str | None:
|
|
2460
|
+
normalized = str(intent or "").strip()
|
|
2461
|
+
if not normalized:
|
|
2462
|
+
return None
|
|
2463
|
+
return SOLUTION_SCHEMA_INTENT_ALIASES.get(normalized.lower()) or SOLUTION_SCHEMA_INTENT_ALIASES.get(normalized)
|
|
2464
|
+
|
|
2465
|
+
|
|
2466
|
+
def _solution_schema_example_payload(
|
|
2467
|
+
*,
|
|
2468
|
+
stage: str,
|
|
2469
|
+
intent: str,
|
|
2470
|
+
include_examples: bool = False,
|
|
2471
|
+
include_selected_example: bool = False,
|
|
2472
|
+
) -> dict[str, Any]:
|
|
2473
|
+
tool_name, payload_key = _solution_schema_tool_metadata(stage)
|
|
2474
|
+
examples = _solution_schema_examples(stage)
|
|
2475
|
+
selected = deepcopy(examples.get(intent) or examples["minimal"])
|
|
2476
|
+
call_example = _solution_schema_call_example(stage)
|
|
2477
|
+
response = {
|
|
2478
|
+
"status": "ok",
|
|
2479
|
+
"stage": stage,
|
|
2480
|
+
"intent": intent,
|
|
2481
|
+
"tool_name": tool_name,
|
|
2482
|
+
"payload_key": payload_key,
|
|
2483
|
+
"available_intents": sorted(examples.keys()),
|
|
2484
|
+
"required_fields": _solution_schema_required_fields(stage),
|
|
2485
|
+
"optional_fields": _solution_schema_optional_fields(stage),
|
|
2486
|
+
"common_errors": _solution_schema_common_errors(stage),
|
|
2487
|
+
"notes": _solution_schema_notes(stage),
|
|
2488
|
+
"call_example": call_example,
|
|
2489
|
+
"suggested_next_call": deepcopy(call_example),
|
|
2490
|
+
}
|
|
2491
|
+
if include_selected_example:
|
|
2492
|
+
response["selected_example"] = selected
|
|
2493
|
+
if include_examples:
|
|
2494
|
+
response["examples"] = examples
|
|
2495
|
+
return response
|
|
2496
|
+
|
|
2497
|
+
|
|
2498
|
+
def _solution_schema_tool_metadata(stage: str) -> tuple[str, str]:
|
|
2499
|
+
mapping = {
|
|
2500
|
+
"app": ("solution_build_app", "app_spec"),
|
|
2501
|
+
"app_update": ("solution_build_app", "app_spec"),
|
|
2502
|
+
"flow": ("solution_build_flow", "flow_spec"),
|
|
2503
|
+
"views": ("solution_build_views", "views_spec"),
|
|
2504
|
+
"analytics_portal": ("solution_build_analytics_portal", "analytics_portal_spec"),
|
|
2505
|
+
"navigation": ("solution_build_navigation", "navigation_spec"),
|
|
2506
|
+
"app_flow": ("solution_build_app_flow", "app_flow_spec"),
|
|
2507
|
+
"all": ("solution_build_all", "solution_spec"),
|
|
2508
|
+
}
|
|
2509
|
+
return mapping[stage]
|
|
2510
|
+
|
|
2511
|
+
|
|
2512
|
+
def _solution_schema_call_example(stage: str) -> dict[str, Any]:
|
|
2513
|
+
tool_name, _payload_key = _solution_schema_tool_metadata(stage)
|
|
2514
|
+
arguments: dict[str, Any] = {"mode": "preflight"}
|
|
2515
|
+
if stage == "app_update":
|
|
2516
|
+
arguments.update({"app_key": "APP_123456", "update_only": True})
|
|
2517
|
+
return {"tool_name": tool_name, "arguments": arguments}
|
|
2518
|
+
|
|
2519
|
+
|
|
2520
|
+
def _solution_schema_required_fields(stage: str) -> list[str]:
|
|
2521
|
+
fields = {
|
|
2522
|
+
"app": ["solution_name", "entities[].entity_id", "entities[].display_name", "entities[].kind", "entities[].fields[]"],
|
|
2523
|
+
"app_update": ["solution_name", "entities[].entity_id", "entities[].display_name", "entities[].kind", "entities[].fields[]", "app_key"],
|
|
2524
|
+
"flow": ["entities[].entity_id", "entities[].workflow or entities[].lifecycle_stages"],
|
|
2525
|
+
"views": ["entities[].entity_id", "entities[].views[]"],
|
|
2526
|
+
"analytics_portal": ["entities[].entity_id or portal.sections[]"],
|
|
2527
|
+
"navigation": ["navigation.items[]"],
|
|
2528
|
+
"app_flow": ["solution_name", "entities[]"],
|
|
2529
|
+
"all": ["solution_name", "entities[]"],
|
|
2530
|
+
}
|
|
2531
|
+
return fields[stage]
|
|
2532
|
+
|
|
2533
|
+
|
|
2534
|
+
def _solution_schema_optional_fields(stage: str) -> list[str]:
|
|
2535
|
+
fields = {
|
|
2536
|
+
"app": ["summary", "business_context", "package", "roles", "publish_policy", "preferences", "entities[].form_layout", "entities[].sample_records"],
|
|
2537
|
+
"app_update": ["package_tag_id", "publish_policy", "preferences", "entities[].form_layout", "entities[].sample_records", "update_only"],
|
|
2538
|
+
"flow": ["solution_name", "roles", "publish_policy", "entities[].status_field_id", "entities[].owner_field_id", "entities[].start_field_id", "entities[].end_field_id"],
|
|
2539
|
+
"views": ["solution_name", "metadata"],
|
|
2540
|
+
"analytics_portal": ["solution_name", "publish_policy", "portal.name", "portal.sections[]"],
|
|
2541
|
+
"navigation": ["solution_name", "publish_policy", "navigation.enabled"],
|
|
2542
|
+
"app_flow": ["summary", "package", "roles", "publish_policy", "preferences"],
|
|
2543
|
+
"all": ["portal", "navigation", "roles", "publish_policy", "preferences", "business_context"],
|
|
2544
|
+
}
|
|
2545
|
+
return fields[stage]
|
|
2546
|
+
|
|
2547
|
+
|
|
2548
|
+
def _solution_schema_common_errors(stage: str) -> list[str]:
|
|
2549
|
+
errors = {
|
|
2550
|
+
"app": [
|
|
2551
|
+
"Use enum values exactly, for example field.type='text' or kind='transaction'.",
|
|
2552
|
+
"Each field_id must be unique within an entity.",
|
|
2553
|
+
"Do not include workflow/views/charts in app_spec. Use solution_build_flow or later stage tools.",
|
|
2554
|
+
],
|
|
2555
|
+
"app_update": [
|
|
2556
|
+
"app_key is required for update mode and must point to an existing app.",
|
|
2557
|
+
"Updating an app uses the same app_spec structure, but the tool target must stay in update mode.",
|
|
2558
|
+
"Do not omit entity_id; updates still need a deterministic entity identity.",
|
|
2559
|
+
],
|
|
2560
|
+
"flow": [
|
|
2561
|
+
"The target entity must already exist in the build manifest or from a prior solution_build_app run.",
|
|
2562
|
+
"status_field_id must point to an existing field, usually 'status'.",
|
|
2563
|
+
"Do not include fields or form_layout in flow_spec.",
|
|
2564
|
+
],
|
|
2565
|
+
"views": [
|
|
2566
|
+
"View field_ids must reference existing fields on the entity.",
|
|
2567
|
+
"Run solution_build_app first so the entity exists.",
|
|
2568
|
+
],
|
|
2569
|
+
"analytics_portal": [
|
|
2570
|
+
"Chart field references must already exist on the entity.",
|
|
2571
|
+
"Portal sections that reference views require solution_build_views to run first.",
|
|
2572
|
+
],
|
|
2573
|
+
"navigation": [
|
|
2574
|
+
"Navigation must include a package target item as the single published root.",
|
|
2575
|
+
],
|
|
2576
|
+
"app_flow": [
|
|
2577
|
+
"If app and flow are authored separately, prefer solution_build_app then solution_build_flow.",
|
|
2578
|
+
],
|
|
2579
|
+
"all": [
|
|
2580
|
+
"Use full solution_spec only when the agent can provide all stages coherently.",
|
|
2581
|
+
],
|
|
2582
|
+
}
|
|
2583
|
+
return errors[stage]
|
|
2584
|
+
|
|
2585
|
+
|
|
2586
|
+
def _solution_schema_notes(stage: str) -> list[str]:
|
|
2587
|
+
notes = {
|
|
2588
|
+
"app": [
|
|
2589
|
+
"Use solution_build_app for package, entity, field, layout, and sample record design.",
|
|
2590
|
+
"If you are unsure, call solution_schema_example(stage='app', intent='minimal') before generating payloads.",
|
|
2591
|
+
],
|
|
2592
|
+
"app_update": [
|
|
2593
|
+
"Use solution_build_app with app_key and update_only=true when modifying an existing app.",
|
|
2594
|
+
"Update mode is not allowed to fall back to new package or new app creation.",
|
|
2595
|
+
],
|
|
2596
|
+
"flow": [
|
|
2597
|
+
"Use solution_build_flow only after app/schema exists.",
|
|
2598
|
+
"workflow.nodes uses node_type enum values like start, audit, fill, copy, branch, condition.",
|
|
2599
|
+
],
|
|
2600
|
+
"views": ["views_spec is purely for view definitions, not fields or workflow."],
|
|
2601
|
+
"analytics_portal": ["analytics_portal_spec may include charts, portal, or both."],
|
|
2602
|
+
"navigation": ["navigation_spec only manages top-level published navigation."],
|
|
2603
|
+
"app_flow": ["solution_build_app_flow remains available for compatibility, but app + flow split is preferred."],
|
|
2604
|
+
"all": ["solution_build_all is best for coherent multi-stage system builds."],
|
|
2605
|
+
}
|
|
2606
|
+
return notes[stage]
|
|
2607
|
+
|
|
2608
|
+
|
|
2609
|
+
def _solution_schema_examples(stage: str) -> dict[str, dict[str, Any]]:
|
|
2610
|
+
examples = {
|
|
2611
|
+
"app": {
|
|
2612
|
+
"minimal": {
|
|
2613
|
+
"solution_name": "Demo Workspace",
|
|
2614
|
+
"entities": [
|
|
2615
|
+
{
|
|
2616
|
+
"entity_id": "demo_request",
|
|
2617
|
+
"display_name": "测试申请",
|
|
2618
|
+
"kind": "transaction",
|
|
2619
|
+
"title_field_id": "title",
|
|
2620
|
+
"fields": [
|
|
2621
|
+
{"field_id": "title", "label": "标题", "type": "text", "required": True},
|
|
2622
|
+
],
|
|
2623
|
+
"form_layout": {"rows": [{"field_ids": ["title"]}]},
|
|
2624
|
+
}
|
|
2625
|
+
],
|
|
2626
|
+
},
|
|
2627
|
+
"full": {
|
|
2628
|
+
"solution_name": "Demo Workspace",
|
|
2629
|
+
"summary": "通用测试应用",
|
|
2630
|
+
"package": {"name": "测试包"},
|
|
2631
|
+
"entities": [
|
|
2632
|
+
{
|
|
2633
|
+
"entity_id": "demo_request",
|
|
2634
|
+
"display_name": "测试申请",
|
|
2635
|
+
"kind": "transaction",
|
|
2636
|
+
"title_field_id": "title",
|
|
2637
|
+
"status_field_id": "status",
|
|
2638
|
+
"fields": [
|
|
2639
|
+
{"field_id": "title", "label": "标题", "type": "text", "required": True},
|
|
2640
|
+
{"field_id": "status", "label": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]},
|
|
2641
|
+
{"field_id": "owner", "label": "负责人", "type": "member"},
|
|
2642
|
+
{"field_id": "amount", "label": "金额", "type": "amount"},
|
|
2643
|
+
],
|
|
2644
|
+
"form_layout": {
|
|
2645
|
+
"sections": [
|
|
2646
|
+
{"section_id": "basic", "title": "基础信息", "rows": [{"field_ids": ["title", "status"]}, {"field_ids": ["owner", "amount"]}]}
|
|
2647
|
+
]
|
|
2648
|
+
},
|
|
2649
|
+
"sample_records": [{"record_id": "demo-1", "values": {"title": "测试记录", "status": "待处理", "amount": 1000}}],
|
|
2650
|
+
}
|
|
2651
|
+
],
|
|
2652
|
+
},
|
|
2653
|
+
"demo": {
|
|
2654
|
+
"solution_name": "全字段演示",
|
|
2655
|
+
"package": {"name": "字段演示"},
|
|
2656
|
+
"entities": [
|
|
2657
|
+
{
|
|
2658
|
+
"entity_id": "field_demo",
|
|
2659
|
+
"display_name": "字段演示",
|
|
2660
|
+
"kind": "transaction",
|
|
2661
|
+
"title_field_id": "title",
|
|
2662
|
+
"fields": [
|
|
2663
|
+
{"field_id": "title", "label": "标题", "type": "text", "required": True},
|
|
2664
|
+
{"field_id": "description", "label": "说明", "type": "long_text"},
|
|
2665
|
+
{"field_id": "amount", "label": "金额", "type": "amount"},
|
|
2666
|
+
{"field_id": "due_at", "label": "截止时间", "type": "datetime"},
|
|
2667
|
+
{"field_id": "assignee", "label": "处理人", "type": "member"},
|
|
2668
|
+
{"field_id": "team", "label": "所属部门", "type": "department"},
|
|
2669
|
+
{"field_id": "category", "label": "类型", "type": "single_select", "options": ["A", "B", "C"]},
|
|
2670
|
+
{"field_id": "tags", "label": "标签", "type": "multi_select", "options": ["高优先级", "新建", "内部"]},
|
|
2671
|
+
{"field_id": "phone", "label": "手机号", "type": "phone"},
|
|
2672
|
+
{"field_id": "email", "label": "邮箱", "type": "email"},
|
|
2673
|
+
{"field_id": "attachment", "label": "附件", "type": "attachment"},
|
|
2674
|
+
{"field_id": "enabled", "label": "启用", "type": "boolean"},
|
|
2675
|
+
],
|
|
2676
|
+
"form_layout": {
|
|
2677
|
+
"sections": [
|
|
2678
|
+
{"section_id": "basic", "title": "基础信息", "rows": [{"field_ids": ["title", "category"]}, {"field_ids": ["description"]}]},
|
|
2679
|
+
{"section_id": "coordination", "title": "协同", "rows": [{"field_ids": ["assignee", "team"]}, {"field_ids": ["due_at", "amount"]}]},
|
|
2680
|
+
{"section_id": "contact", "title": "联系与附件", "rows": [{"field_ids": ["phone", "email"]}, {"field_ids": ["attachment", "enabled"]}]},
|
|
2681
|
+
]
|
|
2682
|
+
},
|
|
2683
|
+
}
|
|
2684
|
+
],
|
|
2685
|
+
},
|
|
2686
|
+
},
|
|
2687
|
+
"app_update": {
|
|
2688
|
+
"minimal": {
|
|
2689
|
+
"solution_name": "Demo Workspace",
|
|
2690
|
+
"entities": [
|
|
2691
|
+
{
|
|
2692
|
+
"entity_id": "demo_request",
|
|
2693
|
+
"display_name": "测试申请",
|
|
2694
|
+
"kind": "transaction",
|
|
2695
|
+
"title_field_id": "title",
|
|
2696
|
+
"fields": [
|
|
2697
|
+
{"field_id": "title", "label": "工单标题", "type": "text", "required": True},
|
|
2698
|
+
],
|
|
2699
|
+
"form_layout": {"rows": [{"field_ids": ["title"]}]},
|
|
2700
|
+
}
|
|
2701
|
+
],
|
|
2702
|
+
},
|
|
2703
|
+
"full": {
|
|
2704
|
+
"solution_name": "Demo Workspace",
|
|
2705
|
+
"entities": [
|
|
2706
|
+
{
|
|
2707
|
+
"entity_id": "demo_request",
|
|
2708
|
+
"display_name": "测试申请",
|
|
2709
|
+
"kind": "transaction",
|
|
2710
|
+
"title_field_id": "title",
|
|
2711
|
+
"status_field_id": "status",
|
|
2712
|
+
"fields": [
|
|
2713
|
+
{"field_id": "title", "label": "工单标题", "type": "text", "required": True},
|
|
2714
|
+
{"field_id": "status", "label": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]},
|
|
2715
|
+
{"field_id": "priority", "label": "优先级", "type": "single_select", "options": ["P0", "P1", "P2"]},
|
|
2716
|
+
],
|
|
2717
|
+
"form_layout": {
|
|
2718
|
+
"sections": [
|
|
2719
|
+
{"section_id": "basic", "title": "基础信息", "rows": [{"field_ids": ["title", "status"]}, {"field_ids": ["priority"]}]}
|
|
2720
|
+
]
|
|
2721
|
+
},
|
|
2722
|
+
}
|
|
2723
|
+
],
|
|
2724
|
+
},
|
|
2725
|
+
"demo": {
|
|
2726
|
+
"solution_name": "Demo Workspace",
|
|
2727
|
+
"entities": [
|
|
2728
|
+
{
|
|
2729
|
+
"entity_id": "customer_order",
|
|
2730
|
+
"display_name": "客户订单",
|
|
2731
|
+
"kind": "transaction",
|
|
2732
|
+
"title_field_id": "title",
|
|
2733
|
+
"fields": [
|
|
2734
|
+
{"field_id": "title", "label": "订单标题", "type": "text", "required": True},
|
|
2735
|
+
{"field_id": "customer_name", "label": "客户名称", "type": "text"},
|
|
2736
|
+
{"field_id": "amount", "label": "订单金额", "type": "amount"},
|
|
2737
|
+
],
|
|
2738
|
+
"form_layout": {"rows": [{"field_ids": ["title", "customer_name"]}, {"field_ids": ["amount"]}]},
|
|
2739
|
+
}
|
|
2740
|
+
],
|
|
2741
|
+
},
|
|
2742
|
+
},
|
|
2743
|
+
"flow": {
|
|
2744
|
+
"minimal": {
|
|
2745
|
+
"solution_name": "Demo Workspace",
|
|
2746
|
+
"entities": [
|
|
2747
|
+
{
|
|
2748
|
+
"entity_id": "demo_request",
|
|
2749
|
+
"status_field_id": "status",
|
|
2750
|
+
"workflow": {
|
|
2751
|
+
"enabled": True,
|
|
2752
|
+
"nodes": [
|
|
2753
|
+
{"node_id": "approve", "name": "审批", "node_type": "audit"},
|
|
2754
|
+
],
|
|
2755
|
+
},
|
|
2756
|
+
}
|
|
2757
|
+
],
|
|
2758
|
+
},
|
|
2759
|
+
"full": {
|
|
2760
|
+
"solution_name": "Demo Workspace",
|
|
2761
|
+
"roles": [{"role_id": "approver", "name": "审批人"}],
|
|
2762
|
+
"entities": [
|
|
2763
|
+
{
|
|
2764
|
+
"entity_id": "demo_request",
|
|
2765
|
+
"status_field_id": "status",
|
|
2766
|
+
"owner_field_id": "owner",
|
|
2767
|
+
"workflow": {
|
|
2768
|
+
"enabled": True,
|
|
2769
|
+
"nodes": [
|
|
2770
|
+
{"node_id": "start", "name": "发起", "node_type": "start"},
|
|
2771
|
+
{"node_id": "approve", "name": "直属审批", "node_type": "audit", "assignees": {"roles": ["approver"]}},
|
|
2772
|
+
],
|
|
2773
|
+
},
|
|
2774
|
+
}
|
|
2775
|
+
],
|
|
2776
|
+
},
|
|
2777
|
+
"demo": {
|
|
2778
|
+
"solution_name": "Demo Workspace",
|
|
2779
|
+
"entities": [
|
|
2780
|
+
{
|
|
2781
|
+
"entity_id": "field_demo",
|
|
2782
|
+
"status_field_id": "status",
|
|
2783
|
+
"workflow": {
|
|
2784
|
+
"enabled": True,
|
|
2785
|
+
"nodes": [
|
|
2786
|
+
{"node_id": "start", "name": "发起", "node_type": "start"},
|
|
2787
|
+
{"node_id": "manager_review", "name": "经理审批", "node_type": "audit"},
|
|
2788
|
+
{"node_id": "copy_notice", "name": "结果抄送", "node_type": "copy"},
|
|
2789
|
+
],
|
|
2790
|
+
},
|
|
2791
|
+
}
|
|
2792
|
+
],
|
|
2793
|
+
},
|
|
2794
|
+
},
|
|
2795
|
+
"views": {
|
|
2796
|
+
"minimal": {
|
|
2797
|
+
"solution_name": "Demo Workspace",
|
|
2798
|
+
"entities": [
|
|
2799
|
+
{"entity_id": "demo_request", "views": [{"view_id": "table", "name": "表格", "type": "table", "field_ids": ["title"], "being_default": True}]}
|
|
2800
|
+
],
|
|
2801
|
+
},
|
|
2802
|
+
"full": {
|
|
2803
|
+
"solution_name": "Demo Workspace",
|
|
2804
|
+
"entities": [
|
|
2805
|
+
{
|
|
2806
|
+
"entity_id": "demo_request",
|
|
2807
|
+
"views": [
|
|
2808
|
+
{"view_id": "table", "name": "表格", "type": "table", "field_ids": ["title", "status", "owner"], "being_default": True},
|
|
2809
|
+
{"view_id": "board", "name": "看板", "type": "board", "field_ids": ["title", "status"], "group_by_field_id": "status"},
|
|
2810
|
+
],
|
|
2811
|
+
}
|
|
2812
|
+
],
|
|
2813
|
+
},
|
|
2814
|
+
"demo": {
|
|
2815
|
+
"solution_name": "Demo Workspace",
|
|
2816
|
+
"entities": [
|
|
2817
|
+
{
|
|
2818
|
+
"entity_id": "field_demo",
|
|
2819
|
+
"views": [
|
|
2820
|
+
{"view_id": "table", "name": "字段表格", "type": "table", "field_ids": ["title", "category", "assignee"], "being_default": True},
|
|
2821
|
+
{"view_id": "card", "name": "卡片", "type": "card", "field_ids": ["title", "category", "due_at"]},
|
|
2822
|
+
],
|
|
2823
|
+
}
|
|
2824
|
+
],
|
|
2825
|
+
},
|
|
2826
|
+
},
|
|
2827
|
+
"analytics_portal": {
|
|
2828
|
+
"minimal": {
|
|
2829
|
+
"solution_name": "Demo Workspace",
|
|
2830
|
+
"entities": [{"entity_id": "demo_request", "charts": [{"chart_id": "total", "name": "总量", "chart_type": "target", "indicator_field_ids": ["title"], "config": {"aggregate": "count"}}]}],
|
|
2831
|
+
},
|
|
2832
|
+
"full": {
|
|
2833
|
+
"solution_name": "Demo Workspace",
|
|
2834
|
+
"entities": [{"entity_id": "demo_request", "charts": [{"chart_id": "total", "name": "总量", "chart_type": "target", "indicator_field_ids": ["title"], "config": {"aggregate": "count"}}]}],
|
|
2835
|
+
"portal": {"name": "Demo 首页", "sections": [{"section_id": "total", "title": "总量", "source_type": "chart", "entity_id": "demo_request", "chart_id": "total"}]},
|
|
2836
|
+
},
|
|
2837
|
+
"demo": {
|
|
2838
|
+
"solution_name": "Demo Workspace",
|
|
2839
|
+
"entities": [{"entity_id": "field_demo", "charts": [{"chart_id": "by_category", "name": "按类型统计", "chart_type": "pie", "dimension_field_ids": ["category"], "config": {"aggregate": "count"}}]}],
|
|
2840
|
+
"portal": {"name": "字段演示首页", "sections": [{"section_id": "category_chart", "title": "类型分布", "source_type": "chart", "entity_id": "field_demo", "chart_id": "by_category"}]},
|
|
2841
|
+
},
|
|
2842
|
+
},
|
|
2843
|
+
"navigation": {
|
|
2844
|
+
"minimal": {"solution_name": "Demo Workspace", "navigation": {"items": [{"item_id": "package_root", "title": "Demo Workspace", "target_type": "package"}]}},
|
|
2845
|
+
"full": {"solution_name": "Demo Workspace", "navigation": {"items": [{"item_id": "package_root", "title": "Demo Workspace", "target_type": "package"}, {"item_id": "demo_request", "title": "测试申请", "target_type": "app", "entity_id": "demo_request"}]}},
|
|
2846
|
+
"demo": {"solution_name": "Demo Workspace", "navigation": {"items": [{"item_id": "package_root", "title": "字段演示", "target_type": "package"}, {"item_id": "field_demo", "title": "字段演示", "target_type": "app", "entity_id": "field_demo"}]}},
|
|
2847
|
+
},
|
|
2848
|
+
"app_flow": {
|
|
2849
|
+
"minimal": {
|
|
2850
|
+
"solution_name": "Demo Workspace",
|
|
2851
|
+
"entities": [
|
|
2852
|
+
{
|
|
2853
|
+
"entity_id": "demo_request",
|
|
2854
|
+
"display_name": "测试申请",
|
|
2855
|
+
"kind": "transaction",
|
|
2856
|
+
"title_field_id": "title",
|
|
2857
|
+
"status_field_id": "status",
|
|
2858
|
+
"fields": [
|
|
2859
|
+
{"field_id": "title", "label": "标题", "type": "text", "required": True},
|
|
2860
|
+
{"field_id": "status", "label": "状态", "type": "single_select", "options": ["待处理", "已完成"]},
|
|
2861
|
+
],
|
|
2862
|
+
"form_layout": {"rows": [{"field_ids": ["title", "status"]}]},
|
|
2863
|
+
"workflow": {"enabled": True, "nodes": [{"node_id": "approve", "name": "审批", "node_type": "audit"}]},
|
|
2864
|
+
}
|
|
2865
|
+
],
|
|
2866
|
+
},
|
|
2867
|
+
"full": {},
|
|
2868
|
+
"demo": {},
|
|
2869
|
+
},
|
|
2870
|
+
"all": {
|
|
2871
|
+
"minimal": {
|
|
2872
|
+
"solution_name": "Demo Workspace",
|
|
2873
|
+
"entities": [
|
|
2874
|
+
{
|
|
2875
|
+
"entity_id": "demo_request",
|
|
2876
|
+
"display_name": "测试申请",
|
|
2877
|
+
"kind": "transaction",
|
|
2878
|
+
"title_field_id": "title",
|
|
2879
|
+
"fields": [{"field_id": "title", "label": "标题", "type": "text", "required": True}],
|
|
2880
|
+
"form_layout": {"rows": [{"field_ids": ["title"]}]},
|
|
2881
|
+
}
|
|
2882
|
+
],
|
|
2883
|
+
},
|
|
2884
|
+
"full": {
|
|
2885
|
+
"solution_name": "Demo Workspace",
|
|
2886
|
+
"package": {"name": "Demo Package"},
|
|
2887
|
+
"entities": [
|
|
2888
|
+
{
|
|
2889
|
+
"entity_id": "demo_request",
|
|
2890
|
+
"display_name": "测试申请",
|
|
2891
|
+
"kind": "transaction",
|
|
2892
|
+
"title_field_id": "title",
|
|
2893
|
+
"status_field_id": "status",
|
|
2894
|
+
"fields": [
|
|
2895
|
+
{"field_id": "title", "label": "标题", "type": "text", "required": True},
|
|
2896
|
+
{"field_id": "status", "label": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]},
|
|
2897
|
+
],
|
|
2898
|
+
"form_layout": {"rows": [{"field_ids": ["title", "status"]}]},
|
|
2899
|
+
"workflow": {"enabled": True, "nodes": [{"node_id": "approve", "name": "审批", "node_type": "audit"}]},
|
|
2900
|
+
"views": [{"view_id": "table", "name": "表格", "type": "table", "field_ids": ["title", "status"], "being_default": True}],
|
|
2901
|
+
"charts": [{"chart_id": "total", "name": "总量", "chart_type": "target", "indicator_field_ids": ["title"], "config": {"aggregate": "count"}}],
|
|
2902
|
+
}
|
|
2903
|
+
],
|
|
2904
|
+
"portal": {"name": "Demo 首页", "sections": [{"section_id": "total", "title": "总量", "source_type": "chart", "entity_id": "demo_request", "chart_id": "total"}]},
|
|
2905
|
+
"navigation": {"items": [{"item_id": "package_root", "title": "Demo Workspace", "target_type": "package"}]},
|
|
2906
|
+
},
|
|
2907
|
+
"demo": {},
|
|
2908
|
+
},
|
|
2909
|
+
}
|
|
2910
|
+
if not examples["app_flow"]["full"]:
|
|
2911
|
+
examples["app_flow"]["full"] = deepcopy(examples["all"]["full"])
|
|
2912
|
+
if not examples["app_flow"]["demo"]:
|
|
2913
|
+
examples["app_flow"]["demo"] = deepcopy(examples["app"]["demo"])
|
|
2914
|
+
examples["app_flow"]["demo"]["entities"][0]["status_field_id"] = "status"
|
|
2915
|
+
examples["app_flow"]["demo"]["entities"][0]["fields"].append(
|
|
2916
|
+
{"field_id": "status", "label": "状态", "type": "single_select", "options": ["待处理", "处理中", "已完成"]}
|
|
2917
|
+
)
|
|
2918
|
+
examples["app_flow"]["demo"]["entities"][0]["workflow"] = {
|
|
2919
|
+
"enabled": True,
|
|
2920
|
+
"nodes": [
|
|
2921
|
+
{"node_id": "start", "name": "发起", "node_type": "start"},
|
|
2922
|
+
{"node_id": "approve", "name": "审批", "node_type": "audit"},
|
|
2923
|
+
],
|
|
2924
|
+
}
|
|
2925
|
+
if not examples["all"]["demo"]:
|
|
2926
|
+
examples["all"]["demo"] = deepcopy(examples["all"]["full"])
|
|
2927
|
+
return examples[stage]
|
|
2928
|
+
|
|
2929
|
+
|
|
2930
|
+
def _solution_error_fields(
|
|
2931
|
+
*,
|
|
2932
|
+
category: str,
|
|
2933
|
+
detail: Any,
|
|
2934
|
+
suggested_next_call: dict[str, Any] | None,
|
|
2935
|
+
stage: str | None = None,
|
|
2936
|
+
) -> dict[str, Any]:
|
|
2937
|
+
detail_text = str(detail)
|
|
2938
|
+
missing_required_fields: list[str] = []
|
|
2939
|
+
unknown_fields: list[str] = []
|
|
2940
|
+
invalid_field_types: list[str] = []
|
|
2941
|
+
invalid_enum_values: list[str] = []
|
|
2942
|
+
if isinstance(detail, list):
|
|
2943
|
+
for item in detail:
|
|
2944
|
+
if not isinstance(item, dict):
|
|
2945
|
+
continue
|
|
2946
|
+
loc = ".".join(str(part) for part in item.get("loc", []))
|
|
2947
|
+
err_type = str(item.get("type", ""))
|
|
2948
|
+
if err_type.endswith("missing") and loc:
|
|
2949
|
+
missing_required_fields.append(loc)
|
|
2950
|
+
if err_type == "extra_forbidden" and loc:
|
|
2951
|
+
unknown_fields.append(loc)
|
|
2952
|
+
if (err_type.endswith("_type") or err_type in {"enum", "literal_error", "model_type", "list_type", "dict_type"}) and loc:
|
|
2953
|
+
invalid_field_types.append(loc)
|
|
2954
|
+
if err_type in {"enum", "literal_error"} and loc:
|
|
2955
|
+
invalid_enum_values.append(loc)
|
|
2956
|
+
if "build_id is required" in detail_text:
|
|
2957
|
+
missing_required_fields.append("build_id")
|
|
2958
|
+
if "session_id is required" in detail_text:
|
|
2959
|
+
missing_required_fields.append("session_id")
|
|
2960
|
+
if "solution_spec must be a non-empty object" in detail_text:
|
|
2961
|
+
missing_required_fields.append("solution_spec")
|
|
2962
|
+
if "design_patch must be a non-empty object" in detail_text:
|
|
2963
|
+
missing_required_fields.append("design_patch")
|
|
2964
|
+
recoverable = category in {"config", "validation"}
|
|
2965
|
+
error_code = f"SOLUTION_{category.upper()}"
|
|
2966
|
+
normalized_stage = "app" if stage == "app_flow" else stage
|
|
2967
|
+
suggested_schema_call = None
|
|
2968
|
+
if recoverable and normalized_stage in SOLUTION_SCHEMA_STAGES:
|
|
2969
|
+
suggested_schema_call = {
|
|
2970
|
+
"tool_name": "solution_schema_example",
|
|
2971
|
+
"arguments": {"stage": normalized_stage, "intent": "minimal"},
|
|
2972
|
+
}
|
|
2973
|
+
return {
|
|
2974
|
+
"error_code": error_code,
|
|
2975
|
+
"recoverable": recoverable,
|
|
2976
|
+
"missing_required_fields": sorted(dict.fromkeys(missing_required_fields)),
|
|
2977
|
+
"unknown_fields": sorted(dict.fromkeys(unknown_fields)),
|
|
2978
|
+
"invalid_field_types": sorted(dict.fromkeys(invalid_field_types)),
|
|
2979
|
+
"invalid_enum_values": sorted(dict.fromkeys(invalid_enum_values)),
|
|
2980
|
+
"suggested_next_call": suggested_next_call,
|
|
2981
|
+
"suggested_schema_call": suggested_schema_call,
|
|
2982
|
+
}
|
|
2983
|
+
|
|
2984
|
+
|
|
2985
|
+
def _coerce_count(value: Any) -> int | None:
|
|
2986
|
+
if isinstance(value, bool) or value is None:
|
|
2987
|
+
return None
|
|
2988
|
+
if isinstance(value, int):
|
|
2989
|
+
return value
|
|
2990
|
+
if isinstance(value, float):
|
|
2991
|
+
return int(value)
|
|
2992
|
+
if isinstance(value, str):
|
|
2993
|
+
text = value.strip()
|
|
2994
|
+
if not text:
|
|
2995
|
+
return None
|
|
2996
|
+
try:
|
|
2997
|
+
return int(text)
|
|
2998
|
+
except ValueError:
|
|
2999
|
+
return None
|
|
3000
|
+
return None
|
|
3001
|
+
|
|
3002
|
+
|
|
3003
|
+
def _failure_signature_for_stage(*, stage_name: str, stage_payload: dict[str, Any], errors: list[dict[str, Any]]) -> str | None:
|
|
3004
|
+
error_steps = sorted({str(error.get("step_name")) for error in errors if isinstance(error, dict) and error.get("step_name")})
|
|
3005
|
+
if stage_name != "app":
|
|
3006
|
+
return fingerprint_payload(
|
|
3007
|
+
{
|
|
3008
|
+
"stage": stage_name,
|
|
3009
|
+
"error_steps": error_steps,
|
|
3010
|
+
}
|
|
3011
|
+
)
|
|
3012
|
+
entities = stage_payload.get("entities")
|
|
3013
|
+
if not isinstance(entities, list) or not entities:
|
|
3014
|
+
return fingerprint_payload(
|
|
3015
|
+
{
|
|
3016
|
+
"stage": stage_name,
|
|
3017
|
+
"error_steps": error_steps,
|
|
3018
|
+
}
|
|
3019
|
+
)
|
|
3020
|
+
normalized_entities = [_normalized_entity_failure_payload(entity) for entity in entities if isinstance(entity, dict)]
|
|
3021
|
+
if not normalized_entities:
|
|
3022
|
+
return fingerprint_payload(
|
|
3023
|
+
{
|
|
3024
|
+
"stage": stage_name,
|
|
3025
|
+
"error_steps": error_steps,
|
|
3026
|
+
}
|
|
3027
|
+
)
|
|
3028
|
+
return fingerprint_payload(
|
|
3029
|
+
{
|
|
3030
|
+
"stage": stage_name,
|
|
3031
|
+
"entities": normalized_entities,
|
|
3032
|
+
"error_steps": error_steps,
|
|
3033
|
+
}
|
|
3034
|
+
)
|
|
3035
|
+
|
|
3036
|
+
|
|
3037
|
+
def _normalized_entity_failure_payload(entity: dict[str, Any]) -> dict[str, Any]:
|
|
3038
|
+
fields = entity.get("fields")
|
|
3039
|
+
layout = entity.get("form_layout")
|
|
3040
|
+
normalized_fields = []
|
|
3041
|
+
if isinstance(fields, list):
|
|
3042
|
+
for field in fields:
|
|
3043
|
+
if not isinstance(field, dict):
|
|
3044
|
+
continue
|
|
3045
|
+
normalized_fields.append(
|
|
3046
|
+
{
|
|
3047
|
+
"field_id": field.get("field_id"),
|
|
3048
|
+
"type": field.get("type"),
|
|
3049
|
+
"required": bool(field.get("required")),
|
|
3050
|
+
}
|
|
3051
|
+
)
|
|
3052
|
+
return {
|
|
3053
|
+
"entity_id": entity.get("entity_id"),
|
|
3054
|
+
"fields": normalized_fields,
|
|
3055
|
+
"layout": _normalized_layout_signature(layout if isinstance(layout, dict) else {}),
|
|
3056
|
+
}
|
|
3057
|
+
|
|
3058
|
+
|
|
3059
|
+
def _normalized_layout_signature(layout: dict[str, Any]) -> dict[str, Any]:
|
|
3060
|
+
rows = layout.get("rows")
|
|
3061
|
+
sections = layout.get("sections")
|
|
3062
|
+
normalized_rows: list[list[str]] = []
|
|
3063
|
+
if isinstance(rows, list):
|
|
3064
|
+
for row in rows:
|
|
3065
|
+
if isinstance(row, dict):
|
|
3066
|
+
normalized_rows.append(list(row.get("field_ids", [])))
|
|
3067
|
+
normalized_sections = []
|
|
3068
|
+
if isinstance(sections, list):
|
|
3069
|
+
for section in sections:
|
|
3070
|
+
if not isinstance(section, dict):
|
|
3071
|
+
continue
|
|
3072
|
+
normalized_sections.append(
|
|
3073
|
+
{
|
|
3074
|
+
"section_id": section.get("section_id"),
|
|
3075
|
+
"rows": [
|
|
3076
|
+
list(row.get("field_ids", []))
|
|
3077
|
+
for row in section.get("rows", [])
|
|
3078
|
+
if isinstance(row, dict)
|
|
3079
|
+
],
|
|
3080
|
+
}
|
|
3081
|
+
)
|
|
3082
|
+
return {"rows": normalized_rows, "sections": normalized_sections}
|
|
3083
|
+
|
|
3084
|
+
|
|
3085
|
+
def _looks_like_full_solution_spec(payload: dict[str, Any]) -> bool:
|
|
3086
|
+
if not isinstance(payload, dict):
|
|
3087
|
+
return False
|
|
3088
|
+
if not isinstance(payload.get("entities"), list):
|
|
3089
|
+
return False
|
|
3090
|
+
return any(
|
|
3091
|
+
key in payload
|
|
3092
|
+
for key in (
|
|
3093
|
+
"portal",
|
|
3094
|
+
"navigation",
|
|
3095
|
+
"requirements",
|
|
3096
|
+
"success_metrics",
|
|
3097
|
+
"roles",
|
|
3098
|
+
"package",
|
|
3099
|
+
"business_context",
|
|
3100
|
+
)
|
|
3101
|
+
)
|
|
3102
|
+
|
|
3103
|
+
|
|
3104
|
+
def _project_stage_payload(stage_name: str, solution_spec: dict[str, Any]) -> dict[str, Any]:
|
|
3105
|
+
if stage_name == "app":
|
|
3106
|
+
entities = deepcopy(solution_spec.get("entities", []))
|
|
3107
|
+
for entity in entities:
|
|
3108
|
+
if not isinstance(entity, dict):
|
|
3109
|
+
continue
|
|
3110
|
+
entity.pop("workflow", None)
|
|
3111
|
+
entity.pop("lifecycle_stages", None)
|
|
3112
|
+
entity.pop("views", None)
|
|
3113
|
+
entity.pop("charts", None)
|
|
3114
|
+
projected = {
|
|
3115
|
+
"solution_name": solution_spec.get("solution_name"),
|
|
3116
|
+
"summary": solution_spec.get("summary"),
|
|
3117
|
+
"business_context": deepcopy(solution_spec.get("business_context", {})),
|
|
3118
|
+
"package": deepcopy(solution_spec.get("package", {})),
|
|
3119
|
+
"entities": entities,
|
|
3120
|
+
"roles": deepcopy(solution_spec.get("roles", [])),
|
|
3121
|
+
"publish_policy": deepcopy(solution_spec.get("publish_policy", {})),
|
|
3122
|
+
"preferences": deepcopy(solution_spec.get("preferences", {})),
|
|
3123
|
+
"assumptions": deepcopy(solution_spec.get("assumptions", [])),
|
|
3124
|
+
"constraints": deepcopy(solution_spec.get("constraints", [])),
|
|
3125
|
+
"metadata": deepcopy(solution_spec.get("metadata", {})),
|
|
3126
|
+
}
|
|
3127
|
+
return {key: value for key, value in projected.items() if value is not None}
|
|
3128
|
+
|
|
3129
|
+
if stage_name == "flow":
|
|
3130
|
+
projected_entities: list[dict[str, Any]] = []
|
|
3131
|
+
for entity in solution_spec.get("entities", []):
|
|
3132
|
+
if not isinstance(entity, dict):
|
|
3133
|
+
continue
|
|
3134
|
+
flow_entity = {"entity_id": entity.get("entity_id")}
|
|
3135
|
+
for key in ("workflow", "lifecycle_stages", "status_field_id", "owner_field_id", "start_field_id", "end_field_id", "metadata"):
|
|
3136
|
+
if key in entity:
|
|
3137
|
+
flow_entity[key] = deepcopy(entity[key])
|
|
3138
|
+
projected_entities.append(flow_entity)
|
|
3139
|
+
return {
|
|
3140
|
+
"solution_name": solution_spec.get("solution_name"),
|
|
3141
|
+
"entities": projected_entities,
|
|
3142
|
+
"roles": deepcopy(solution_spec.get("roles", [])),
|
|
3143
|
+
"publish_policy": deepcopy(solution_spec.get("publish_policy", {})),
|
|
3144
|
+
"metadata": deepcopy(solution_spec.get("metadata", {})),
|
|
3145
|
+
}
|
|
3146
|
+
|
|
3147
|
+
if stage_name == "app_flow":
|
|
3148
|
+
entities = deepcopy(solution_spec.get("entities", []))
|
|
3149
|
+
for entity in entities:
|
|
3150
|
+
if not isinstance(entity, dict):
|
|
3151
|
+
continue
|
|
3152
|
+
entity.pop("views", None)
|
|
3153
|
+
entity.pop("charts", None)
|
|
3154
|
+
projected = {
|
|
3155
|
+
"solution_name": solution_spec.get("solution_name"),
|
|
3156
|
+
"summary": solution_spec.get("summary"),
|
|
3157
|
+
"business_context": deepcopy(solution_spec.get("business_context", {})),
|
|
3158
|
+
"package": deepcopy(solution_spec.get("package", {})),
|
|
3159
|
+
"entities": entities,
|
|
3160
|
+
"roles": deepcopy(solution_spec.get("roles", [])),
|
|
3161
|
+
"publish_policy": deepcopy(solution_spec.get("publish_policy", {})),
|
|
3162
|
+
"preferences": deepcopy(solution_spec.get("preferences", {})),
|
|
3163
|
+
"assumptions": deepcopy(solution_spec.get("assumptions", [])),
|
|
3164
|
+
"constraints": deepcopy(solution_spec.get("constraints", [])),
|
|
3165
|
+
"metadata": deepcopy(solution_spec.get("metadata", {})),
|
|
3166
|
+
}
|
|
3167
|
+
return {key: value for key, value in projected.items() if value is not None}
|
|
3168
|
+
|
|
3169
|
+
if stage_name == "views":
|
|
3170
|
+
return {
|
|
3171
|
+
"solution_name": solution_spec.get("solution_name"),
|
|
3172
|
+
"entities": [
|
|
3173
|
+
{
|
|
3174
|
+
"entity_id": entity.get("entity_id"),
|
|
3175
|
+
"views": deepcopy(entity.get("views", [])),
|
|
3176
|
+
}
|
|
3177
|
+
for entity in solution_spec.get("entities", [])
|
|
3178
|
+
if isinstance(entity, dict) and entity.get("views")
|
|
3179
|
+
],
|
|
3180
|
+
"metadata": deepcopy(solution_spec.get("metadata", {})),
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
if stage_name == "analytics_portal":
|
|
3184
|
+
return {
|
|
3185
|
+
"solution_name": solution_spec.get("solution_name"),
|
|
3186
|
+
"entities": [
|
|
3187
|
+
{
|
|
3188
|
+
"entity_id": entity.get("entity_id"),
|
|
3189
|
+
"charts": deepcopy(entity.get("charts", [])),
|
|
3190
|
+
}
|
|
3191
|
+
for entity in solution_spec.get("entities", [])
|
|
3192
|
+
if isinstance(entity, dict) and entity.get("charts")
|
|
3193
|
+
],
|
|
3194
|
+
"portal": deepcopy(solution_spec.get("portal", {})),
|
|
3195
|
+
"publish_policy": deepcopy(solution_spec.get("publish_policy", {})),
|
|
3196
|
+
"metadata": deepcopy(solution_spec.get("metadata", {})),
|
|
3197
|
+
}
|
|
3198
|
+
|
|
3199
|
+
if stage_name == "navigation":
|
|
3200
|
+
return {
|
|
3201
|
+
"solution_name": solution_spec.get("solution_name"),
|
|
3202
|
+
"navigation": deepcopy(solution_spec.get("navigation", {})),
|
|
3203
|
+
"publish_policy": deepcopy(solution_spec.get("publish_policy", {})),
|
|
3204
|
+
"metadata": deepcopy(solution_spec.get("metadata", {})),
|
|
3205
|
+
}
|
|
3206
|
+
raise ValueError(f"unsupported stage '{stage_name}'")
|
|
3207
|
+
|
|
3208
|
+
|
|
3209
|
+
def _split_solution_spec_into_stage_payloads(solution_spec: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
|
3210
|
+
return {stage_name: _project_stage_payload(stage_name, solution_spec) for stage_name in STAGE_ORDER}
|
|
3211
|
+
|
|
3212
|
+
|
|
3213
|
+
def _stage_payload_is_empty(stage_name: str, stage_payload: dict[str, Any]) -> bool:
|
|
3214
|
+
if stage_name == "app_flow":
|
|
3215
|
+
return not bool(stage_payload.get("entities"))
|
|
3216
|
+
if stage_name == "views":
|
|
3217
|
+
return not any(
|
|
3218
|
+
isinstance(entity, dict) and entity.get("views")
|
|
3219
|
+
for entity in stage_payload.get("entities", [])
|
|
3220
|
+
)
|
|
3221
|
+
if stage_name == "analytics_portal":
|
|
3222
|
+
has_charts = any(
|
|
3223
|
+
isinstance(entity, dict) and entity.get("charts")
|
|
3224
|
+
for entity in stage_payload.get("entities", [])
|
|
3225
|
+
)
|
|
3226
|
+
portal_payload = stage_payload.get("portal", {}) if isinstance(stage_payload.get("portal"), dict) else {}
|
|
3227
|
+
has_portal = bool(portal_payload.get("sections")) and portal_payload.get("enabled", True) is not False
|
|
3228
|
+
return not has_charts and not has_portal
|
|
3229
|
+
if stage_name == "navigation":
|
|
3230
|
+
navigation_payload = stage_payload.get("navigation", {}) if isinstance(stage_payload.get("navigation"), dict) else {}
|
|
3231
|
+
return not (navigation_payload.get("enabled", True) and navigation_payload.get("items"))
|
|
3232
|
+
raise ValueError(f"unsupported stage '{stage_name}'")
|
|
3233
|
+
|
|
3234
|
+
|
|
3235
|
+
def _stage_skip_reason(stage_name: str) -> str:
|
|
3236
|
+
reasons = {
|
|
3237
|
+
"app_flow": "No app flow entities were provided for this build stage.",
|
|
3238
|
+
"views": "No view definitions were provided, so the views stage was skipped.",
|
|
3239
|
+
"analytics_portal": "No charts or enabled portal sections were provided, so the analytics stage was skipped.",
|
|
3240
|
+
"navigation": "No enabled navigation items were provided, so the navigation stage was skipped.",
|
|
3241
|
+
}
|
|
3242
|
+
return reasons.get(stage_name, "Stage skipped because no applicable payload was provided.")
|
|
3243
|
+
|
|
3244
|
+
|
|
3245
|
+
def _append_verification_error(verification: dict[str, Any], scope: str, exc: Exception) -> None:
|
|
3246
|
+
errors = verification.setdefault("errors", [])
|
|
3247
|
+
if isinstance(errors, list):
|
|
3248
|
+
errors.append(
|
|
3249
|
+
{
|
|
3250
|
+
"scope": scope,
|
|
3251
|
+
"message": str(exc),
|
|
3252
|
+
}
|
|
3253
|
+
)
|
|
3254
|
+
|
|
3255
|
+
|
|
3256
|
+
PREFERRED_RECORD_TITLE_LABELS = (
|
|
3257
|
+
"名称",
|
|
3258
|
+
"客户名称",
|
|
3259
|
+
"医疗机构名称",
|
|
3260
|
+
"联系人姓名",
|
|
3261
|
+
"商机名称",
|
|
3262
|
+
"销售机会名称",
|
|
3263
|
+
"合同名称",
|
|
3264
|
+
"采购合同名称",
|
|
3265
|
+
"拜访主题",
|
|
3266
|
+
"主题",
|
|
3267
|
+
"项目名称",
|
|
3268
|
+
"任务名称",
|
|
3269
|
+
)
|
|
3270
|
+
|
|
3271
|
+
|
|
3272
|
+
def _readback_record_page(
|
|
3273
|
+
record_tools: RecordTools,
|
|
3274
|
+
profile: str,
|
|
3275
|
+
app_key: str,
|
|
3276
|
+
*,
|
|
3277
|
+
entity_spec: dict[str, Any] | None = None,
|
|
3278
|
+
field_meta: dict[str, Any] | None = None,
|
|
3279
|
+
) -> dict[str, Any]:
|
|
3280
|
+
select_columns = _readback_select_columns(entity_spec=entity_spec, field_meta=field_meta)
|
|
3281
|
+
fallback: dict[str, Any] = {}
|
|
3282
|
+
for list_type in (8, 9):
|
|
3283
|
+
response = record_tools.record_query(
|
|
3284
|
+
profile=profile,
|
|
3285
|
+
query_mode="list",
|
|
3286
|
+
app_key=app_key,
|
|
3287
|
+
apply_id=None,
|
|
3288
|
+
page_num=1,
|
|
3289
|
+
page_size=20,
|
|
3290
|
+
requested_pages=1,
|
|
3291
|
+
scan_max_pages=1,
|
|
3292
|
+
query_key=None,
|
|
3293
|
+
filters=[],
|
|
3294
|
+
sorts=[],
|
|
3295
|
+
max_rows=20,
|
|
3296
|
+
max_columns=max(1, len(select_columns)),
|
|
3297
|
+
select_columns=select_columns,
|
|
3298
|
+
amount_column=None,
|
|
3299
|
+
time_range={},
|
|
3300
|
+
stat_policy={},
|
|
3301
|
+
strict_full=False,
|
|
3302
|
+
output_profile="compact",
|
|
3303
|
+
list_type=list_type,
|
|
3304
|
+
view_key=None,
|
|
3305
|
+
view_name=None,
|
|
3306
|
+
)
|
|
3307
|
+
data = response.get("data") if isinstance(response, dict) else None
|
|
3308
|
+
list_payload = data.get("list") if isinstance(data, dict) and isinstance(data.get("list"), dict) else {}
|
|
3309
|
+
pagination = list_payload.get("pagination") if isinstance(list_payload.get("pagination"), dict) else {}
|
|
3310
|
+
rows = list_payload.get("rows") if isinstance(list_payload.get("rows"), list) else []
|
|
3311
|
+
merged_page = {
|
|
3312
|
+
"list": rows,
|
|
3313
|
+
"backend_reported_total": _coerce_count(pagination.get("backend_reported_total")),
|
|
3314
|
+
"returned_rows": _coerce_count(pagination.get("returned_items")) or len(rows),
|
|
3315
|
+
"effective_count": _coerce_count(pagination.get("result_amount")),
|
|
3316
|
+
"list_type": list_type,
|
|
3317
|
+
"list_type_label": get_record_list_type_label(list_type),
|
|
3318
|
+
"page_num": _coerce_count(pagination.get("page_num")) or 1,
|
|
3319
|
+
"page_size": _coerce_count(pagination.get("page_size")) or 20,
|
|
3320
|
+
}
|
|
3321
|
+
if not fallback and isinstance(merged_page, dict):
|
|
3322
|
+
fallback = merged_page
|
|
3323
|
+
if rows:
|
|
3324
|
+
return merged_page
|
|
3325
|
+
return fallback
|
|
3326
|
+
|
|
3327
|
+
|
|
3328
|
+
def _extract_record_titles(
|
|
3329
|
+
rows: list[Any],
|
|
3330
|
+
*,
|
|
3331
|
+
entity_spec: dict[str, Any] | None = None,
|
|
3332
|
+
field_meta: dict[str, Any] | None = None,
|
|
3333
|
+
) -> list[str]:
|
|
3334
|
+
preferred_titles = set(PREFERRED_RECORD_TITLE_LABELS)
|
|
3335
|
+
title_labels, title_que_ids = _title_field_hints(entity_spec=entity_spec, field_meta=field_meta)
|
|
3336
|
+
titles: list[str] = []
|
|
3337
|
+
for row in rows:
|
|
3338
|
+
if not isinstance(row, dict):
|
|
3339
|
+
continue
|
|
3340
|
+
answers = row.get("answers")
|
|
3341
|
+
selected: str | None = None
|
|
3342
|
+
fallback: str | None = None
|
|
3343
|
+
if isinstance(answers, list):
|
|
3344
|
+
for answer in answers:
|
|
3345
|
+
if not isinstance(answer, dict):
|
|
3346
|
+
continue
|
|
3347
|
+
normalized_values = _extract_answer_values(answer)
|
|
3348
|
+
if not normalized_values:
|
|
3349
|
+
continue
|
|
3350
|
+
candidate = " / ".join(normalized_values)
|
|
3351
|
+
answer_title = answer.get("queTitle") or answer.get("title")
|
|
3352
|
+
answer_que_id = _coerce_count(answer.get("queId"))
|
|
3353
|
+
if answer_que_id in title_que_ids or answer_title in title_labels:
|
|
3354
|
+
selected = candidate
|
|
3355
|
+
break
|
|
3356
|
+
if fallback is None and not _looks_like_secondary_answer(answer_title, candidate):
|
|
3357
|
+
fallback = candidate
|
|
3358
|
+
if answer_title in preferred_titles:
|
|
3359
|
+
selected = candidate
|
|
3360
|
+
break
|
|
3361
|
+
title = selected or fallback
|
|
3362
|
+
if title is None:
|
|
3363
|
+
for label in [*sorted(title_labels), *[item for item in PREFERRED_RECORD_TITLE_LABELS if item not in title_labels]]:
|
|
3364
|
+
candidate = _normalize_row_title(row.get(label))
|
|
3365
|
+
if candidate and not _looks_like_secondary_answer(label, candidate):
|
|
3366
|
+
title = candidate
|
|
3367
|
+
break
|
|
3368
|
+
if title is None:
|
|
3369
|
+
for key, value in row.items():
|
|
3370
|
+
if key in {"apply_id", "id"}:
|
|
3371
|
+
continue
|
|
3372
|
+
candidate = _normalize_row_title(value)
|
|
3373
|
+
if candidate and not _looks_like_secondary_answer(key, candidate):
|
|
3374
|
+
title = candidate
|
|
3375
|
+
break
|
|
3376
|
+
if title is None:
|
|
3377
|
+
for key in ("title", "name", "recordName", "applyName"):
|
|
3378
|
+
value = row.get(key)
|
|
3379
|
+
candidate = _normalize_row_title(value)
|
|
3380
|
+
if candidate:
|
|
3381
|
+
title = candidate
|
|
3382
|
+
break
|
|
3383
|
+
if title:
|
|
3384
|
+
titles.append(title)
|
|
3385
|
+
return titles[:5]
|
|
3386
|
+
|
|
3387
|
+
|
|
3388
|
+
def _readback_select_columns(
|
|
3389
|
+
*,
|
|
3390
|
+
entity_spec: dict[str, Any] | None,
|
|
3391
|
+
field_meta: dict[str, Any] | None,
|
|
3392
|
+
) -> list[str]:
|
|
3393
|
+
title_labels, _ = _title_field_hints(entity_spec=entity_spec, field_meta=field_meta)
|
|
3394
|
+
by_label = field_meta.get("by_label", {}) if isinstance(field_meta, dict) and isinstance(field_meta.get("by_label"), dict) else {}
|
|
3395
|
+
selected: list[str] = []
|
|
3396
|
+
|
|
3397
|
+
def append(label: Any) -> None:
|
|
3398
|
+
if not isinstance(label, str) or not label or label in selected:
|
|
3399
|
+
return
|
|
3400
|
+
if by_label and label not in by_label:
|
|
3401
|
+
return
|
|
3402
|
+
selected.append(label)
|
|
3403
|
+
|
|
3404
|
+
for label in sorted(title_labels):
|
|
3405
|
+
append(label)
|
|
3406
|
+
for label in PREFERRED_RECORD_TITLE_LABELS:
|
|
3407
|
+
append(label)
|
|
3408
|
+
if isinstance(by_label, dict):
|
|
3409
|
+
for label in by_label:
|
|
3410
|
+
append(label)
|
|
3411
|
+
if len(selected) >= 5:
|
|
3412
|
+
break
|
|
3413
|
+
if not selected and isinstance(entity_spec, dict):
|
|
3414
|
+
for field in entity_spec.get("fields", []):
|
|
3415
|
+
if not isinstance(field, dict):
|
|
3416
|
+
continue
|
|
3417
|
+
append(field.get("label"))
|
|
3418
|
+
if len(selected) >= 5:
|
|
3419
|
+
break
|
|
3420
|
+
return selected[:5] or ["名称"]
|
|
3421
|
+
|
|
3422
|
+
|
|
3423
|
+
def _title_field_hints(
|
|
3424
|
+
*,
|
|
3425
|
+
entity_spec: dict[str, Any] | None,
|
|
3426
|
+
field_meta: dict[str, Any] | None,
|
|
3427
|
+
) -> tuple[set[str], set[int]]:
|
|
3428
|
+
title_labels: set[str] = set()
|
|
3429
|
+
title_que_ids: set[int] = set()
|
|
3430
|
+
if not isinstance(entity_spec, dict):
|
|
3431
|
+
return title_labels, title_que_ids
|
|
3432
|
+
title_field_candidates = {str(entity_spec.get("title_field_id") or "title")}
|
|
3433
|
+
for field in entity_spec.get("fields", []):
|
|
3434
|
+
if not isinstance(field, dict):
|
|
3435
|
+
continue
|
|
3436
|
+
field_id = field.get("field_id")
|
|
3437
|
+
field_label = field.get("label")
|
|
3438
|
+
field_config = field.get("config", {}) if isinstance(field.get("config"), dict) else {}
|
|
3439
|
+
if field_id in title_field_candidates or field_config.get("role") == "title":
|
|
3440
|
+
if isinstance(field_label, str) and field_label:
|
|
3441
|
+
title_labels.add(field_label)
|
|
3442
|
+
if isinstance(field_id, str):
|
|
3443
|
+
title_field_candidates.add(field_id)
|
|
3444
|
+
if isinstance(field_meta, dict):
|
|
3445
|
+
by_field_id = field_meta.get("by_field_id", {})
|
|
3446
|
+
if isinstance(by_field_id, dict):
|
|
3447
|
+
for field_id in title_field_candidates:
|
|
3448
|
+
que_id = _coerce_count(by_field_id.get(field_id))
|
|
3449
|
+
if que_id is not None:
|
|
3450
|
+
title_que_ids.add(que_id)
|
|
3451
|
+
return title_labels, title_que_ids
|
|
3452
|
+
|
|
3453
|
+
|
|
3454
|
+
def _extract_answer_values(answer: dict[str, Any]) -> list[str]:
|
|
3455
|
+
values = answer.get("values")
|
|
3456
|
+
normalized_values: list[str] = []
|
|
3457
|
+
if isinstance(values, list):
|
|
3458
|
+
for value in values:
|
|
3459
|
+
if isinstance(value, str) and value:
|
|
3460
|
+
normalized_values.append(value)
|
|
3461
|
+
continue
|
|
3462
|
+
if isinstance(value, dict):
|
|
3463
|
+
for key in ("value", "name", "text", "label"):
|
|
3464
|
+
item = value.get(key)
|
|
3465
|
+
if isinstance(item, str) and item:
|
|
3466
|
+
normalized_values.append(item)
|
|
3467
|
+
break
|
|
3468
|
+
elif isinstance(values, str) and values:
|
|
3469
|
+
normalized_values.append(values)
|
|
3470
|
+
return normalized_values
|
|
3471
|
+
|
|
3472
|
+
|
|
3473
|
+
def _normalize_row_title(value: Any) -> str | None:
|
|
3474
|
+
if isinstance(value, str):
|
|
3475
|
+
text = value.strip()
|
|
3476
|
+
return text or None
|
|
3477
|
+
if isinstance(value, (int, float)) and not isinstance(value, bool):
|
|
3478
|
+
return str(value)
|
|
3479
|
+
if isinstance(value, list):
|
|
3480
|
+
parts = [_normalize_row_title(item) for item in value]
|
|
3481
|
+
normalized = [part for part in parts if isinstance(part, str) and part]
|
|
3482
|
+
return " / ".join(normalized) if normalized else None
|
|
3483
|
+
if isinstance(value, dict):
|
|
3484
|
+
for key in ("value", "name", "text", "label"):
|
|
3485
|
+
candidate = _normalize_row_title(value.get(key))
|
|
3486
|
+
if candidate:
|
|
3487
|
+
return candidate
|
|
3488
|
+
return None
|
|
3489
|
+
|
|
3490
|
+
|
|
3491
|
+
def _looks_like_secondary_answer(answer_title: Any, candidate: str) -> bool:
|
|
3492
|
+
if not isinstance(candidate, str) or not candidate:
|
|
3493
|
+
return True
|
|
3494
|
+
title = str(answer_title or "")
|
|
3495
|
+
secondary_markers = ("状态", "日期", "时间", "负责人", "处理人", "优先级", "金额", "费用", "电话", "类型")
|
|
3496
|
+
if any(marker in title for marker in secondary_markers):
|
|
3497
|
+
return True
|
|
3498
|
+
if len(candidate) >= 10 and candidate[4] == "-" and candidate[7] == "-":
|
|
3499
|
+
return True
|
|
3500
|
+
return False
|
|
3501
|
+
|
|
3502
|
+
|
|
3503
|
+
def _count_portal_components(payload: dict[str, Any]) -> int | None:
|
|
3504
|
+
if not isinstance(payload, dict):
|
|
3505
|
+
return None
|
|
3506
|
+
for key in ("components", "sections", "dashItemList"):
|
|
3507
|
+
items = payload.get(key)
|
|
3508
|
+
if isinstance(items, list):
|
|
3509
|
+
return len(items)
|
|
3510
|
+
return None
|
|
3511
|
+
|
|
3512
|
+
|
|
3513
|
+
def _extract_navigation_items(items: list[Any]) -> list[dict[str, Any]]:
|
|
3514
|
+
extracted: list[dict[str, Any]] = []
|
|
3515
|
+
for item in items:
|
|
3516
|
+
if not isinstance(item, dict):
|
|
3517
|
+
continue
|
|
3518
|
+
result = item.get("result") if isinstance(item.get("result"), dict) else {}
|
|
3519
|
+
result_payload = result.get("result") if isinstance(result.get("result"), dict) else {}
|
|
3520
|
+
summarized = {
|
|
3521
|
+
"item_id": item.get("item_id"),
|
|
3522
|
+
"navigation_item_id": result_payload.get("navigationItemId") or result_payload.get("id"),
|
|
3523
|
+
"title": result_payload.get("navigationItemName") or result_payload.get("title"),
|
|
3524
|
+
}
|
|
3525
|
+
extracted.append({key: value for key, value in summarized.items() if value is not None})
|
|
3526
|
+
return extracted
|
|
3527
|
+
|
|
3528
|
+
|
|
3529
|
+
def _resolve_app_stage_views_strategy(stage_result: dict[str, Any]) -> str:
|
|
3530
|
+
verification = stage_result.get("verification")
|
|
3531
|
+
if isinstance(verification, dict):
|
|
3532
|
+
strategy = verification.get("views_strategy")
|
|
3533
|
+
if isinstance(strategy, str) and strategy:
|
|
3534
|
+
return strategy
|
|
3535
|
+
return "not_included"
|
|
3536
|
+
|
|
3537
|
+
|
|
3538
|
+
def _resolve_target_package_tag_id(target: dict[str, Any]) -> int | None:
|
|
3539
|
+
if not target:
|
|
3540
|
+
return None
|
|
3541
|
+
package_tag_id = target.get("package_tag_id")
|
|
3542
|
+
if package_tag_id is None and isinstance(target.get("package"), dict):
|
|
3543
|
+
package_tag_id = target["package"].get("tag_id")
|
|
3544
|
+
if package_tag_id is None:
|
|
3545
|
+
return None
|
|
3546
|
+
if not isinstance(package_tag_id, int) or package_tag_id <= 0:
|
|
3547
|
+
raise ValueError("target.package_tag_id must be a positive integer")
|
|
3548
|
+
return package_tag_id
|
|
3549
|
+
|
|
3550
|
+
|
|
3551
|
+
def _bind_existing_package_target(compiled: CompiledSolution, package_tag_id: int) -> CompiledSolution:
|
|
3552
|
+
compiled.package_payload = None
|
|
3553
|
+
compiled.execution_plan = build_execution_plan(compiled.normalized_spec, include_package=False, attach_package=True)
|
|
3554
|
+
for entity in compiled.entities:
|
|
3555
|
+
entity.app_create_payload["tagIds"] = [package_tag_id]
|
|
3556
|
+
return compiled
|
|
3557
|
+
|
|
3558
|
+
|
|
3559
|
+
def _bind_existing_app_target(compiled: CompiledSolution) -> CompiledSolution:
|
|
3560
|
+
compiled.package_payload = None
|
|
3561
|
+
compiled.execution_plan = build_execution_plan(compiled.normalized_spec, include_package=False, attach_package=False)
|
|
3562
|
+
return compiled
|
|
3563
|
+
|
|
3564
|
+
|
|
3565
|
+
def _merge_stage_manifest(*, stage_name: str, manifest: dict[str, Any], stage_payload: dict[str, Any]) -> dict[str, Any]:
|
|
3566
|
+
merged = deepcopy(manifest or default_manifest())
|
|
3567
|
+
if stage_name == "app_flow":
|
|
3568
|
+
return _merge_app_flow_manifest(merged, stage_payload)
|
|
3569
|
+
if stage_name == "views":
|
|
3570
|
+
return _merge_views_manifest(merged, stage_payload)
|
|
3571
|
+
if stage_name == "analytics_portal":
|
|
3572
|
+
return _merge_analytics_portal_manifest(merged, stage_payload)
|
|
3573
|
+
if stage_name == "navigation":
|
|
3574
|
+
return _merge_navigation_manifest(merged, stage_payload)
|
|
3575
|
+
raise ValueError(f"unsupported stage '{stage_name}'")
|
|
3576
|
+
|
|
3577
|
+
|
|
3578
|
+
def _merge_app_flow_manifest(manifest: dict[str, Any], stage_payload: dict[str, Any]) -> dict[str, Any]:
|
|
3579
|
+
entity_map = {entity["entity_id"]: deepcopy(entity) for entity in manifest.get("entities", [])}
|
|
3580
|
+
merged_entities: list[dict[str, Any]] = []
|
|
3581
|
+
staged_entity_ids: set[str] = set()
|
|
3582
|
+
for entity in stage_payload.get("entities", []):
|
|
3583
|
+
existing = entity_map.get(entity["entity_id"], {})
|
|
3584
|
+
next_entity = deepcopy(existing)
|
|
3585
|
+
next_entity.update(deepcopy(entity))
|
|
3586
|
+
next_entity["views"] = deepcopy(existing.get("views", []))
|
|
3587
|
+
next_entity["charts"] = deepcopy(existing.get("charts", []))
|
|
3588
|
+
merged_entities.append(next_entity)
|
|
3589
|
+
staged_entity_ids.add(entity["entity_id"])
|
|
3590
|
+
for entity_id, entity in entity_map.items():
|
|
3591
|
+
if entity_id not in staged_entity_ids:
|
|
3592
|
+
merged_entities.append(deepcopy(entity))
|
|
3593
|
+
manifest["solution_name"] = stage_payload.get("solution_name", manifest.get("solution_name"))
|
|
3594
|
+
manifest["summary"] = stage_payload.get("summary", manifest.get("summary"))
|
|
3595
|
+
manifest["business_context"] = deepcopy(stage_payload.get("business_context", manifest.get("business_context", {})))
|
|
3596
|
+
manifest["package"] = deepcopy(_merge_dict(manifest.get("package", {}), stage_payload.get("package", {})))
|
|
3597
|
+
manifest["entities"] = merged_entities
|
|
3598
|
+
if "roles" in stage_payload:
|
|
3599
|
+
manifest["roles"] = deepcopy(stage_payload["roles"])
|
|
3600
|
+
manifest["publish_policy"] = deepcopy(_merge_dict(manifest.get("publish_policy", {}), stage_payload.get("publish_policy", {})))
|
|
3601
|
+
manifest["preferences"] = deepcopy(_merge_dict(manifest.get("preferences", {}), stage_payload.get("preferences", {})))
|
|
3602
|
+
if "assumptions" in stage_payload:
|
|
3603
|
+
manifest["assumptions"] = deepcopy(stage_payload["assumptions"])
|
|
3604
|
+
if "constraints" in stage_payload:
|
|
3605
|
+
manifest["constraints"] = deepcopy(stage_payload["constraints"])
|
|
3606
|
+
manifest["metadata"] = deepcopy(_merge_dict(manifest.get("metadata", {}), stage_payload.get("metadata", {})))
|
|
3607
|
+
return manifest
|
|
3608
|
+
|
|
3609
|
+
|
|
3610
|
+
def _merge_views_manifest(manifest: dict[str, Any], stage_payload: dict[str, Any]) -> dict[str, Any]:
|
|
3611
|
+
entity_map = {entity["entity_id"]: deepcopy(entity) for entity in manifest.get("entities", [])}
|
|
3612
|
+
for entity_patch in stage_payload.get("entities", []):
|
|
3613
|
+
entity_id = entity_patch["entity_id"]
|
|
3614
|
+
if entity_id not in entity_map:
|
|
3615
|
+
raise ValueError(f"views stage references unknown entity '{entity_id}'")
|
|
3616
|
+
entity_map[entity_id]["views"] = deepcopy(entity_patch.get("views", []))
|
|
3617
|
+
manifest["entities"] = list(entity_map.values())
|
|
3618
|
+
manifest["metadata"] = deepcopy(_merge_dict(manifest.get("metadata", {}), stage_payload.get("metadata", {})))
|
|
3619
|
+
return manifest
|
|
3620
|
+
|
|
3621
|
+
|
|
3622
|
+
def _merge_analytics_portal_manifest(manifest: dict[str, Any], stage_payload: dict[str, Any]) -> dict[str, Any]:
|
|
3623
|
+
entity_map = {entity["entity_id"]: deepcopy(entity) for entity in manifest.get("entities", [])}
|
|
3624
|
+
for entity_patch in stage_payload.get("entities", []):
|
|
3625
|
+
entity_id = entity_patch["entity_id"]
|
|
3626
|
+
if entity_id not in entity_map:
|
|
3627
|
+
raise ValueError(f"analytics stage references unknown entity '{entity_id}'")
|
|
3628
|
+
entity_map[entity_id]["charts"] = deepcopy(entity_patch.get("charts", []))
|
|
3629
|
+
manifest["entities"] = list(entity_map.values())
|
|
3630
|
+
if "portal" in stage_payload:
|
|
3631
|
+
portal_payload = deepcopy(stage_payload["portal"])
|
|
3632
|
+
if portal_payload.get("sections"):
|
|
3633
|
+
manifest["portal"] = deepcopy(_merge_dict(manifest.get("portal", {}), portal_payload))
|
|
3634
|
+
manifest["portal"]["enabled"] = portal_payload.get("enabled", True)
|
|
3635
|
+
manifest.setdefault("preferences", {})
|
|
3636
|
+
manifest["preferences"]["create_portal"] = bool(portal_payload.get("enabled", True))
|
|
3637
|
+
elif portal_payload.get("enabled") is False:
|
|
3638
|
+
manifest["portal"] = deepcopy(_merge_dict(manifest.get("portal", {}), portal_payload))
|
|
3639
|
+
manifest.setdefault("preferences", {})
|
|
3640
|
+
manifest["preferences"]["create_portal"] = False
|
|
3641
|
+
if "publish_policy" in stage_payload:
|
|
3642
|
+
manifest["publish_policy"] = deepcopy(_merge_dict(manifest.get("publish_policy", {}), stage_payload["publish_policy"]))
|
|
3643
|
+
manifest["metadata"] = deepcopy(_merge_dict(manifest.get("metadata", {}), stage_payload.get("metadata", {})))
|
|
3644
|
+
return manifest
|
|
3645
|
+
|
|
3646
|
+
|
|
3647
|
+
def _merge_navigation_manifest(manifest: dict[str, Any], stage_payload: dict[str, Any]) -> dict[str, Any]:
|
|
3648
|
+
navigation_payload = deepcopy(stage_payload.get("navigation", {}))
|
|
3649
|
+
manifest["navigation"] = deepcopy(_merge_dict(manifest.get("navigation", {}), navigation_payload))
|
|
3650
|
+
if navigation_payload.get("items"):
|
|
3651
|
+
manifest["navigation"]["enabled"] = navigation_payload.get("enabled", True)
|
|
3652
|
+
manifest.setdefault("preferences", {})
|
|
3653
|
+
manifest["preferences"]["create_navigation"] = bool(navigation_payload.get("enabled", True))
|
|
3654
|
+
if "publish_policy" in stage_payload:
|
|
3655
|
+
manifest["publish_policy"] = deepcopy(_merge_dict(manifest.get("publish_policy", {}), stage_payload["publish_policy"]))
|
|
3656
|
+
manifest["metadata"] = deepcopy(_merge_dict(manifest.get("metadata", {}), stage_payload.get("metadata", {})))
|
|
3657
|
+
return manifest
|
|
3658
|
+
|
|
3659
|
+
|
|
3660
|
+
def _filter_compiled_solution(
|
|
3661
|
+
compiled: CompiledSolution,
|
|
3662
|
+
*,
|
|
3663
|
+
stage_name: str,
|
|
3664
|
+
stage_payload: dict[str, Any],
|
|
3665
|
+
stage_variant: str | None = None,
|
|
3666
|
+
) -> CompiledSolution:
|
|
3667
|
+
included_step_names = _stage_step_names(
|
|
3668
|
+
compiled.execution_plan,
|
|
3669
|
+
stage_name=stage_name,
|
|
3670
|
+
stage_payload=stage_payload,
|
|
3671
|
+
stage_variant=stage_variant,
|
|
3672
|
+
)
|
|
3673
|
+
allowed = set(included_step_names)
|
|
3674
|
+
filtered_steps = [
|
|
3675
|
+
ExecutionStep(
|
|
3676
|
+
step_name=step.step_name,
|
|
3677
|
+
resource_type=step.resource_type,
|
|
3678
|
+
resource_ref=step.resource_ref,
|
|
3679
|
+
description=step.description,
|
|
3680
|
+
depends_on=[dependency for dependency in step.depends_on if dependency in allowed],
|
|
3681
|
+
)
|
|
3682
|
+
for step in compiled.execution_plan.steps
|
|
3683
|
+
if step.step_name in allowed
|
|
3684
|
+
]
|
|
3685
|
+
filtered = deepcopy(compiled)
|
|
3686
|
+
filtered.execution_plan = ExecutionPlan(steps=filtered_steps)
|
|
3687
|
+
return filtered
|
|
3688
|
+
|
|
3689
|
+
|
|
3690
|
+
def _stage_step_names(
|
|
3691
|
+
plan: ExecutionPlan,
|
|
3692
|
+
*,
|
|
3693
|
+
stage_name: str,
|
|
3694
|
+
stage_payload: dict[str, Any],
|
|
3695
|
+
stage_variant: str | None = None,
|
|
3696
|
+
) -> list[str]:
|
|
3697
|
+
variant = stage_variant or stage_name
|
|
3698
|
+
if variant == "navigation":
|
|
3699
|
+
return [step.step_name for step in plan.steps if step.step_name in {"navigation.create", "publish.navigation"}]
|
|
3700
|
+
|
|
3701
|
+
entity_ids = {
|
|
3702
|
+
entity["entity_id"]
|
|
3703
|
+
for entity in stage_payload.get("entities", [])
|
|
3704
|
+
if isinstance(entity, dict) and isinstance(entity.get("entity_id"), str)
|
|
3705
|
+
}
|
|
3706
|
+
included: list[str] = []
|
|
3707
|
+
for step in plan.steps:
|
|
3708
|
+
if variant == "app":
|
|
3709
|
+
if (
|
|
3710
|
+
step.step_name == "package.create"
|
|
3711
|
+
or step.step_name.startswith("role.create.")
|
|
3712
|
+
or any(
|
|
3713
|
+
step.step_name.startswith(prefix)
|
|
3714
|
+
for prefix in (
|
|
3715
|
+
"app.create.",
|
|
3716
|
+
"package.attach.",
|
|
3717
|
+
"form.base.",
|
|
3718
|
+
"form.relations.",
|
|
3719
|
+
"publish.form.",
|
|
3720
|
+
"publish.app.",
|
|
3721
|
+
"seed_data.",
|
|
3722
|
+
)
|
|
3723
|
+
)
|
|
3724
|
+
):
|
|
3725
|
+
if "." in step.step_name and step.resource_ref not in entity_ids and not step.step_name.startswith("role.create.") and step.step_name != "package.create":
|
|
3726
|
+
continue
|
|
3727
|
+
included.append(step.step_name)
|
|
3728
|
+
continue
|
|
3729
|
+
if variant == "flow":
|
|
3730
|
+
if (
|
|
3731
|
+
step.step_name.startswith("role.create.")
|
|
3732
|
+
or step.step_name.startswith("workflow.")
|
|
3733
|
+
or step.step_name.startswith("publish.workflow.")
|
|
3734
|
+
or step.step_name.startswith("publish.app.")
|
|
3735
|
+
):
|
|
3736
|
+
if "." in step.step_name and step.resource_ref not in entity_ids and not step.step_name.startswith("role.create."):
|
|
3737
|
+
continue
|
|
3738
|
+
included.append(step.step_name)
|
|
3739
|
+
continue
|
|
3740
|
+
if variant == "app_flow":
|
|
3741
|
+
if (
|
|
3742
|
+
step.step_name == "package.create"
|
|
3743
|
+
or step.step_name.startswith("role.create.")
|
|
3744
|
+
or any(
|
|
3745
|
+
step.step_name.startswith(prefix)
|
|
3746
|
+
for prefix in (
|
|
3747
|
+
"app.create.",
|
|
3748
|
+
"package.attach.",
|
|
3749
|
+
"form.base.",
|
|
3750
|
+
"form.relations.",
|
|
3751
|
+
"workflow.",
|
|
3752
|
+
"publish.form.",
|
|
3753
|
+
"publish.workflow.",
|
|
3754
|
+
"publish.app.",
|
|
3755
|
+
"seed_data.",
|
|
3756
|
+
)
|
|
3757
|
+
)
|
|
3758
|
+
):
|
|
3759
|
+
if "." in step.step_name and step.resource_ref not in entity_ids and not step.step_name.startswith("role.create.") and step.step_name != "package.create":
|
|
3760
|
+
continue
|
|
3761
|
+
included.append(step.step_name)
|
|
3762
|
+
continue
|
|
3763
|
+
if variant == "views":
|
|
3764
|
+
if step.resource_ref in entity_ids and (step.step_name.startswith("views.") or step.step_name.startswith("publish.app.")):
|
|
3765
|
+
included.append(step.step_name)
|
|
3766
|
+
continue
|
|
3767
|
+
if variant == "analytics_portal":
|
|
3768
|
+
if step.resource_ref in entity_ids and (step.step_name.startswith("charts.") or step.step_name.startswith("publish.app.")):
|
|
3769
|
+
included.append(step.step_name)
|
|
3770
|
+
continue
|
|
3771
|
+
if step.step_name in {"portal.create", "publish.portal"}:
|
|
3772
|
+
included.append(step.step_name)
|
|
3773
|
+
continue
|
|
3774
|
+
return included
|
|
3775
|
+
|
|
3776
|
+
|
|
3777
|
+
def _merge_dict(base: dict[str, Any], patch: dict[str, Any]) -> dict[str, Any]:
|
|
3778
|
+
merged = deepcopy(base)
|
|
3779
|
+
for key, value in patch.items():
|
|
3780
|
+
merged[key] = deepcopy(value)
|
|
3781
|
+
return merged
|
|
3782
|
+
|
|
3783
|
+
|
|
3784
|
+
def _stage_run_id(build_id: str, stage_name: str) -> str:
|
|
3785
|
+
return f"{build_id}--{stage_name}"
|
|
3786
|
+
|
|
3787
|
+
|
|
3788
|
+
def _build_status_for_stage(stage_name: str, result_status: str) -> str:
|
|
3789
|
+
if result_status != "success":
|
|
3790
|
+
return result_status
|
|
3791
|
+
return "success" if stage_name == "navigation" else "ready"
|
|
3792
|
+
|
|
3793
|
+
|
|
3794
|
+
def _build_summary(assembly: BuildAssemblyStore) -> dict[str, Any]:
|
|
3795
|
+
summary = assembly.summary()
|
|
3796
|
+
stage_specs = summary.get("stage_specs", {}) if isinstance(summary.get("stage_specs"), dict) else {}
|
|
3797
|
+
stage_history = summary.get("stage_history", []) if isinstance(summary.get("stage_history"), list) else []
|
|
3798
|
+
latest_entries: dict[str, dict[str, Any]] = {}
|
|
3799
|
+
for entry in stage_history:
|
|
3800
|
+
stage = entry.get("stage")
|
|
3801
|
+
if isinstance(stage, str):
|
|
3802
|
+
latest_entries[stage] = entry
|
|
3803
|
+
stage_statuses: dict[str, str] = {}
|
|
3804
|
+
for stage in STAGE_ORDER:
|
|
3805
|
+
latest = latest_entries.get(stage)
|
|
3806
|
+
if latest and latest.get("status"):
|
|
3807
|
+
stage_statuses[stage] = str(latest["status"])
|
|
3808
|
+
continue
|
|
3809
|
+
stage_statuses[stage] = "draft" if stage_specs.get(stage) else "pending"
|
|
3810
|
+
ready_stages = [stage for stage in STAGE_ORDER if not _missing_stage_prerequisites(stage_name=stage, assembly=assembly, stage_payload=stage_specs.get(stage, {}) or {})]
|
|
3811
|
+
completed_stages = [stage for stage, status in stage_statuses.items() if status in {"success", "ready", "skipped"}]
|
|
3812
|
+
next_stage = next((stage for stage in STAGE_ORDER if stage_statuses.get(stage) not in {"success", "ready", "skipped"}), None)
|
|
3813
|
+
prerequisites = {
|
|
3814
|
+
stage: _missing_stage_prerequisites(stage_name=stage, assembly=assembly, stage_payload=stage_specs.get(stage, {}) or {})
|
|
3815
|
+
for stage in STAGE_ORDER
|
|
3816
|
+
}
|
|
3817
|
+
return {
|
|
3818
|
+
"build_id": summary.get("build_id"),
|
|
3819
|
+
"status": summary.get("status"),
|
|
3820
|
+
"build_path": summary.get("build_path"),
|
|
3821
|
+
"stage_statuses": stage_statuses,
|
|
3822
|
+
"completed_stages": completed_stages,
|
|
3823
|
+
"ready_stages": ready_stages,
|
|
3824
|
+
"next_recommended_stage": next_stage,
|
|
3825
|
+
"missing_prerequisites": prerequisites,
|
|
3826
|
+
"stage_history": stage_history,
|
|
3827
|
+
}
|
|
3828
|
+
|
|
3829
|
+
|
|
3830
|
+
def _missing_stage_prerequisites(*, stage_name: str, assembly: BuildAssemblyStore, stage_payload: dict[str, Any]) -> list[str]:
|
|
3831
|
+
manifest = assembly.get_manifest()
|
|
3832
|
+
stage_specs = (assembly.data.get("stage_specs", {}) or {}) if isinstance(assembly.data.get("stage_specs"), dict) else {}
|
|
3833
|
+
entity_map = {
|
|
3834
|
+
entity.get("entity_id"): entity
|
|
3835
|
+
for entity in manifest.get("entities", [])
|
|
3836
|
+
if isinstance(entity, dict) and isinstance(entity.get("entity_id"), str)
|
|
3837
|
+
}
|
|
3838
|
+
missing: list[str] = []
|
|
3839
|
+
if stage_name in {"app_flow", "app"}:
|
|
3840
|
+
return missing
|
|
3841
|
+
if stage_name == "flow":
|
|
3842
|
+
if not entity_map and not stage_specs.get("app_flow"):
|
|
3843
|
+
return ["Run solution_build_app first to define package, apps, and fields before configuring workflow."]
|
|
3844
|
+
for entity in stage_payload.get("entities", []):
|
|
3845
|
+
entity_id = entity.get("entity_id")
|
|
3846
|
+
if entity_id not in entity_map:
|
|
3847
|
+
missing.append(f"Entity '{entity_id}' is not defined yet. Run solution_build_app first.")
|
|
3848
|
+
return list(dict.fromkeys(missing))
|
|
3849
|
+
if not entity_map and not stage_specs.get("app_flow"):
|
|
3850
|
+
return ["Run solution_build_app_flow first to define package, apps, and form/workflow entities."]
|
|
3851
|
+
if stage_name == "views":
|
|
3852
|
+
for entity in stage_payload.get("entities", []):
|
|
3853
|
+
entity_id = entity.get("entity_id")
|
|
3854
|
+
if entity_id not in entity_map:
|
|
3855
|
+
missing.append(f"Entity '{entity_id}' is not defined yet. Run solution_build_app_flow first.")
|
|
3856
|
+
return missing
|
|
3857
|
+
if stage_name == "analytics_portal":
|
|
3858
|
+
for entity in stage_payload.get("entities", []):
|
|
3859
|
+
entity_id = entity.get("entity_id")
|
|
3860
|
+
if entity_id not in entity_map:
|
|
3861
|
+
missing.append(f"Entity '{entity_id}' is not defined yet. Run solution_build_app_flow first.")
|
|
3862
|
+
portal_payload = stage_payload.get("portal", {}) if isinstance(stage_payload.get("portal"), dict) else {}
|
|
3863
|
+
for section in portal_payload.get("sections", []):
|
|
3864
|
+
if section.get("source_type") == "view":
|
|
3865
|
+
entity_id = section.get("entity_id")
|
|
3866
|
+
view_id = section.get("view_id")
|
|
3867
|
+
entity = entity_map.get(entity_id) if isinstance(entity_id, str) else None
|
|
3868
|
+
known_view_ids = {
|
|
3869
|
+
view.get("view_id")
|
|
3870
|
+
for view in (entity.get("views", []) if isinstance(entity, dict) else [])
|
|
3871
|
+
if isinstance(view, dict)
|
|
3872
|
+
}
|
|
3873
|
+
if view_id not in known_view_ids:
|
|
3874
|
+
missing.append(
|
|
3875
|
+
f"Portal section '{section.get('section_id')}' references view '{view_id}' on entity '{entity_id}'. Run solution_build_views first."
|
|
3876
|
+
)
|
|
3877
|
+
return list(dict.fromkeys(missing))
|
|
3878
|
+
if stage_name == "navigation":
|
|
3879
|
+
navigation_payload = stage_payload.get("navigation", {}) if isinstance(stage_payload.get("navigation"), dict) else {}
|
|
3880
|
+
package_targets = [
|
|
3881
|
+
item for item in navigation_payload.get("items", [])
|
|
3882
|
+
if isinstance(item, dict) and item.get("target_type") == "package"
|
|
3883
|
+
]
|
|
3884
|
+
if not package_targets:
|
|
3885
|
+
missing.append("Navigation stage must include a package target item as the single published navigation root.")
|
|
3886
|
+
return list(dict.fromkeys(missing))
|
|
3887
|
+
return missing
|