procesio-cli 0.1.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (647) hide show
  1. procesio_cli/__init__.py +18 -0
  2. procesio_cli/_cli.py +71 -0
  3. procesio_cli/agents/__init__.py +0 -0
  4. procesio_cli/agents/_lib/__init__.py +0 -0
  5. procesio_cli/agents/_lib/actiondef.py +24 -0
  6. procesio_cli/agents/_lib/errors.py +40 -0
  7. procesio_cli/agents/_lib/reversibility.py +236 -0
  8. procesio_cli/agents/_lib/toolrunner.py +141 -0
  9. procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-PLAYBOOK.md +62 -0
  10. procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-PROCESIO-INTEROP.md +76 -0
  11. procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-TROUBLESHOOTING.md +60 -0
  12. procesio_cli/agents/connector-builder/README.md +51 -0
  13. procesio_cli/agents/connector-builder/agent.yaml +61 -0
  14. procesio_cli/agents/connector-builder/context.py +19 -0
  15. procesio_cli/agents/connector-builder/handlers/__init__.py +1 -0
  16. procesio_cli/agents/connector-builder/handlers/checklist.py +71 -0
  17. procesio_cli/agents/connector-builder/handlers/guidance.py +37 -0
  18. procesio_cli/agents/connector-builder/handlers/nextstep.py +221 -0
  19. procesio_cli/agents/connector-builder/handlers/status.py +35 -0
  20. procesio_cli/agents/connector-builder/knowledge.py +47 -0
  21. procesio_cli/agents/connector-builder/main.py +93 -0
  22. procesio_cli/agents/connector-builder/tests/conftest.py +95 -0
  23. procesio_cli/agents/connector-builder/tests/test_cba_agent.py +148 -0
  24. procesio_cli/agents/orchestrator/__init__.py +0 -0
  25. procesio_cli/agents/orchestrator/bootstraplib.py +166 -0
  26. procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/00-INDEX.md +60 -0
  27. procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/01-PROCESSES.md +230 -0
  28. procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/02-FORMS.md +287 -0
  29. procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/03-INTEGRATIONS.md +384 -0
  30. procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/04-DATABASE.md +293 -0
  31. procesio_cli/agents/procesio/PROCESIO-API-RELIABILITY-DOCTRINE.md +113 -0
  32. procesio_cli/agents/procesio/PROCESIO-BEST-PRACTICES.md +316 -0
  33. procesio_cli/agents/procesio/PROCESIO-BUILD-AND-TEST-PLAYBOOK.md +217 -0
  34. procesio_cli/agents/procesio/PROCESIO-DATASTORE.md +72 -0
  35. procesio_cli/agents/procesio/PROCESIO-ENVIRONMENTS.md +62 -0
  36. procesio_cli/agents/procesio/PROCESIO-SCHEDULING.md +47 -0
  37. procesio_cli/agents/procesio/PROCESIO-VISUAL-ORGANIZATION.md +175 -0
  38. procesio_cli/agents/procesio/README.md +97 -0
  39. procesio_cli/agents/procesio/__init__.py +0 -0
  40. procesio_cli/agents/procesio/agent.yaml +73 -0
  41. procesio_cli/agents/procesio/audit.py +226 -0
  42. procesio_cli/agents/procesio/checklist.py +135 -0
  43. procesio_cli/agents/procesio/context.py +19 -0
  44. procesio_cli/agents/procesio/handlers/__init__.py +0 -0
  45. procesio_cli/agents/procesio/handlers/audit.py +39 -0
  46. procesio_cli/agents/procesio/handlers/checklist.py +34 -0
  47. procesio_cli/agents/procesio/handlers/guidance.py +36 -0
  48. procesio_cli/agents/procesio/handlers/status.py +33 -0
  49. procesio_cli/agents/procesio/handlers/verify.py +39 -0
  50. procesio_cli/agents/procesio/knowledge.py +115 -0
  51. procesio_cli/agents/procesio/main.py +89 -0
  52. procesio_cli/agents/procesio/tests/__init__.py +0 -0
  53. procesio_cli/agents/procesio/tests/conftest.py +62 -0
  54. procesio_cli/agents/procesio/tests/test_audit.py +91 -0
  55. procesio_cli/agents/procesio/tests/test_checklist.py +28 -0
  56. procesio_cli/agents/procesio/tests/test_guidance.py +78 -0
  57. procesio_cli/agents/procesio/tests/test_manifest_sync.py +48 -0
  58. procesio_cli/agents/procesio/tests/test_verify.py +134 -0
  59. procesio_cli/agents/procesio/tests/test_workspace_scope.py +97 -0
  60. procesio_cli/agents/procesio/verifylib.py +222 -0
  61. procesio_cli/dashboard/README.md +117 -0
  62. procesio_cli/dashboard/__init__.py +18 -0
  63. procesio_cli/dashboard/serve.py +98 -0
  64. procesio_cli/dashboard/server/__init__.py +1 -0
  65. procesio_cli/dashboard/server/config_schemas.py +92 -0
  66. procesio_cli/dashboard/server/google.py +103 -0
  67. procesio_cli/dashboard/server/handler.py +160 -0
  68. procesio_cli/dashboard/server/inventory.py +213 -0
  69. procesio_cli/dashboard/server/jobs.py +118 -0
  70. procesio_cli/dashboard/server/llmtest.py +181 -0
  71. procesio_cli/dashboard/server/procesio_env.py +94 -0
  72. procesio_cli/dashboard/server/routes.py +74 -0
  73. procesio_cli/dashboard/server/runner.py +150 -0
  74. procesio_cli/dashboard/server/security.py +29 -0
  75. procesio_cli/dashboard/server/setup.py +211 -0
  76. procesio_cli/dashboard/server/validate.py +152 -0
  77. procesio_cli/dashboard/tests/conftest.py +16 -0
  78. procesio_cli/dashboard/tests/test_dashboard.py +225 -0
  79. procesio_cli/dashboard/tests/test_procesio_env.py +159 -0
  80. procesio_cli/dashboard/web/actions.js +274 -0
  81. procesio_cli/dashboard/web/api.js +79 -0
  82. procesio_cli/dashboard/web/app.js +385 -0
  83. procesio_cli/dashboard/web/copilot.js +72 -0
  84. procesio_cli/dashboard/web/google.js +121 -0
  85. procesio_cli/dashboard/web/index.html +67 -0
  86. procesio_cli/dashboard/web/procesio_env.js +187 -0
  87. procesio_cli/dashboard/web/styles.css +207 -0
  88. procesio_cli/registry.py +370 -0
  89. procesio_cli/scripts/build-router.py +249 -0
  90. procesio_cli/scripts/build-tool-skill.py +202 -0
  91. procesio_cli/scripts/check-requirements.py +131 -0
  92. procesio_cli/scripts/check-skill-governance.py +174 -0
  93. procesio_cli/scripts/evaluate-skill-routing.py +217 -0
  94. procesio_cli/scripts/evaluate-skills.py +316 -0
  95. procesio_cli/scripts/get-skill.py +122 -0
  96. procesio_cli/scripts/list-skills.py +68 -0
  97. procesio_cli/scripts/list-tools.py +73 -0
  98. procesio_cli/scripts/run-agent.py +59 -0
  99. procesio_cli/scripts/run-tool.py +123 -0
  100. procesio_cli/scripts/secret_scan.py +584 -0
  101. procesio_cli/scripts/set-credential.py +126 -0
  102. procesio_cli/scripts/validate-skills.py +530 -0
  103. procesio_cli/skills/PORTFOLIO.md +134 -0
  104. procesio_cli/skills/__init__.py +6 -0
  105. procesio_cli/skills/agent-skill-engineer/SKILL.md +226 -0
  106. procesio_cli/skills/agent-skill-engineer/evals/evals.json +422 -0
  107. procesio_cli/skills/agent-skill-engineer/references/authoring-standard.md +178 -0
  108. procesio_cli/skills/agent-skill-engineer/references/decision-framework.md +107 -0
  109. procesio_cli/skills/agent-skill-engineer/references/domain-evidence-protocol.md +248 -0
  110. procesio_cli/skills/agent-skill-engineer/references/evaluation-standard.md +191 -0
  111. procesio_cli/skills/agent-skill-engineer/references/expert-lenses.md +167 -0
  112. procesio_cli/skills/agent-skill-engineer/references/field-gate-standard.md +231 -0
  113. procesio_cli/skills/agent-skill-engineer/references/optimization-protocol.md +233 -0
  114. procesio_cli/skills/agent-skill-engineer/references/optimizer-contract.md +268 -0
  115. procesio_cli/skills/agent-skill-engineer/references/portability-standard.md +99 -0
  116. procesio_cli/skills/agent-skill-engineer/references/research-basis.md +128 -0
  117. procesio_cli/skills/agent-skill-engineer/references/review-standard.md +143 -0
  118. procesio_cli/skills/agent-skill-engineer/scripts/audit_skill.py +959 -0
  119. procesio_cli/skills/agent-skill-engineer/scripts/optimize_skill.py +891 -0
  120. procesio_cli/skills/agent-skill-engineer/scripts/scaffold_skill.py +397 -0
  121. procesio_cli/skills/evals/baseline-catalog.json +14 -0
  122. procesio_cli/skills/evals/baseline-routing-v2.json +38 -0
  123. procesio_cli/skills/evals/baseline.json +37 -0
  124. procesio_cli/skills/evals/behavioral.json +367 -0
  125. procesio_cli/skills/evals/field-boundary-development-v1.json +126 -0
  126. procesio_cli/skills/evals/field-closeout-correction-development-v1.json +23 -0
  127. procesio_cli/skills/evals/field-closeout-development-v1.json +89 -0
  128. procesio_cli/skills/evals/portfolio-pressure-training-v1.json +83 -0
  129. procesio_cli/skills/evals/routing.json +363 -0
  130. procesio_cli/skills/evals/validation-baseline.json +4 -0
  131. procesio_cli/skills/procesio-cli/SKILL.md +134 -0
  132. procesio_cli/skills/procesio-cli/evals/evals.json +111 -0
  133. procesio_cli/skills/procesio-cli/evals/workflows.json +16 -0
  134. procesio_cli/skills/procesio-cli/references/connector-lifecycle.md +47 -0
  135. procesio_cli/skills/procesio-cli/references/credentials-admin.md +113 -0
  136. procesio_cli/skills/procesio-cli/references/data-verification.md +71 -0
  137. procesio_cli/skills/procesio-cli/references/documents-files.md +44 -0
  138. procesio_cli/skills/procesio-cli/references/form-e2e.md +57 -0
  139. procesio_cli/skills/procesio-cli/references/operation-contract.md +138 -0
  140. procesio_cli/skills/procesio-cli/references/process-debugging.md +65 -0
  141. procesio_cli/skills/procesio-cli/references/process-lifecycle.md +88 -0
  142. procesio_cli/skills/procesio-cli/references/schedules-webhooks.md +58 -0
  143. procesio_cli/skills/procesio-cli/references/transport-environments.md +53 -0
  144. procesio_cli/skills/procesio-cli-maintainer/SKILL.md +84 -0
  145. procesio_cli/skills/procesio-cli-maintainer/evals/evals.json +116 -0
  146. procesio_cli/skills/procesio-cli-maintainer/references/change-contract.md +34 -0
  147. procesio_cli/skills/procesio-cli-maintainer/references/mcp-compatibility.md +13 -0
  148. procesio_cli/skills/procesio-cli-maintainer/references/skill-authoring.md +51 -0
  149. procesio_cli/skills/procesio-platform-advisor/SKILL.md +82 -0
  150. procesio_cli/skills/procesio-platform-advisor/evals/evals.json +116 -0
  151. procesio_cli/skills/procesio-platform-advisor/references/capability-map.md +22 -0
  152. procesio_cli/skills/procesio-platform-advisor/references/fit-framework.md +35 -0
  153. procesio_cli/skills/procesio-platform-advisor/references/freshness-policy.md +25 -0
  154. procesio_cli/skills/procesio-platform-advisor/references/sizing-method.md +70 -0
  155. procesio_cli/skills/procesio-platform-advisor/scripts/estimate_capacity.py +99 -0
  156. procesio_cli/skills/procesio-setup/SKILL.md +180 -0
  157. procesio_cli/skills/sql-server-optimizer/SKILL.md +86 -0
  158. procesio_cli/skills/sql-server-optimizer/evals/evals.json +116 -0
  159. procesio_cli/skills/sql-server-optimizer/references/db-context.md +44 -0
  160. procesio_cli/skills/sql-server-optimizer/references/generic-mode.md +32 -0
  161. procesio_cli/skills/sql-server-optimizer/references/procesio-mode.md +40 -0
  162. procesio_cli/skills/sql-server-optimizer/references/sql-rules.md +43 -0
  163. procesio_cli/skills/sql-server-optimizer/scripts/export-indexes.sql +178 -0
  164. procesio_cli/skills/sql-server-optimizer/scripts/export-procs-and-functions.sql +118 -0
  165. procesio_cli/skills/sql-server-optimizer/scripts/export-tables.sql +367 -0
  166. procesio_cli/skills/tests/__init__.py +0 -0
  167. procesio_cli/skills/tests/test_agent_skill_engineer.py +240 -0
  168. procesio_cli/skills/tests/test_auth_failure_stop.py +25 -0
  169. procesio_cli/skills/tests/test_capacity_estimator.py +85 -0
  170. procesio_cli/skills/tests/test_field_boundary_learning.py +100 -0
  171. procesio_cli/skills/tests/test_field_closeout_learning.py +123 -0
  172. procesio_cli/skills/tests/test_field_gate_standard.py +76 -0
  173. procesio_cli/skills/tests/test_operational_playbooks.py +51 -0
  174. procesio_cli/skills/tests/test_portfolio_pressure_training.py +35 -0
  175. procesio_cli/skills/tests/test_process_lifecycle_single_run.py +25 -0
  176. procesio_cli/skills/tests/test_skill_evaluator.py +67 -0
  177. procesio_cli/skills/tests/test_skill_optimizer.py +473 -0
  178. procesio_cli/skills/tests/test_skill_portfolio_review.py +70 -0
  179. procesio_cli/skills/tests/test_skill_release_binding.py +204 -0
  180. procesio_cli/skills/tests/test_skill_routing.py +74 -0
  181. procesio_cli/skills/tests/test_skill_scaffold_v2.py +116 -0
  182. procesio_cli/skills/tests/test_validate_skills.py +115 -0
  183. procesio_cli/tools/__init__.py +0 -0
  184. procesio_cli/tools/_lib/README.md +119 -0
  185. procesio_cli/tools/_lib/__init__.py +0 -0
  186. procesio_cli/tools/_lib/accounting.py +120 -0
  187. procesio_cli/tools/_lib/contacts.py +544 -0
  188. procesio_cli/tools/_lib/creds.py +83 -0
  189. procesio_cli/tools/_lib/creds_backends.py +578 -0
  190. procesio_cli/tools/_lib/dbprofiles.py +147 -0
  191. procesio_cli/tools/_lib/dbquery.py +286 -0
  192. procesio_cli/tools/_lib/dbschema.py +225 -0
  193. procesio_cli/tools/_lib/io.py +54 -0
  194. procesio_cli/tools/_lib/manifest.py +297 -0
  195. procesio_cli/tools/_lib/skill_release.py +127 -0
  196. procesio_cli/tools/_lib/skill_resources.py +118 -0
  197. procesio_cli/tools/_lib/userdata.py +228 -0
  198. procesio_cli/tools/_lib/webserialize/__init__.py +16 -0
  199. procesio_cli/tools/_lib/webserialize/admin.py +61 -0
  200. procesio_cli/tools/_lib/webserialize/client.py +264 -0
  201. procesio_cli/tools/_lib/webserialize/config.py +96 -0
  202. procesio_cli/tools/_lib/webserialize/errors.py +27 -0
  203. procesio_cli/tools/_lib/webserialize/framing.py +45 -0
  204. procesio_cli/tools/_lib/webserialize/registry.py +47 -0
  205. procesio_cli/tools/_lib/webserialize/runtime.py +61 -0
  206. procesio_cli/tools/_lib/webserialize/server.py +510 -0
  207. procesio_cli/tools/_lib/websession.py +194 -0
  208. procesio_cli/tools/connector-builder/CONNECTOR-BUILDER-API-NOTES.md +91 -0
  209. procesio_cli/tools/connector-builder/README.md +101 -0
  210. procesio_cli/tools/connector-builder/SKILL.md +248 -0
  211. procesio_cli/tools/connector-builder/__init__.py +7 -0
  212. procesio_cli/tools/connector-builder/actiondef.py +24 -0
  213. procesio_cli/tools/connector-builder/client.py +251 -0
  214. procesio_cli/tools/connector-builder/errors.py +44 -0
  215. procesio_cli/tools/connector-builder/gen_manifest.py +142 -0
  216. procesio_cli/tools/connector-builder/handlers/__init__.py +1 -0
  217. procesio_cli/tools/connector-builder/handlers/admin.py +182 -0
  218. procesio_cli/tools/connector-builder/handlers/auth.py +74 -0
  219. procesio_cli/tools/connector-builder/handlers/builds.py +396 -0
  220. procesio_cli/tools/connector-builder/handlers/common.py +104 -0
  221. procesio_cli/tools/connector-builder/handlers/files.py +111 -0
  222. procesio_cli/tools/connector-builder/handlers/raw.py +38 -0
  223. procesio_cli/tools/connector-builder/handlers/telemetry.py +39 -0
  224. procesio_cli/tools/connector-builder/main.py +94 -0
  225. procesio_cli/tools/connector-builder/tests/conftest.py +146 -0
  226. procesio_cli/tools/connector-builder/tests/test_cb_client.py +86 -0
  227. procesio_cli/tools/connector-builder/tests/test_cb_dispatch.py +105 -0
  228. procesio_cli/tools/connector-builder/tests/test_cb_manifest_sync.py +76 -0
  229. procesio_cli/tools/connector-builder/tests/test_cb_wait.py +123 -0
  230. procesio_cli/tools/connector-builder/tool.yaml +671 -0
  231. procesio_cli/tools/framework-map/.gitattributes +2 -0
  232. procesio_cli/tools/framework-map/README.md +98 -0
  233. procesio_cli/tools/framework-map/SKILL.md +30 -0
  234. procesio_cli/tools/framework-map/app.js +188 -0
  235. procesio_cli/tools/framework-map/fm_builder.py +476 -0
  236. procesio_cli/tools/framework-map/fm_catalog.py +19 -0
  237. procesio_cli/tools/framework-map/fm_ro_catalog.py +94 -0
  238. procesio_cli/tools/framework-map/fm_strings.py +101 -0
  239. procesio_cli/tools/framework-map/main.py +95 -0
  240. procesio_cli/tools/framework-map/styles.css +214 -0
  241. procesio_cli/tools/framework-map/tests/conftest.py +33 -0
  242. procesio_cli/tools/framework-map/tests/test_fm_build.py +126 -0
  243. procesio_cli/tools/framework-map/tests/test_fm_manifest_sync.py +63 -0
  244. procesio_cli/tools/framework-map/tests/test_fm_store_tree.py +75 -0
  245. procesio_cli/tools/framework-map/tool.yaml +61 -0
  246. procesio_cli/tools/mysql/MOVED-TO-CENTRAL.md +29 -0
  247. procesio_cli/tools/mysql/README.md +196 -0
  248. procesio_cli/tools/mysql/SKILL.md +37 -0
  249. procesio_cli/tools/mysql/__init__.py +0 -0
  250. procesio_cli/tools/mysql/connection.py +191 -0
  251. procesio_cli/tools/mysql/main.py +487 -0
  252. procesio_cli/tools/mysql/profiles.template.json +1 -0
  253. procesio_cli/tools/mysql/schema.py +434 -0
  254. procesio_cli/tools/mysql/tests/__init__.py +0 -0
  255. procesio_cli/tools/mysql/tests/conftest.py +21 -0
  256. procesio_cli/tools/mysql/tests/test_manifest_sync.py +68 -0
  257. procesio_cli/tools/mysql/tests/test_mysql.py +201 -0
  258. procesio_cli/tools/mysql/tests/test_mysql_schema.py +337 -0
  259. procesio_cli/tools/mysql/tool.yaml +112 -0
  260. procesio_cli/tools/procesio/DTO-SUBTOOLS-NOTE.md +360 -0
  261. procesio_cli/tools/procesio/FORM-DEV-GUIDE/00-INDEX.md +67 -0
  262. procesio_cli/tools/procesio/FORM-DEV-GUIDE/01-ANATOMY.md +209 -0
  263. procesio_cli/tools/procesio/FORM-DEV-GUIDE/02-CODE-INJECTION.md +309 -0
  264. procesio_cli/tools/procesio/FORM-DEV-GUIDE/03-DOM-CONTRACT.md +370 -0
  265. procesio_cli/tools/procesio/FORM-DEV-GUIDE/04-INTERACTION-RECIPES.md +600 -0
  266. procesio_cli/tools/procesio/FORM-DEV-GUIDE/05-STEPPER-AND-MOTION.md +426 -0
  267. procesio_cli/tools/procesio/FORM-DEV-GUIDE/06-PROCESS-INTEGRATION.md +347 -0
  268. procesio_cli/tools/procesio/FORM-DEV-GUIDE/07-DEPLOY-WORKFLOW.md +294 -0
  269. procesio_cli/tools/procesio/FORM-DEV-GUIDE/08-PITFALLS.md +569 -0
  270. procesio_cli/tools/procesio/FORM-DEV-GUIDE/09-NAVIGATION-AND-HOSTING.md +308 -0
  271. procesio_cli/tools/procesio/PHASE4-E2E-NOTES.md +594 -0
  272. procesio_cli/tools/procesio/PROCESIO-API-CORRECTIONS.md +295 -0
  273. procesio_cli/tools/procesio/PROCESIO-API-NOTES.md +6976 -0
  274. procesio_cli/tools/procesio/PROCESIO-AUTH-NOTES.md +226 -0
  275. procesio_cli/tools/procesio/PROCESIO-CARD-BUILD-NOTES.md +183 -0
  276. procesio_cli/tools/procesio/PROCESIO-CONNECTORS-NOTES.md +141 -0
  277. procesio_cli/tools/procesio/PROCESIO-CUSTOM-ACTION-NOTES.md +179 -0
  278. procesio_cli/tools/procesio/PROCESIO-DOCS-FIX-REPORT.md +123 -0
  279. procesio_cli/tools/procesio/PROCESIO-ENVIRONMENTS-NOTES.md +94 -0
  280. procesio_cli/tools/procesio/PROCESIO-FE-VALIDATION-NOTES.md +107 -0
  281. procesio_cli/tools/procesio/PROCESIO-FOREACH-NOTES.md +199 -0
  282. procesio_cli/tools/procesio/PROCESIO-FORM-API-HANG-NOTE.md +101 -0
  283. procesio_cli/tools/procesio/PROCESIO-FORM-SUBMISSION-NOTES.md +172 -0
  284. procesio_cli/tools/procesio/PROCESIO-HTML-TO-DOCX-NOTES.md +110 -0
  285. procesio_cli/tools/procesio/PROCESIO-INSTANCE-HISTORY-NOTES.md +242 -0
  286. procesio_cli/tools/procesio/PROCESIO-METERING-NOTES.md +201 -0
  287. procesio_cli/tools/procesio/PROCESIO-NODE-CODE-NOTES.md +279 -0
  288. procesio_cli/tools/procesio/PROCESIO-NODE-MODULE-WHITELIST.md +113 -0
  289. procesio_cli/tools/procesio/PROCESIO-PYTHON-ACTION-NOTES.md +165 -0
  290. procesio_cli/tools/procesio/PROCESIO-QUERY-STORE-NOTES.md +826 -0
  291. procesio_cli/tools/procesio/PROCESIO-RECONCILIATION-PATTERNS.md +104 -0
  292. procesio_cli/tools/procesio/PROCESIO-RESOURCE-MODEL-NOTES.md +1100 -0
  293. procesio_cli/tools/procesio/PROCESIO-SEND-EMAIL-CRED-OPTIONALITY.md +45 -0
  294. procesio_cli/tools/procesio/PROCESIO-SEND-EMAIL-NOTES.md +252 -0
  295. procesio_cli/tools/procesio/PROCESIO-SQL-ACTIONS-NOTES.md +284 -0
  296. procesio_cli/tools/procesio/PROCESIO-USAGE-GUIDE.md +219 -0
  297. procesio_cli/tools/procesio/README.md +341 -0
  298. procesio_cli/tools/procesio/SCHEDULE-INPUT-SECURITY-NOTES.md +63 -0
  299. procesio_cli/tools/procesio/SKILL.md +694 -0
  300. procesio_cli/tools/procesio/__init__.py +1 -0
  301. procesio_cli/tools/procesio/actiondef.py +27 -0
  302. procesio_cli/tools/procesio/assets/html2docx/html2docx.js +2155 -0
  303. procesio_cli/tools/procesio/auth.py +354 -0
  304. procesio_cli/tools/procesio/autogen.py +121 -0
  305. procesio_cli/tools/procesio/client.py +355 -0
  306. procesio_cli/tools/procesio/config.py +96 -0
  307. procesio_cli/tools/procesio/data/endpoints.json +2901 -0
  308. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/01-authentication.md +180 -0
  309. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/02-conventions.md +116 -0
  310. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/ANONYMOUS-SURFACE.md +51 -0
  311. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/README.md +104 -0
  312. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/SYMBOL-INDEX.md +444 -0
  313. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/03-auth-users-workspaces.md +731 -0
  314. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/04-processes.md +913 -0
  315. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/05-actions.md +605 -0
  316. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/06-files.md +237 -0
  317. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/07-forms-documents.md +985 -0
  318. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/08-credentials.md +532 -0
  319. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/09-datastore-datatypes.md +830 -0
  320. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/10-custom-urls.md +319 -0
  321. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/11-subscriptions-resources-analytics.md +455 -0
  322. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/12-webhooks-notifications-misc.md +519 -0
  323. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/13-realtime-signalr.md +153 -0
  324. procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/99-internal-and-other-services.md +52 -0
  325. procesio_cli/tools/procesio/docs_info/FormBuilder mock.ts +5725 -0
  326. procesio_cli/tools/procesio/docs_info/Procesio Platform Actions.json +30219 -0
  327. procesio_cli/tools/procesio/docs_info/form-config-enums.ts +92 -0
  328. procesio_cli/tools/procesio/docs_info/form-theme.ts +1603 -0
  329. procesio_cli/tools/procesio/docs_info/process-validation-reference-1.md +2739 -0
  330. procesio_cli/tools/procesio/dto/__init__.py +13 -0
  331. procesio_cli/tools/procesio/dto/credential/builder.py +179 -0
  332. procesio_cli/tools/procesio/dto/credential/config.schema.json +19 -0
  333. procesio_cli/tools/procesio/dto/credential/description.md +193 -0
  334. procesio_cli/tools/procesio/dto/credential/fixtures/rest.config.json +11 -0
  335. procesio_cli/tools/procesio/dto/credential/fixtures/rest.dto.json +28 -0
  336. procesio_cli/tools/procesio/dto/credential/fixtures/rest.template.json +44 -0
  337. procesio_cli/tools/procesio/dto/credential/template.dto.json +16 -0
  338. procesio_cli/tools/procesio/dto/data/_refresh.py +147 -0
  339. procesio_cli/tools/procesio/dto/data/action_catalog.json +30825 -0
  340. procesio_cli/tools/procesio/dto/data/action_families.json +55 -0
  341. procesio_cli/tools/procesio/dto/data/platform_types.json +182 -0
  342. procesio_cli/tools/procesio/dto/datatype/builder.py +223 -0
  343. procesio_cli/tools/procesio/dto/datatype/config.schema.json +47 -0
  344. procesio_cli/tools/procesio/dto/datatype/description.md +85 -0
  345. procesio_cli/tools/procesio/dto/datatype/fixtures/customer.config.json +41 -0
  346. procesio_cli/tools/procesio/dto/datatype/fixtures/customer.dto.json +164 -0
  347. procesio_cli/tools/procesio/dto/datatype/template.dto.json +37 -0
  348. procesio_cli/tools/procesio/dto/document/builder.py +193 -0
  349. procesio_cli/tools/procesio/dto/document/config.schema.json +33 -0
  350. procesio_cli/tools/procesio/dto/document/description.md +124 -0
  351. procesio_cli/tools/procesio/dto/document/fixtures/invoice.config.json +16 -0
  352. procesio_cli/tools/procesio/dto/document/fixtures/invoice.dto.json +32 -0
  353. procesio_cli/tools/procesio/dto/document/template.dto.json +14 -0
  354. procesio_cli/tools/procesio/dto/form/FORM-STYLING-NOTES.md +142 -0
  355. procesio_cli/tools/procesio/dto/form/GOLDEN-DRIFT-NOTES.md +72 -0
  356. procesio_cli/tools/procesio/dto/form/RUN-PROCESS-RESULT-NOTES.md +80 -0
  357. procesio_cli/tools/procesio/dto/form/addelement.py +277 -0
  358. procesio_cli/tools/procesio/dto/form/builder.py +1009 -0
  359. procesio_cli/tools/procesio/dto/form/capture_goldens.py +106 -0
  360. procesio_cli/tools/procesio/dto/form/code_cipher.py +67 -0
  361. procesio_cli/tools/procesio/dto/form/config.schema.json +76 -0
  362. procesio_cli/tools/procesio/dto/form/data_shell.json +1 -0
  363. procesio_cli/tools/procesio/dto/form/description.md +346 -0
  364. procesio_cli/tools/procesio/dto/form/elements/approval.json +259 -0
  365. procesio_cli/tools/procesio/dto/form/elements/assignee.json +137 -0
  366. procesio_cli/tools/procesio/dto/form/elements/button.json +125 -0
  367. procesio_cli/tools/procesio/dto/form/elements/chart.json +341 -0
  368. procesio_cli/tools/procesio/dto/form/elements/chat.json +199 -0
  369. procesio_cli/tools/procesio/dto/form/elements/checkbox.json +153 -0
  370. procesio_cli/tools/procesio/dto/form/elements/column.json +77 -0
  371. procesio_cli/tools/procesio/dto/form/elements/columns.json +78 -0
  372. procesio_cli/tools/procesio/dto/form/elements/datetime-input.json +304 -0
  373. procesio_cli/tools/procesio/dto/form/elements/divider.json +54 -0
  374. procesio_cli/tools/procesio/dto/form/elements/dropdown.json +130 -0
  375. procesio_cli/tools/procesio/dto/form/elements/dynamic-table-row.json +253 -0
  376. procesio_cli/tools/procesio/dto/form/elements/file-upload.json +196 -0
  377. procesio_cli/tools/procesio/dto/form/elements/file-viewer.json +169 -0
  378. procesio_cli/tools/procesio/dto/form/elements/heading.json +79 -0
  379. procesio_cli/tools/procesio/dto/form/elements/icon.json +107 -0
  380. procesio_cli/tools/procesio/dto/form/elements/image.json +174 -0
  381. procesio_cli/tools/procesio/dto/form/elements/input.json +206 -0
  382. procesio_cli/tools/procesio/dto/form/elements/list.json +202 -0
  383. procesio_cli/tools/procesio/dto/form/elements/number-input.json +202 -0
  384. procesio_cli/tools/procesio/dto/form/elements/paragraph.json +79 -0
  385. procesio_cli/tools/procesio/dto/form/elements/radiobox.json +174 -0
  386. procesio_cli/tools/procesio/dto/form/elements/section.json +114 -0
  387. procesio_cli/tools/procesio/dto/form/elements/select.json +217 -0
  388. procesio_cli/tools/procesio/dto/form/elements/side-panel.json +146 -0
  389. procesio_cli/tools/procesio/dto/form/elements/signature-pad.json +141 -0
  390. procesio_cli/tools/procesio/dto/form/elements/static-table-row.json +84 -0
  391. procesio_cli/tools/procesio/dto/form/elements/step.json +105 -0
  392. procesio_cli/tools/procesio/dto/form/elements/stepper.json +126 -0
  393. procesio_cli/tools/procesio/dto/form/elements/tab.json +77 -0
  394. procesio_cli/tools/procesio/dto/form/elements/table.json +279 -0
  395. procesio_cli/tools/procesio/dto/form/elements/tabs.json +114 -0
  396. procesio_cli/tools/procesio/dto/form/elements/textarea.json +213 -0
  397. procesio_cli/tools/procesio/dto/form/enum_map.json +68 -0
  398. procesio_cli/tools/procesio/dto/form/fieldpath.py +284 -0
  399. procesio_cli/tools/procesio/dto/form/fixtures/reg.config.json +32 -0
  400. procesio_cli/tools/procesio/dto/form/fixtures/reg.elements.json +638 -0
  401. procesio_cli/tools/procesio/dto/form/sync_from_mock.py +386 -0
  402. procesio_cli/tools/procesio/dto/framework.py +125 -0
  403. procesio_cli/tools/procesio/dto/process/builder.py +1991 -0
  404. procesio_cli/tools/procesio/dto/process/config.schema.json +159 -0
  405. procesio_cli/tools/procesio/dto/process/description.md +218 -0
  406. procesio_cli/tools/procesio/dto/process/fixtures/greet.config.json +32 -0
  407. procesio_cli/tools/procesio/dto/process/fixtures/greet.dto.json +317 -0
  408. procesio_cli/tools/procesio/dto/process/naming.py +305 -0
  409. procesio_cli/tools/procesio/dto/process/normalize.py +74 -0
  410. procesio_cli/tools/procesio/dto/process/template.dto.json +32 -0
  411. procesio_cli/tools/procesio/dto/refdata.py +87 -0
  412. procesio_cli/tools/procesio/dto/registry.py +27 -0
  413. procesio_cli/tools/procesio/dto/webhook/builder.py +142 -0
  414. procesio_cli/tools/procesio/dto/webhook/config.schema.json +25 -0
  415. procesio_cli/tools/procesio/dto/webhook/description.md +92 -0
  416. procesio_cli/tools/procesio/dto/webhook/fixtures/order.config.json +10 -0
  417. procesio_cli/tools/procesio/dto/webhook/fixtures/order.dto.json +25 -0
  418. procesio_cli/tools/procesio/dto/webhook/template.dto.json +14 -0
  419. procesio_cli/tools/procesio/environments.py +264 -0
  420. procesio_cli/tools/procesio/errors.py +166 -0
  421. procesio_cli/tools/procesio/flowmodel/README.md +37 -0
  422. procesio_cli/tools/procesio/flowmodel/__init__.py +14 -0
  423. procesio_cli/tools/procesio/flowmodel/digest.py +223 -0
  424. procesio_cli/tools/procesio/flowmodel/families.py +34 -0
  425. procesio_cli/tools/procesio/flowmodel/fevalidation.py +1098 -0
  426. procesio_cli/tools/procesio/flowmodel/formdigest.py +232 -0
  427. procesio_cli/tools/procesio/flowmodel/inspect.py +78 -0
  428. procesio_cli/tools/procesio/flowmodel/model.py +82 -0
  429. procesio_cli/tools/procesio/flowmodel/nodeparam.py +871 -0
  430. procesio_cli/tools/procesio/flowmodel/reader.py +191 -0
  431. procesio_cli/tools/procesio/flowmodel/sqlparam.py +410 -0
  432. procesio_cli/tools/procesio/form_code_keys.py +50 -0
  433. procesio_cli/tools/procesio/formlint.py +292 -0
  434. procesio_cli/tools/procesio/gen_manifest.py +118 -0
  435. procesio_cli/tools/procesio/handlers/__init__.py +1 -0
  436. procesio_cli/tools/procesio/handlers/auth_actions.py +103 -0
  437. procesio_cli/tools/procesio/handlers/catalog.py +147 -0
  438. procesio_cli/tools/procesio/handlers/common.py +90 -0
  439. procesio_cli/tools/procesio/handlers/customaction_ops.py +146 -0
  440. procesio_cli/tools/procesio/handlers/datastore_ops.py +482 -0
  441. procesio_cli/tools/procesio/handlers/datatype_ops.py +275 -0
  442. procesio_cli/tools/procesio/handlers/dto_actions.py +258 -0
  443. procesio_cli/tools/procesio/handlers/environment_admin.py +146 -0
  444. procesio_cli/tools/procesio/handlers/fevalidate.py +236 -0
  445. procesio_cli/tools/procesio/handlers/files.py +141 -0
  446. procesio_cli/tools/procesio/handlers/flowgraph.py +229 -0
  447. procesio_cli/tools/procesio/handlers/flowlint.py +269 -0
  448. procesio_cli/tools/procesio/handlers/form_chains.py +336 -0
  449. procesio_cli/tools/procesio/handlers/form_code.py +277 -0
  450. procesio_cli/tools/procesio/handlers/form_element.py +199 -0
  451. procesio_cli/tools/procesio/handlers/form_events.py +441 -0
  452. procesio_cli/tools/procesio/handlers/form_update.py +270 -0
  453. procesio_cli/tools/procesio/handlers/formprocess.py +211 -0
  454. procesio_cli/tools/procesio/handlers/generic.py +135 -0
  455. procesio_cli/tools/procesio/handlers/layout.py +137 -0
  456. procesio_cli/tools/procesio/handlers/nodeparams.py +1018 -0
  457. procesio_cli/tools/procesio/handlers/process_layout.py +189 -0
  458. procesio_cli/tools/procesio/handlers/process_naming.py +213 -0
  459. procesio_cli/tools/procesio/handlers/processes.py +416 -0
  460. procesio_cli/tools/procesio/handlers/profile_admin.py +184 -0
  461. procesio_cli/tools/procesio/handlers/resource_ops.py +447 -0
  462. procesio_cli/tools/procesio/handlers/schedules.py +320 -0
  463. procesio_cli/tools/procesio/handlers/sqlactions.py +232 -0
  464. procesio_cli/tools/procesio/handlers/transport.py +463 -0
  465. procesio_cli/tools/procesio/handlers/usageguide.py +222 -0
  466. procesio_cli/tools/procesio/layout/README.md +196 -0
  467. procesio_cli/tools/procesio/layout/__init__.py +11 -0
  468. procesio_cli/tools/procesio/layout/adapter.py +736 -0
  469. procesio_cli/tools/procesio/layout/cycles.py +186 -0
  470. procesio_cli/tools/procesio/layout/dispatch.py +39 -0
  471. procesio_cli/tools/procesio/layout/elk/elk_runner.mjs +50 -0
  472. procesio_cli/tools/procesio/layout/elk/package-lock.json +21 -0
  473. procesio_cli/tools/procesio/layout/elk/package.json +11 -0
  474. procesio_cli/tools/procesio/layout/elk_engine.py +568 -0
  475. procesio_cli/tools/procesio/layout/engine.py +1311 -0
  476. procesio_cli/tools/procesio/layout/report.py +187 -0
  477. procesio_cli/tools/procesio/main.py +179 -0
  478. procesio_cli/tools/procesio/profiles.py +193 -0
  479. procesio_cli/tools/procesio/reliability.py +193 -0
  480. procesio_cli/tools/procesio/sqlformat.py +167 -0
  481. procesio_cli/tools/procesio/swagger.py +51 -0
  482. procesio_cli/tools/procesio/tests/__init__.py +0 -0
  483. procesio_cli/tools/procesio/tests/conftest.py +129 -0
  484. procesio_cli/tools/procesio/tests/test_auth.py +200 -0
  485. procesio_cli/tools/procesio/tests/test_autogen.py +78 -0
  486. procesio_cli/tools/procesio/tests/test_builder_attrpath_sqlbind.py +180 -0
  487. procesio_cli/tools/procesio/tests/test_builder_custom_action_catalog.py +72 -0
  488. procesio_cli/tools/procesio/tests/test_client.py +114 -0
  489. procesio_cli/tools/procesio/tests/test_client_reliability.py +139 -0
  490. procesio_cli/tools/procesio/tests/test_customaction.py +103 -0
  491. procesio_cli/tools/procesio/tests/test_customaction_test.py +80 -0
  492. procesio_cli/tools/procesio/tests/test_cycles.py +69 -0
  493. procesio_cli/tools/procesio/tests/test_datastore.py +378 -0
  494. procesio_cli/tools/procesio/tests/test_datastore_node.py +94 -0
  495. procesio_cli/tools/procesio/tests/test_dispatch.py +170 -0
  496. procesio_cli/tools/procesio/tests/test_document_page_size.py +19 -0
  497. procesio_cli/tools/procesio/tests/test_dto.py +734 -0
  498. procesio_cli/tools/procesio/tests/test_edit_dryrun_preview.py +96 -0
  499. procesio_cli/tools/procesio/tests/test_elk_layout.py +221 -0
  500. procesio_cli/tools/procesio/tests/test_environments.py +214 -0
  501. procesio_cli/tools/procesio/tests/test_fevalidate_gate.py +104 -0
  502. procesio_cli/tools/procesio/tests/test_fevalidation.py +317 -0
  503. procesio_cli/tools/procesio/tests/test_fieldpath_variables.py +87 -0
  504. procesio_cli/tools/procesio/tests/test_files.py +72 -0
  505. procesio_cli/tools/procesio/tests/test_flow_digest.py +202 -0
  506. procesio_cli/tools/procesio/tests/test_flowlint.py +114 -0
  507. procesio_cli/tools/procesio/tests/test_flowmodel.py +158 -0
  508. procesio_cli/tools/procesio/tests/test_form_add_element.py +256 -0
  509. procesio_cli/tools/procesio/tests/test_form_approval_gaps.py +319 -0
  510. procesio_cli/tools/procesio/tests/test_form_auto_names.py +60 -0
  511. procesio_cli/tools/procesio/tests/test_form_chains.py +217 -0
  512. procesio_cli/tools/procesio/tests/test_form_code_actions.py +188 -0
  513. procesio_cli/tools/procesio/tests/test_form_code_keys.py +92 -0
  514. procesio_cli/tools/procesio/tests/test_form_code_style.py +178 -0
  515. procesio_cli/tools/procesio/tests/test_form_delete_element.py +99 -0
  516. procesio_cli/tools/procesio/tests/test_form_element_config.py +141 -0
  517. procesio_cli/tools/procesio/tests/test_form_event_maps.py +135 -0
  518. procesio_cli/tools/procesio/tests/test_form_event_ops.py +376 -0
  519. procesio_cli/tools/procesio/tests/test_form_event_replace.py +86 -0
  520. procesio_cli/tools/procesio/tests/test_form_events.py +92 -0
  521. procesio_cli/tools/procesio/tests/test_form_parity.py +171 -0
  522. procesio_cli/tools/procesio/tests/test_form_select_source.py +74 -0
  523. procesio_cli/tools/procesio/tests/test_form_sync.py +35 -0
  524. procesio_cli/tools/procesio/tests/test_form_update.py +133 -0
  525. procesio_cli/tools/procesio/tests/test_formlint.py +155 -0
  526. procesio_cli/tools/procesio/tests/test_formlint_css_scope.py +38 -0
  527. procesio_cli/tools/procesio/tests/test_formlint_field_names.py +83 -0
  528. procesio_cli/tools/procesio/tests/test_guards.py +110 -0
  529. procesio_cli/tools/procesio/tests/test_hidden_setting_types.py +39 -0
  530. procesio_cli/tools/procesio/tests/test_html2docx_asset.py +132 -0
  531. procesio_cli/tools/procesio/tests/test_key_handle_terminology.py +45 -0
  532. procesio_cli/tools/procesio/tests/test_layout.py +574 -0
  533. procesio_cli/tools/procesio/tests/test_layout_report.py +88 -0
  534. procesio_cli/tools/procesio/tests/test_manifest_sync.py +68 -0
  535. procesio_cli/tools/procesio/tests/test_nodedelete.py +151 -0
  536. procesio_cli/tools/procesio/tests/test_nodeparam.py +826 -0
  537. procesio_cli/tools/procesio/tests/test_normalize.py +70 -0
  538. procesio_cli/tools/procesio/tests/test_payload_docs.py +86 -0
  539. procesio_cli/tools/procesio/tests/test_process_action_naming.py +170 -0
  540. procesio_cli/tools/procesio/tests/test_process_call_api_seed.py +46 -0
  541. procesio_cli/tools/procesio/tests/test_process_ext.py +373 -0
  542. procesio_cli/tools/procesio/tests/test_process_invariants.py +183 -0
  543. procesio_cli/tools/procesio/tests/test_process_layout.py +117 -0
  544. procesio_cli/tools/procesio/tests/test_process_naming.py +188 -0
  545. procesio_cli/tools/procesio/tests/test_process_query_store.py +172 -0
  546. procesio_cli/tools/procesio/tests/test_processinfo_variable.py +56 -0
  547. procesio_cli/tools/procesio/tests/test_profiles.py +127 -0
  548. procesio_cli/tools/procesio/tests/test_put_projects.py +41 -0
  549. procesio_cli/tools/procesio/tests/test_reliability.py +107 -0
  550. procesio_cli/tools/procesio/tests/test_removal_gate.py +95 -0
  551. procesio_cli/tools/procesio/tests/test_resource_ops.py +173 -0
  552. procesio_cli/tools/procesio/tests/test_save_validity.py +207 -0
  553. procesio_cli/tools/procesio/tests/test_schedule_cron_shape.py +111 -0
  554. procesio_cli/tools/procesio/tests/test_schedules.py +309 -0
  555. procesio_cli/tools/procesio/tests/test_sqlconvert.py +234 -0
  556. procesio_cli/tools/procesio/tests/test_sqlformat.py +83 -0
  557. procesio_cli/tools/procesio/tests/test_sqlparam.py +187 -0
  558. procesio_cli/tools/procesio/tests/test_superseded_actions.py +54 -0
  559. procesio_cli/tools/procesio/tests/test_surgical_concurrency.py +156 -0
  560. procesio_cli/tools/procesio/tests/test_tier4.py +123 -0
  561. procesio_cli/tools/procesio/tests/test_transport.py +147 -0
  562. procesio_cli/tools/procesio/tests/test_transport_data_stores.py +168 -0
  563. procesio_cli/tools/procesio/tests/test_transport_mapper_repair.py +150 -0
  564. procesio_cli/tools/procesio/tests/test_transport_webhook_strip.py +253 -0
  565. procesio_cli/tools/procesio/tests/test_usage_guide.py +96 -0
  566. procesio_cli/tools/procesio/tool.yaml +12486 -0
  567. procesio_cli/tools/sqlserver/MOVED-TO-CENTRAL.md +29 -0
  568. procesio_cli/tools/sqlserver/README.md +192 -0
  569. procesio_cli/tools/sqlserver/SKILL.md +37 -0
  570. procesio_cli/tools/sqlserver/SQLSERVER-SCHEMA-NOTES.md +284 -0
  571. procesio_cli/tools/sqlserver/__init__.py +0 -0
  572. procesio_cli/tools/sqlserver/connection.py +242 -0
  573. procesio_cli/tools/sqlserver/main.py +521 -0
  574. procesio_cli/tools/sqlserver/profiles.template.json +1 -0
  575. procesio_cli/tools/sqlserver/schema.py +600 -0
  576. procesio_cli/tools/sqlserver/tests/__init__.py +0 -0
  577. procesio_cli/tools/sqlserver/tests/conftest.py +21 -0
  578. procesio_cli/tools/sqlserver/tests/test_connection.py +75 -0
  579. procesio_cli/tools/sqlserver/tests/test_dbprofiles.py +92 -0
  580. procesio_cli/tools/sqlserver/tests/test_dbquery.py +220 -0
  581. procesio_cli/tools/sqlserver/tests/test_manifest_sync.py +68 -0
  582. procesio_cli/tools/sqlserver/tests/test_sqlserver.py +326 -0
  583. procesio_cli/tools/sqlserver/tests/test_sqlserver_schema.py +488 -0
  584. procesio_cli/tools/sqlserver/tool.yaml +123 -0
  585. procesio_cli/tools/web/README.md +381 -0
  586. procesio_cli/tools/web/SKILL.md +37 -0
  587. procesio_cli/tools/web/WEB-AUTOMATION-NOTES.md +277 -0
  588. procesio_cli/tools/web/__init__.py +21 -0
  589. procesio_cli/tools/web/actiondef.py +24 -0
  590. procesio_cli/tools/web/api.py +444 -0
  591. procesio_cli/tools/web/brokered.py +107 -0
  592. procesio_cli/tools/web/driver.py +839 -0
  593. procesio_cli/tools/web/drivercontract.py +44 -0
  594. procesio_cli/tools/web/errors.py +54 -0
  595. procesio_cli/tools/web/handlers/__init__.py +1 -0
  596. procesio_cli/tools/web/handlers/archive.py +367 -0
  597. procesio_cli/tools/web/handlers/browse.py +182 -0
  598. procesio_cli/tools/web/handlers/contacts.py +74 -0
  599. procesio_cli/tools/web/handlers/sessmgmt.py +120 -0
  600. procesio_cli/tools/web/main.py +128 -0
  601. procesio_cli/tools/web/sessions.py +253 -0
  602. procesio_cli/tools/web/steps.py +162 -0
  603. procesio_cli/tools/web/tests/__init__.py +0 -0
  604. procesio_cli/tools/web/tests/conftest.py +197 -0
  605. procesio_cli/tools/web/tests/test_api.py +330 -0
  606. procesio_cli/tools/web/tests/test_archive.py +190 -0
  607. procesio_cli/tools/web/tests/test_brokered.py +138 -0
  608. procesio_cli/tools/web/tests/test_dispatch.py +132 -0
  609. procesio_cli/tools/web/tests/test_errors.py +60 -0
  610. procesio_cli/tools/web/tests/test_manifest_sync.py +76 -0
  611. procesio_cli/tools/web/tests/test_page_contacts.py +68 -0
  612. procesio_cli/tools/web/tests/test_sessions.py +145 -0
  613. procesio_cli/tools/web/tests/test_steps.py +210 -0
  614. procesio_cli/tools/web/tool.yaml +184 -0
  615. procesio_cli/tools/xlsx/README.md +34 -0
  616. procesio_cli/tools/xlsx/SKILL.md +28 -0
  617. procesio_cli/tools/xlsx/__init__.py +0 -0
  618. procesio_cli/tools/xlsx/main.py +129 -0
  619. procesio_cli/tools/xlsx/reader.py +91 -0
  620. procesio_cli/tools/xlsx/tests/__init__.py +0 -0
  621. procesio_cli/tools/xlsx/tests/conftest.py +39 -0
  622. procesio_cli/tools/xlsx/tests/test_xlsx.py +103 -0
  623. procesio_cli/tools/xlsx/tool.yaml +30 -0
  624. procesio_cli/webplatform/aat_mcp/AAT-MCP-NOTES.md +244 -0
  625. procesio_cli/webplatform/aat_mcp/README.md +78 -0
  626. procesio_cli/webplatform/aat_mcp/bridge.py +705 -0
  627. procesio_cli/webplatform/aat_mcp/chat_server.py +194 -0
  628. procesio_cli/webplatform/aat_mcp/chat_surface.snapshot.json +668 -0
  629. procesio_cli/webplatform/aat_mcp/chat_surface.yaml +372 -0
  630. procesio_cli/webplatform/aat_mcp/gate.py +68 -0
  631. procesio_cli/webplatform/aat_mcp/http_server.py +139 -0
  632. procesio_cli/webplatform/aat_mcp/protocol.py +175 -0
  633. procesio_cli/webplatform/aat_mcp/server.py +306 -0
  634. procesio_cli/webplatform/aat_mcp/tests/conftest.py +8 -0
  635. procesio_cli/webplatform/aat_mcp/tests/test_aat_mcp.py +255 -0
  636. procesio_cli/webplatform/aat_mcp/tests/test_chat_surface.py +247 -0
  637. procesio_cli/webplatform/aat_mcp/tests/test_discovery.py +489 -0
  638. procesio_cli/webplatform/aat_mcp/tests/test_multiuser.py +86 -0
  639. procesio_cli/webplatform/aat_mcp/tests/test_progressive_discovery.py +89 -0
  640. procesio_cli/webplatform/aat_mcp/tests/test_protocol.py +180 -0
  641. procesio_cli/webplatform/aat_mcp/tests/test_skill_discovery.py +23 -0
  642. procesio_cli-0.1.1.dist-info/METADATA +467 -0
  643. procesio_cli-0.1.1.dist-info/RECORD +647 -0
  644. procesio_cli-0.1.1.dist-info/WHEEL +4 -0
  645. procesio_cli-0.1.1.dist-info/entry_points.txt +7 -0
  646. procesio_cli-0.1.1.dist-info/licenses/LICENSE +201 -0
  647. procesio_cli-0.1.1.dist-info/licenses/NOTICE +5 -0
@@ -0,0 +1,1991 @@
1
+ """Process (Flow) builder — data-driven from the action catalog.
2
+
3
+ config -> FlowRequestDto (POST /api/Projects). Verified live: a built flow passes
4
+ POST /api/Projects/validate, persists with the client-supplied Id, and runs to
5
+ STATUS_FINISH with output variables populated.
6
+
7
+ Design:
8
+ * variables -> VariableDto[] (INPUT/PROCESS/OUTPUT, primitive or model type, isList)
9
+ * actions -> ActionDto[] (any catalog template by name; Start/Stop auto-injected)
10
+ * params -> ParametersDto[] bound to the template's properties (TabPropertyId),
11
+ as a literal Value, a variable ref ("<%0%>" + Variable[]), a variable
12
+ with an attribute path, or a multi-var template.
13
+ * edges -> PortDto[] (explicit branching, or implicit linear chain)
14
+
15
+ The action template structure (property ids/labels) comes from ctx["catalog"] —
16
+ the live workspace catalog (custom/connector actions included), with the bundled
17
+ snapshot as the offline default. The build step itself is PURE.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import copy
22
+ import json
23
+ import re
24
+ import uuid
25
+ from functools import lru_cache
26
+ from pathlib import Path
27
+
28
+ from tools.procesio.dto import refdata
29
+ from tools.procesio.dto.framework import Component
30
+ from tools.procesio.dto.process import naming
31
+ from tools.procesio.sqlformat import format_sql, is_unreadable
32
+ from tools.procesio.errors import RemovalBlocked, UsageError
33
+
34
+ DIR = Path(__file__).resolve().parent
35
+ _CATALOG = DIR.parent / "data" / "action_catalog.json"
36
+
37
+ START_TEMPLATE = "c0e32108-6e3e-4ab8-96bd-cd61be6edb33"
38
+ STOP_TEMPLATE = "c0e32108-6e3e-4ab8-96bd-cd61be6edb34"
39
+ JOIN_TEMPLATE = "fb6a9d14-dd15-420d-a2b2-fc637c0c37c6" # flow-control Join (inputPorts=-1)
40
+ NULL_GUID = "00000000-0000-0000-0000-000000000000"
41
+ # Setting types the designer never binds a variable to: it saves the template's own value
42
+ # verbatim. `auto` (engine-written, read-only) joined `ignore` with the Auto variable type;
43
+ # the designer treats the two identically, so the builder must too.
44
+ _HIDDEN_SETTING_TYPES = ("ignore", "auto")
45
+ _STARTED_FLOW_SUFFIX = "a03fe2" # Call Subprocess: the started-flow (instance id) slot
46
+ _BUILTIN_MODELS = {"file": "10c6ac59-3929-49e6-99dc-121212121219"} # the platform File model
47
+ _GUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
48
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
49
+ ERROR_MODEL = "10c6ac59-3929-49e6-99dc-121212121220" # ErrorDataModel (error-port variable type)
50
+ _DIRECTION = {"input": 10, "process": 20, "output": 30, "processinfo": 40}
51
+
52
+ # The platform's own per-run facts (workspaceId, instanceId, templateId, name, startedOn).
53
+ # It is an ordinary flow variable of a fixed model, NOT an ambient runtime object, and the
54
+ # designer adds it the first time something references it - so a flow rebuilt from a config
55
+ # silently loses it unless the config can ask for it.
56
+ SYSTEM_DATA_MODEL_ID = "10c6ac59-3929-49e6-99dc-121212121221"
57
+ PROCESS_INFO_NAME = "ProcessInfo"
58
+ # actions that have NO error port (per PROCESIO) — onError is rejected on these
59
+ _NO_ERROR_PORT = {"start", "stop", "join", "for each", "foreach"}
60
+ # Some action names are ambiguous (a versioned/list variant shares the name). For
61
+ # the flow-control actions the exact template matters (Join's inputPorts=-1 lets
62
+ # branches converge), so pin them by id; everything else resolves by name/id.
63
+ _CONTROL_TEMPLATE = {"start": START_TEMPLATE, "stop": STOP_TEMPLATE, "join": JOIN_TEMPLATE}
64
+
65
+ # Superseded/frozen action versions -> (latest replacement, migration hint).
66
+ # PROCESIO keeps frozen version pins (e.g. "Call API v3") in the catalog next to
67
+ # the live/latest action ("Call API"); referencing a pin silently builds on an
68
+ # OLD generation (different output props, missing features). The builder BLOCKS a
69
+ # superseded ref with an actionable message so a build can't quietly regress —
70
+ # pass --force (ctx["_force"]) to pin one intentionally. "Unversioned = latest" is
71
+ # NOT a universal rule (e.g. Read Mailbox V2 > Read Mailbox), so this is a CURATED
72
+ # map, not a heuristic; extend it as PROCESIO deprecates actions (source of truth:
73
+ # the live "To be decommissioned" palette folder + /api/Actions). See
74
+ # dto/process/description.md "Call API version" gotcha.
75
+ SUPERSEDED_ACTIONS = {
76
+ "call api v1": ("Call API", "outputs are Response Status / Response Body / Response Headers / Response File"),
77
+ "call api v2": ("Call API", "outputs are Response Status / Response Body / Response Headers / Response File"),
78
+ "call api v3": ("Call API", "rename output params Status Output -> Response Status, Body Output -> Response Body"),
79
+ "execute query v1": ("Execute Query", "bind SQL via @params (config map), not inline <%N%>"),
80
+ "execute query v2": ("Execute Query", "bind SQL via @params (config map), not inline <%N%>"),
81
+ "execute command v1": ("Execute Command", ""),
82
+ }
83
+
84
+
85
+ def _new_id(ctx) -> str:
86
+ return (ctx.get("new_id") or (lambda: str(uuid.uuid4())))()
87
+
88
+
89
+ # -- catalog ------------------------------------------------------------------
90
+
91
+ @lru_cache(maxsize=1)
92
+ def _bundled_catalog() -> list[dict]:
93
+ return json.loads(_CATALOG.read_text(encoding="utf-8")).get("actions", [])
94
+
95
+
96
+ def catalog_index(extra: list[dict] | None = None) -> dict:
97
+ """name(lower)->template and actionId->template. `extra` (a live catalog) wins."""
98
+ idx: dict[str, dict] = {}
99
+ for a in _bundled_catalog():
100
+ idx[(a.get("name") or "").strip().lower()] = a
101
+ if a.get("actionId"):
102
+ idx[a["actionId"]] = a
103
+ for a in (extra or []):
104
+ idx[(a.get("name") or "").strip().lower()] = a
105
+ if a.get("actionId"):
106
+ idx[a["actionId"]] = a
107
+ return idx
108
+
109
+
110
+ def _resolve_template(ref: str, ctx: dict) -> dict:
111
+ idx = ctx.get("catalog") or catalog_index()
112
+ key = (ref or "").strip().lower()
113
+ sup = SUPERSEDED_ACTIONS.get(key)
114
+ if sup and not ctx.get("_force"): # block frozen version pins
115
+ latest, hint = sup
116
+ raise UsageError(
117
+ f"action {ref!r} is a superseded/frozen version — use {latest!r} (the "
118
+ f"live/latest action)" + (f"; {hint}" if hint else "") +
119
+ ". Pass --force to pin the old version intentionally.")
120
+ if key in _CONTROL_TEMPLATE: # pin ambiguous control actions
121
+ t = idx.get(_CONTROL_TEMPLATE[key])
122
+ if t:
123
+ return t
124
+ t = idx.get(key) or idx.get(ref)
125
+ if not t:
126
+ raise UsageError(f"unknown action {ref!r} — not in the action catalog")
127
+ return t
128
+
129
+
130
+ def _property_index(template: dict) -> dict:
131
+ """label(lower)->property and id->property, across all tabs (recursing into
132
+ side-pannel nested sub-properties whose `value` holds child settings)."""
133
+ out: dict[str, dict] = {}
134
+
135
+ def walk(settings):
136
+ for s in settings or []:
137
+ if s.get("label"):
138
+ out.setdefault(s["label"].strip().lower(), s)
139
+ if s.get("id"):
140
+ out[s["id"]] = s
141
+ val = s.get("value")
142
+ if isinstance(val, list): # side-pannel nested props
143
+ walk(val)
144
+ for tab in template.get("configuration", []):
145
+ walk(tab.get("settings", []))
146
+ return out
147
+
148
+
149
+ # -- parameter binding --------------------------------------------------------
150
+
151
+ _GUID_RE = re.compile(r"^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-"
152
+ r"[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$")
153
+
154
+
155
+ def _attr_chain(path, model_id, ctx: dict):
156
+ """A VariableAttributeDto chain {attributeId, nextAttribute} from a list of attribute names.
157
+
158
+ `attributeId` is parsed as a Guid by the API, so a NAME written there is not a soft failure —
159
+ the whole save is rejected. Each step is resolved against the model the previous step lands in
160
+ (`ctx['model_attrs']`, filled by prepare_ctx), which is also what lets a nested path work.
161
+
162
+ A value that already looks like a Guid passes straight through, so a caller who knows the
163
+ attribute id never needs the model resolved. When the model is unknown (offline, or an untyped
164
+ variable) the name passes through unchanged, preserving the old behaviour rather than
165
+ inventing an id.
166
+ """
167
+ if not path:
168
+ return None
169
+ head, *rest = path
170
+ key = str(head).strip()
171
+ attrs = (ctx.get("model_attrs") or {}).get(model_id) if model_id else None
172
+
173
+ if attrs is None or _GUID_RE.match(key):
174
+ return {"attributeId": key, "nextAttribute": _attr_chain(rest, None, ctx)}
175
+
176
+ found = attrs.get(key.lower())
177
+ if found is None:
178
+ known = ", ".join(sorted(a.get("name", k) for k, a in attrs.items())) or "(none)"
179
+ raise UsageError(
180
+ f"attribute {head!r} is not on this variable's data model. Known attributes: {known}")
181
+ return {"attributeId": found["id"],
182
+ "nextAttribute": _attr_chain(rest, found.get("dataTypeId"), ctx)}
183
+
184
+
185
+ def _model_of(ctx: dict, var_name) -> str | None:
186
+ """The data-model id of a bound variable, so its attribute path can be resolved."""
187
+ return (ctx.get("var_models") or {}).get(str(var_name).strip().lower())
188
+
189
+
190
+ def _make_parameter(prop_id: str, binding, ctx: dict, counter: list | None = None) -> dict:
191
+ """Build one ParametersDto. Binding forms:
192
+ literal scalar | {"value": x} -> Value=x, Variable=[]
193
+ {"var": name[, "path": [...]]} -> Value="<%N%>", Variable=[{N, varId, attr}]
194
+ {"template": "..<%0%>..<%1%>..", "vars": [name, ...]} -> multi-var
195
+ `counter` is a per-ACTION global index ([next]); the <%N%> ids are unique
196
+ across all of an action's parameters so actions that reuse one property id for
197
+ several params (Javascript Code+Output, Decisional Cases+Default) don't collide.
198
+ """
199
+ var_ids = ctx.get("var_ids", {})
200
+ counter = counter if counter is not None else [0]
201
+
202
+ def resolve_var(name):
203
+ vid = var_ids.get(str(name).strip().lower())
204
+ if not vid:
205
+ raise UsageError(f"parameter binds unknown variable {name!r}")
206
+ return vid
207
+
208
+ def take():
209
+ i = counter[0]
210
+ counter[0] += 1
211
+ return i
212
+
213
+ variable = []
214
+ if isinstance(binding, dict) and "var" in binding:
215
+ attr = _attr_chain(binding.get("path"), _model_of(ctx, binding["var"]), ctx)
216
+ idx = take()
217
+ variable = [{"id": idx, "variableId": resolve_var(binding["var"]), "attribute": attr}]
218
+ value = f"<%{idx}%>"
219
+ elif isinstance(binding, dict) and "template" in binding:
220
+ # remap the template's local <%i%> -> a global index unique to the action
221
+ local_to_global: dict[int, int] = {}
222
+ for i, nm in enumerate(binding.get("vars", [])):
223
+ g = take()
224
+ local_to_global[i] = g
225
+ if isinstance(nm, dict): # {"var": name, "path": [...]} attr ref
226
+ attr = _attr_chain(nm.get("path"), _model_of(ctx, nm["var"]), ctx)
227
+ variable.append({"id": g, "variableId": resolve_var(nm["var"]),
228
+ "attribute": attr})
229
+ else:
230
+ variable.append({"id": g, "variableId": resolve_var(nm), "attribute": None})
231
+ def _remap(t): # remap <%i%> in str/list/dict leaves
232
+ if isinstance(t, str):
233
+ return re.sub(r"<%(\d+)%>",
234
+ lambda m: f"<%{local_to_global.get(int(m.group(1)), m.group(1))}%>", t)
235
+ if isinstance(t, list):
236
+ return [_remap(x) for x in t]
237
+ if isinstance(t, dict):
238
+ return {k: _remap(v) for k, v in t.items()}
239
+ return t
240
+ value = _remap(binding["template"])
241
+ elif isinstance(binding, dict) and "credential" in binding:
242
+ # a credential reference: Value = the credential INSTANCE gid; the action's
243
+ # credentialsTemplateId (the credential TYPE) is already in the template config.
244
+ gid = binding["credential"]
245
+ if not _GUID_RE.match(str(gid or "")):
246
+ raise UsageError(
247
+ f"credential binding must be a credential instance GUID, got {gid!r}")
248
+ value = gid
249
+ elif isinstance(binding, dict) and "value" in binding:
250
+ value = binding["value"]
251
+ else:
252
+ value = binding # bare literal
253
+ return {"TabPropertyId": prop_id, "Variable": variable, "Value": value}
254
+
255
+
256
+ def _operand(spec, var_ids: dict, counter: list, ctx: dict) -> dict:
257
+ """A decisional operand {value, variable}. A var operand -> value '<%N%>' with
258
+ the variable recorded; a literal -> value=literal, variable=[]."""
259
+ if isinstance(spec, dict) and "var" in spec:
260
+ vid = var_ids.get(spec["var"].strip().lower())
261
+ if not vid:
262
+ raise UsageError(f"condition references unknown variable {spec['var']!r}")
263
+ idx = counter[0]
264
+ counter[0] += 1
265
+ attr = _attr_chain(spec.get("path"), _model_of(ctx, spec.get("var")), ctx)
266
+ return {"value": f"<%{idx}%>",
267
+ "variable": [{"id": idx, "variableId": vid, "attribute": attr}]}
268
+ val = spec.get("value") if isinstance(spec, dict) else spec
269
+ return {"value": val, "variable": []}
270
+
271
+
272
+ _LOGIC = {"and": 1, "or": 0}
273
+
274
+
275
+ def _build_decisional(template: dict, spec: dict, node_id: dict, var_ids: dict, ctx: dict):
276
+ """Build the Cases + Default ParametersDto and the branch PortDto list for a
277
+ Decisional / AI Decisional action. Returns (params, ports). For AI Decisional a
278
+ case `condition` is a natural-language STRING (not an operator tree)."""
279
+ pidx = _property_index(template)
280
+ cases_prop = pidx["cases"]
281
+ cases_pid = cases_prop["id"]
282
+ is_ai = cases_prop.get("type") == "ai-decisional-case"
283
+ default_pid = pidx["default"]["id"]
284
+ branches = spec["branches"]
285
+ counter = [0] # shared <%N%> index across all conditions
286
+ all_vars: list = []
287
+ cases = []
288
+ ports = []
289
+ ai_meta: list = [] # designer name/internalId per case (AI only)
290
+ dec_meta: list = [] # designer name per case (rule-based Decisional)
291
+ default_target = None
292
+ src = node_id[spec["id"]]
293
+ for i, br in enumerate(b for b in branches if not b.get("default")):
294
+ to = br["to"]
295
+ if to not in node_id:
296
+ raise UsageError(f"branch target {to!r} is not an action id")
297
+ if is_ai:
298
+ cond = br.get("condition")
299
+ if not isinstance(cond, str) or not cond.strip():
300
+ raise UsageError(
301
+ f"AI Decisional case -> {to!r} needs a natural-language 'condition' string")
302
+ cases.append({"id": i, "actionid": node_id[to], "condition": cond})
303
+ ai_meta.append({"id": i,
304
+ "name": br.get("name") or naming.derive_branch_name(br, True) or f"Case {i + 1}",
305
+ "internalId": _new_id(ctx)})
306
+ else:
307
+ conds = []
308
+ for j, cnd in enumerate(br.get("when", [])):
309
+ left = _operand(cnd.get("left"), var_ids, counter, ctx)
310
+ right = _operand(cnd.get("right", ""), var_ids, counter, ctx)
311
+ all_vars.extend(left["variable"]); all_vars.extend(right["variable"])
312
+ conds.append({"id": j, "operator": cnd["op"],
313
+ "logicOperator": _LOGIC.get(str(cnd.get("logic", "and")).lower(), 1),
314
+ "leftOperator": left, "rightOperator": right, "auxOperator": None})
315
+ cases.append({"id": i, "actionid": node_id[to], "condition": conds})
316
+ dec_meta.append({"id": i,
317
+ "name": br.get("name") or naming.derive_branch_name(br, False) or f"Case {i + 1}"})
318
+ ports.append(_port(src, node_id[to], ctx))
319
+ for br in branches:
320
+ if br.get("default"):
321
+ default_target = node_id[br["to"]]
322
+ ports.append(_port(src, node_id[br["to"]], ctx, {"Data": {"isDefault": "default"}}))
323
+ if is_ai:
324
+ ctx.setdefault("_ai_case_meta", {})[src] = ai_meta
325
+ else:
326
+ ctx.setdefault("_dec_case_meta", {})[src] = dec_meta
327
+ params = [
328
+ {"TabPropertyId": cases_pid, "Variable": all_vars, "Value": cases},
329
+ {"TabPropertyId": default_pid, "Variable": [], "Value": default_target},
330
+ ]
331
+ return params, ports
332
+
333
+
334
+ def _doc_mapper_property(template: dict) -> dict | None:
335
+ for tab in template.get("configuration", []):
336
+ for s in tab.get("settings", []):
337
+ if isinstance(s.get("value"), list):
338
+ for sub in s["value"]:
339
+ if sub.get("type") == "document-mapper":
340
+ return sub
341
+ return None
342
+
343
+
344
+ def _build_doc_mapper(template: dict, doc_map: dict, doc_vars: dict, var_ids: dict,
345
+ ctx: dict, counter: list) -> dict | None:
346
+ """Build the Map Document Data (document-mapper) parameter: map each document
347
+ template variable -> a process variable. doc_map = {docVarName: procVarName | {var:...}}."""
348
+ prop = _doc_mapper_property(template)
349
+ if prop is None:
350
+ raise UsageError("this action has no document-mapper property")
351
+ rows = []
352
+ for i, (doc_var, binding) in enumerate(doc_map.items()):
353
+ doc_id = doc_vars.get(doc_var.strip().lower()) or doc_var
354
+ dst_idx = counter[0]
355
+ counter[0] += 1
356
+ destination = {"id": dst_idx, "variableId": doc_id, "attribute": None}
357
+ src = _operand(binding if isinstance(binding, dict) else {"var": binding},
358
+ var_ids, counter, ctx)
359
+ rows.append({"id": i, "source": src, "destination": destination})
360
+ # source/destination carry their variables inline; the param-level Variable is empty
361
+ return {"TabPropertyId": prop["id"], "Variable": [], "Value": rows}
362
+
363
+
364
+ # -- Data Store node: Set Values (data-store-mapper) + Where (data-store-decisional) --
365
+ # The Set Values row reuses the document-mapper source operand but names its target
366
+ # `column` (resolved by NAME). The Where value is a DataStoreQueryFilterGroupDto -- the
367
+ # filter tree shared verbatim with Process-Execution (Domain/Enums/DataStore/Filter).
368
+ _DS_OPERATORS = {
369
+ "equals": "EQUALS", "eq": "EQUALS", "==": "EQUALS",
370
+ "notequals": "DOES_NOT_EQUAL", "ne": "DOES_NOT_EQUAL", "!=": "DOES_NOT_EQUAL",
371
+ "contains": "CONTAINS", "notcontains": "DOES_NOT_CONTAIN",
372
+ "doesnotcontain": "DOES_NOT_CONTAIN",
373
+ "greaterthan": "GREATER_THAN", "gt": "GREATER_THAN",
374
+ "greaterthanorequal": "GREATER_THAN_OR_EQUAL_TO", "gte": "GREATER_THAN_OR_EQUAL_TO",
375
+ "lessthan": "LESS_THAN", "lt": "LESS_THAN",
376
+ "lessthanorequal": "LESS_THAN_OR_EQUAL_TO", "lte": "LESS_THAN_OR_EQUAL_TO",
377
+ "istrue": "IS_TRUE", "isfalse": "IS_FALSE",
378
+ "isempty": "IS_EMPTY", "isnotempty": "IS_NOT_EMPTY",
379
+ "belongs": "BELONGS", "notbelongs": "DOES_NOT_BELONG", "doesnotbelong": "DOES_NOT_BELONG",
380
+ }
381
+ _DS_LOGIC = {"and": 1, "or": 2}
382
+
383
+
384
+ def _ds_side_property(template: dict, wanted_type: str) -> dict | None:
385
+ """Find a Data Store side-panel child setting by its control `type`."""
386
+ for tab in template.get("configuration", []):
387
+ for s in tab.get("settings", []):
388
+ if s.get("type") == wanted_type:
389
+ return s
390
+ if isinstance(s.get("value"), list):
391
+ for sub in s["value"]:
392
+ if sub.get("type") == wanted_type:
393
+ return sub
394
+ return None
395
+
396
+
397
+ def _ds_resolve_operator(raw) -> str:
398
+ if isinstance(raw, bool):
399
+ raise UsageError("Data Store Where operator must be an operator name")
400
+ key = str(raw).strip().lower().replace("_", "").replace(" ", "")
401
+ if key in _DS_OPERATORS:
402
+ return _DS_OPERATORS[key]
403
+ up = str(raw).strip().upper()
404
+ if up in set(_DS_OPERATORS.values()): # already a decisional token
405
+ return up
406
+ raise UsageError(f"unknown Data Store Where operator {raw!r}; known: "
407
+ f"{', '.join(sorted(_DS_OPERATORS))}")
408
+
409
+
410
+ def _build_ds_mapper(template: dict, ds_map: dict, var_ids: dict, ctx: dict, counter: list) -> dict:
411
+ """Data Store `Set Values` (data-store-mapper) for InsertRows/UpdateRows: each entry
412
+ maps a COLUMN NAME -> a value binding. Runtime row {id, source:{value,variable}, column}.
413
+ A bare string/number binding is a LITERAL value (unlike docMap, where bare = variable)."""
414
+ prop = _ds_side_property(template, "data-store-mapper")
415
+ if prop is None:
416
+ raise UsageError("this action has no data-store-mapper property "
417
+ "(Set Values renders only for a Data Store Insert/Update)")
418
+ rows = []
419
+ for i, (col, binding) in enumerate(ds_map.items()):
420
+ rows.append({"id": i, "source": _operand(binding, var_ids, counter, ctx), "column": col})
421
+ return {"TabPropertyId": prop["id"], "Variable": [], "Value": rows}
422
+
423
+
424
+ def _ds_mapper_config(rows):
425
+ """data-store-mapper designer rows -- the shape PROCESIO's designer actually stores:
426
+ {id, left:<columnName>, right:<varId|literal>} (verified against a live export)."""
427
+ out = []
428
+ for r in rows or []:
429
+ src = r.get("source") or {}
430
+ right = _ref_from_variable(src.get("variable"))
431
+ if right is None:
432
+ right = src.get("value")
433
+ out.append({"id": r.get("id"), "left": r.get("column"), "right": right})
434
+ return out
435
+
436
+
437
+ def _build_ds_where(template: dict, where_spec, var_ids: dict, ctx: dict, counter: list) -> dict:
438
+ """Data Store `Where` (data-store-decisional) for Select/Update/Delete. The runtime value
439
+ is a JSON ARRAY of InputDataStoreDecisional (Domain.DTOs.DataStore.Decisional) -- the SAME
440
+ operand/condition tree a rule `Decisional` uses, NOT the REST filter group. Verified live:
441
+ [{id:<GUID>, condition:[{id, operator, logicOperator, leftOperator, rightOperator, auxOperator}]}]
442
+ The element `id` MUST be a GUID (an int there deserializes into a .NET Guid? as null ->
443
+ 'Nullable object must have a value.'). The LEFT operand is the COLUMN (a literal display
444
+ name); the RIGHT operand is the compared value/variable, whose `<%N%>` binding lives INLINE
445
+ (the parameter-level `Variable` stays EMPTY, unlike a rule Decisional). `logicOperator`
446
+ connects a condition to the NEXT one (last condition = 0). Spec forms:
447
+ [{column, op, value}] -> ANDed conditions
448
+ {logic:'and'|'or', conditions:[{column, op, value}]}
449
+ `op` is a decisional operator name (equals, notEquals, contains, greaterThan, ...)."""
450
+ prop = _ds_side_property(template, "data-store-decisional")
451
+ if prop is None:
452
+ raise UsageError("this action has no data-store-decisional property "
453
+ "(Where renders only for a Data Store Select/Update/Delete)")
454
+ if isinstance(where_spec, dict):
455
+ logic = _DS_LOGIC.get(str(where_spec.get("logic", "and")).strip().lower(), 1)
456
+ conds_spec = where_spec.get("conditions") or where_spec.get("where") or []
457
+ else:
458
+ logic = 1
459
+ conds_spec = where_spec or []
460
+ n = len(conds_spec)
461
+ conds = []
462
+ for j, c in enumerate(conds_spec):
463
+ if not isinstance(c, dict):
464
+ raise UsageError("each Data Store Where condition is {column, op, value}")
465
+ col = c.get("column") or c.get("name")
466
+ if not col:
467
+ raise UsageError("each Data Store Where condition needs a 'column' (display name)")
468
+ op = _ds_resolve_operator(c.get("op", c.get("operator", "equals")))
469
+ left = {"value": col, "variable": []}
470
+ right = _operand(c.get("value", ""), var_ids, counter, ctx)
471
+ conds.append({"id": j, "operator": op,
472
+ "logicOperator": (logic if j < n - 1 else 0),
473
+ "leftOperator": left, "rightOperator": right, "auxOperator": None})
474
+ element = {"id": _new_id(ctx), "condition": conds}
475
+ return {"TabPropertyId": prop["id"], "Variable": [], "Value": [element]}
476
+
477
+
478
+ def _ds_where_config(value, ctx):
479
+ """data-store-decisional designer value -- the shape PROCESIO's designer stores (verified
480
+ against a live export): [{id, name:'Where', target:'', condition:[{id, uid, operator,
481
+ leftOperator, rightOperator, auxOperator, logicOperator}]}]. Operands -> designer form via
482
+ _operand_config; the element `id` is carried over from the runtime element so the two layers
483
+ agree, and each condition gets a fresh `uid` (a client render key)."""
484
+ out = []
485
+ for el in value or []:
486
+ conds = []
487
+ for c in el.get("condition") or []:
488
+ conds.append({
489
+ "id": c.get("id"), "uid": _new_id(ctx), "operator": c.get("operator"),
490
+ "leftOperator": _operand_config(c.get("leftOperator")),
491
+ "rightOperator": _operand_config(c.get("rightOperator")),
492
+ "auxOperator": {"variable": "", "attribute": {"id": "", "nextAttribute": None}, "value": ""},
493
+ "logicOperator": c.get("logicOperator", 1),
494
+ })
495
+ out.append({"id": el.get("id"), "name": "Where", "target": "", "condition": conds})
496
+ return out
497
+
498
+
499
+ # -- Query Store node: one SQL statement over the workspace's Data Stores ------
500
+ # The Query (code-editor) carries `<%N%>` placeholders whose Variable[] entries are
501
+ # CHIPS bound to a Data Store / column id (type dataStoreTable / dataStoreColumn), NOT a
502
+ # process variable. `@param` in the SQL is bound by the Parameters map (b103). Outputs
503
+ # (Result Rows / Total Count) bind like any {var}. One <%N%> id sequence per node.
504
+ # Verified against a live export. Query Store is MySQL and runs SELECT/WITH plus
505
+ # INSERT/UPDATE/DELETE/REPLACE; see PROCESIO-QUERY-STORE-NOTES.md.
506
+ _QS_TOKEN = re.compile(r"{{\s*(ds|table|col|column)\s*:\s*([^}]+?)\s*}}", re.I)
507
+
508
+
509
+ def _qs_prop(pidx: dict, label: str, ctype: str) -> dict:
510
+ p = pidx.get(label) or next((s for s in pidx.values() if s.get("type") == ctype), None)
511
+ if p is None:
512
+ raise UsageError(f"Query Store template has no {label!r} ({ctype}) property")
513
+ return p
514
+
515
+
516
+ def _qs_store_id(ref: str, ctx: dict) -> str:
517
+ ref = ref.strip()
518
+ if _GUID_RE.match(ref):
519
+ return ref
520
+ ent = (ctx.get("datastore_meta") or {}).get(ref.lower())
521
+ if not ent:
522
+ known = ", ".join(sorted(k for k in (ctx.get("datastore_meta") or {})
523
+ if not _GUID_RE.match(k))) or "(none)"
524
+ raise UsageError(f"queryStore references unknown Data Store {ref!r}; known: {known}")
525
+ return ent["id"]
526
+
527
+
528
+ def _qs_col_id(store_ref: str, store_id: str, col: str, ctx: dict) -> str:
529
+ col = col.strip()
530
+ if _GUID_RE.match(col):
531
+ return col
532
+ meta = ctx.get("datastore_meta") or {}
533
+ ent = meta.get(store_ref.strip().lower()) or meta.get(str(store_id).lower()) or {}
534
+ cid = (ent.get("columns") or {}).get(col.lower())
535
+ if not cid:
536
+ known = ", ".join(sorted(ent.get("columns") or {})) or "(none)"
537
+ raise UsageError(f"queryStore column {col!r} is not on Data Store {store_ref!r}; known: {known}")
538
+ return cid
539
+
540
+
541
+ def fetch_datastore_meta(client) -> dict:
542
+ """Data Store name and column name -> id, keyed lower-case and also by id.
543
+
544
+ What a Query Store chip needs: a chip carries ids, while a statement is written with names.
545
+ Separate from prepare_ctx because a surgical editor needs ONLY this - prepare_ctx also pulls
546
+ the whole live action catalog, which is the expensive half and irrelevant when no node is
547
+ being built from scratch.
548
+
549
+ Failure is swallowed on purpose: with no metadata a caller can still pass GUIDs directly, and
550
+ the resolver says which names it knows.
551
+ """
552
+ dsmeta: dict = {}
553
+ try:
554
+ r = client.get("/api/DataStore", {"pageNumber": 1, "pageItemCount": 500})
555
+ items = r.get("pageItems") if isinstance(r, dict) else r
556
+ for it in items or []:
557
+ sid = it.get("id") or it.get("Id")
558
+ cols = {}
559
+ for c in (it.get("columns") or it.get("Columns") or []):
560
+ cn = (c.get("name") or c.get("Name") or "").strip().lower()
561
+ if cn:
562
+ cols[cn] = c.get("columnId") or c.get("ColumnId") or c.get("id")
563
+ entry = {"id": sid, "columns": cols}
564
+ nm = (it.get("name") or it.get("Name") or "").strip().lower()
565
+ if nm:
566
+ dsmeta[nm] = entry
567
+ if sid:
568
+ dsmeta[str(sid).lower()] = entry
569
+ except Exception: # noqa: BLE001 - names unresolved -> caller can pass GUIDs
570
+ pass
571
+ return dsmeta
572
+
573
+
574
+ def render_query_tokens(sql: str, ctx: dict, counter: list | None = None) -> tuple[str, list]:
575
+ """Turn a Query Store statement's {{ds:Store}} / {{col:Store.Column}} tokens into the stored
576
+ shape: `<%N%>` placeholders plus the CHIP list their Variable[] carries.
577
+
578
+ Each token occurrence consumes its own index, so naming the same column five times yields
579
+ five chips pointing at it - which is how a statement converts one column several times (a
580
+ Guid column, for instance, has to be reassembled from its bytes, and a placeholder is not
581
+ reusable). The caller owns the counter because a node numbers its Query chips and its
582
+ Parameters-map sources from ONE sequence.
583
+
584
+ Laid out over lines before it is stored. A Query Store node IS its statement, so a statement
585
+ concatenated onto one line makes the node unreadable in the designer; the formatter only
586
+ moves whitespace outside string literals and hands back the input untouched if it cannot
587
+ prove that (tools/procesio/sqlformat.py).
588
+ """
589
+ if counter is None:
590
+ counter = [0]
591
+ chips: list = []
592
+
593
+ def sub(m):
594
+ kind = m.group(1).lower()
595
+ ref = m.group(2).strip()
596
+ idx = counter[0]
597
+ counter[0] += 1
598
+ if kind in ("ds", "table"):
599
+ chips.append({"id": idx, "type": "dataStoreTable", "variableId": None,
600
+ "dataStoreId": _qs_store_id(ref, ctx), "attribute": None})
601
+ else:
602
+ store_ref, _, col = ref.rpartition(".")
603
+ if not store_ref or not col:
604
+ raise UsageError(f"queryStore column token must be col:Store.Column, got {ref!r}")
605
+ sid = _qs_store_id(store_ref, ctx)
606
+ chips.append({"id": idx, "type": "dataStoreColumn", "variableId": None,
607
+ "dataStoreId": sid, "columnId": _qs_col_id(store_ref, sid, col, ctx),
608
+ "attribute": None})
609
+ return f"<%{idx}%>"
610
+
611
+ return format_sql(_QS_TOKEN.sub(sub, sql)), chips
612
+
613
+
614
+ def _build_query_store(template: dict, spec: dict, var_ids: dict, ctx: dict, counter: list) -> list:
615
+ """Query Store node Parameters (b102 Query + chips, b103 Parameters map, b104 Time Out,
616
+ b105 Result Rows, b106 Total Count) from a friendly spec:
617
+ {sql, params:{name: binding}, timeout?, resultRows?, totalCount?}
618
+ sql uses {{ds:StoreNameOrId}} / {{col:StoreNameOrId.ColNameOrId}} tokens (-> id chips)
619
+ and @param (-> Parameters map). ids resolve from ctx['datastore_meta'] or pass through
620
+ when already a GUID. One <%N%> id sequence across the whole node."""
621
+ if not isinstance(spec, dict) or not spec.get("sql"):
622
+ raise UsageError("queryStore needs a 'sql' string")
623
+ pidx = _property_index(template)
624
+ q_prop = _qs_prop(pidx, "query", "code-editor")
625
+ p_prop = _qs_prop(pidx, "parameters config tab", "map-parameters")
626
+ text, chips = render_query_tokens(spec["sql"], ctx, counter)
627
+ out = [{"TabPropertyId": q_prop["id"], "Variable": chips, "Value": text}]
628
+ rows = []
629
+ for i, (pname, binding) in enumerate((spec.get("params") or {}).items()):
630
+ srcbind = binding if isinstance(binding, dict) else {"value": binding}
631
+ rows.append({"id": i, "source": _operand(srcbind, var_ids, counter, ctx),
632
+ "destination": {"value": str(pname).lstrip("@"), "variable": []}})
633
+ out.append({"TabPropertyId": p_prop["id"], "Variable": [], "Value": rows})
634
+ t_prop = pidx.get("time out")
635
+ if t_prop:
636
+ out.append({"TabPropertyId": t_prop["id"], "Variable": [],
637
+ "Value": str(spec.get("timeout", 1800))})
638
+ for label, key in (("result rows", "resultRows"), ("total count", "totalCount")):
639
+ b = spec.get(key)
640
+ prop = pidx.get(label)
641
+ if b is not None and prop is not None:
642
+ out.append(_make_parameter(prop["id"], b if isinstance(b, dict) else {"var": b},
643
+ ctx, counter))
644
+ return out
645
+
646
+
647
+ def _qs_params_config(rows) -> list:
648
+ """map-parameters runtime rows {id, source:{value,variable}, destination:{value}} ->
649
+ designer rows {id, destination:<paramName>, source:<varId|literal>} (verified export)."""
650
+ out = []
651
+ for r in rows or []:
652
+ src = r.get("source") or {}
653
+ source = _ref_from_variable(src.get("variable"))
654
+ if source is None:
655
+ source = src.get("value")
656
+ dest = r.get("destination")
657
+ out.append({"id": r.get("id"),
658
+ "destination": dest.get("value") if isinstance(dest, dict) else dest,
659
+ "source": source})
660
+ return out
661
+
662
+
663
+ def _ensure_query_store_properties(template: dict, params: list) -> list:
664
+ """Seed a Query Store node's Parameters map (b103) EMPTY when unbound, like the Execute
665
+ Query bind: the designer renders it from the template and refuses the save without it."""
666
+ if (template.get("name") or "").strip().lower() != "query store":
667
+ return params
668
+ pidx = _property_index(template)
669
+ p = pidx.get("parameters config tab") or next(
670
+ (s for s in pidx.values() if s.get("type") == "map-parameters"), None)
671
+ if p and not any(x.get("TabPropertyId") == p["id"] for x in params):
672
+ params = params + [{"TabPropertyId": p["id"], "Variable": [], "Value": []}]
673
+ return params
674
+
675
+
676
+ def _is_foreach(a: dict) -> bool:
677
+ return (a.get("action") or "").strip().lower() in ("for each", "foreach")
678
+
679
+
680
+ def _build_subprocess(template: dict, spec: dict, var_ids: dict, ctx: dict, counter: list) -> list[dict]:
681
+ """Build the Call/Trigger Subprocess params: started-flow placeholder, the target
682
+ flow (flow-list), and the input/output variable maps. spec:
683
+ {target, inputs: {subInputVarId: parentBinding}, outputs: {parentVarName: subOutputVarId}}.
684
+ Sub-process variable ids are GUIDs the caller supplies (e.g. from read-flow-graph on
685
+ the target flow); parent vars resolve by name. Runtime row shape matches the document
686
+ mapper ({id, source, destination}); the designer shape is {id, subprocess, process}."""
687
+ pidx = _property_index(template)
688
+ out: list[dict] = []
689
+ # The started-flow slot is the hidden property `...a03fe2`. Match it by id: the template
690
+ # now declares further hidden slots (the child-instance-id outputs), so "the first hidden
691
+ # property" only holds while the catalog keeps them in that order.
692
+ hidden = [s for s in pidx.values() if s.get("type") in _HIDDEN_SETTING_TYPES]
693
+ started = next((s for s in hidden if str(s.get("id", "")).endswith(_STARTED_FLOW_SUFFIX)),
694
+ hidden[0] if hidden else None)
695
+ if started:
696
+ out.append({"TabPropertyId": started["id"], "Variable": [], "Value": NULL_GUID})
697
+ flowlist = next((s for s in pidx.values() if s.get("type") == "flow-list"), None)
698
+ if not flowlist:
699
+ raise UsageError("this action has no flow-list property (not a subprocess action)")
700
+ out.append({"TabPropertyId": flowlist["id"], "Variable": [], "Value": spec.get("target")})
701
+ in_prop = next((s for s in pidx.values() if s.get("type") == "process-inputs"), None)
702
+ if in_prop:
703
+ rows = []
704
+ for i, (sub_var_id, binding) in enumerate((spec.get("inputs") or {}).items()):
705
+ src = _operand(binding if isinstance(binding, dict) else {"var": binding},
706
+ var_ids, counter, ctx)
707
+ rows.append({"id": i, "source": src,
708
+ "destination": {"id": i, "variableId": sub_var_id, "attribute": None}})
709
+ out.append({"TabPropertyId": in_prop["id"], "Variable": [], "Value": rows})
710
+ out_prop = next((s for s in pidx.values() if s.get("type") == "process-outputs"), None)
711
+ if out_prop:
712
+ rows = []
713
+ for i, (parent_var, sub_var_id) in enumerate((spec.get("outputs") or {}).items()):
714
+ pid = var_ids.get(str(parent_var).strip().lower())
715
+ if not pid:
716
+ raise UsageError(f"subprocess output binds unknown variable {parent_var!r}")
717
+ idx = counter[0]
718
+ counter[0] += 1
719
+ rows.append({"id": i,
720
+ "source": {"value": f"<%{idx}%>",
721
+ "variable": [{"id": idx, "variableId": sub_var_id, "attribute": None}]},
722
+ "destination": {"id": i, "variableId": pid, "attribute": None}})
723
+ out.append({"TabPropertyId": out_prop["id"], "Variable": [], "Value": rows})
724
+ return out
725
+
726
+
727
+ def _subprocess_map_config(rows, is_input):
728
+ """process-inputs/outputs runtime rows {id, source:{value,variable}, destination:{variableId}}
729
+ -> designer rows {id, subprocess:<sub var>, process:<parent var>}. Sub side = destination
730
+ for inputs, source for outputs; parent side is the other."""
731
+ out = []
732
+ for r in rows or []:
733
+ src = _ref_from_variable((r.get("source") or {}).get("variable")) or (r.get("source") or {}).get("value")
734
+ dst = (r.get("destination") or {}).get("variableId")
735
+ sub, proc = (dst, src) if is_input else (src, dst)
736
+ out.append({"id": r.get("id"), "subprocess": sub, "process": proc})
737
+ return out
738
+
739
+
740
+ def _refuse_unreadable_code(tpl: dict, params: list, action_name: str) -> None:
741
+ """Refuse a script body stored as one long line.
742
+
743
+ SQL can be laid out safely, because only whitespace outside string literals moves and the
744
+ token stream is checked afterwards. A script cannot: reformatting someone's JavaScript or
745
+ Python risks changing it. So the guard here REFUSES instead of rewriting - the author lays
746
+ it out, which costs a moment, and nobody inherits a node whose logic is a single
747
+ three-thousand-character line that the designer shows as one row scrolling off the screen.
748
+ """
749
+ by_id = {p["id"]: p for p in _property_index(tpl).values() if isinstance(p, dict) and p.get("id")}
750
+ for prm in params:
751
+ prop = by_id.get(prm.get("TabPropertyId"))
752
+ if not prop or prop.get("type") != "code-editor":
753
+ continue
754
+ if str(prop.get("label", "")).strip().lower() == "query":
755
+ continue # SQL: the formatter already handled it
756
+ value = prm.get("Value")
757
+ if is_unreadable(value):
758
+ raise UsageError(
759
+ f"{action_name}: the script in '{prop.get('label')}' is "
760
+ f"{len(value.strip())} characters on ONE line. Lay it out over lines, one "
761
+ f"statement per line - a script node's body is its logic, and a single long "
762
+ f"line is unreadable in the designer. (The builder will not reformat code for "
763
+ f"you: rewriting a script risks changing it.)")
764
+
765
+
766
+ def _action_parameters(template: dict, params: dict, ctx: dict, counter: list | None = None) -> list[dict]:
767
+ if not params:
768
+ return []
769
+ pidx = _property_index(template)
770
+ counter = counter if counter is not None else [0] # global <%N%> index per action
771
+ out = []
772
+ for key, binding in params.items():
773
+ prop = pidx.get(str(key).strip().lower()) or pidx.get(key)
774
+ if not prop:
775
+ known = sorted({s["label"] for t in template.get("configuration", [])
776
+ for s in t.get("settings", []) if s.get("label")})
777
+ raise UsageError(
778
+ f"action {template.get('name')!r} has no property {key!r}; known: {known}")
779
+ out.append(_make_parameter(prop["id"], binding, ctx, counter))
780
+ return out
781
+
782
+
783
+ def _is_template_default_output(s: dict) -> bool:
784
+ """A template setting the builder must MATERIALISE rather than drop: a
785
+ runtime-written OUTPUT slot the compact config never binds — `direction == 2`
786
+ (output) or `type == "ignore"` — that the template gives a concrete SCALAR
787
+ default `value`. The designer copies these verbatim into every flow; a
788
+ binding-driven builder emits one param per binding and so drops them, and a
789
+ `For Each` with no `Zero based list index` slot then hangs having iterated its
790
+ body zero times (measured: iterations went 0→3 when exactly the two are
791
+ restored, and the drop was located here). A list/dict `value` is a nested
792
+ side-panel container, not a default; None/'' is no default, both skipped.
793
+ Empirically, over the live 238-action catalog (2026-10), this class is exactly four
794
+ settings across two actions: `For Each` (`61724e`=-1, `808b0d`=the datetime sentinel)
795
+ and `Call Subprocess` (`a03fe2`=the null-guid started-flow slot, `a03fe3`=the
796
+ null-guid child-instance-id output the engine fills; its list sibling `a03fe4` has no
797
+ default and is skipped)."""
798
+ if not ((s.get("direction") == 2) or (s.get("type") in _HIDDEN_SETTING_TYPES)):
799
+ return False
800
+ v = s.get("value")
801
+ if v is None or isinstance(v, (list, dict)):
802
+ return False
803
+ if isinstance(v, str):
804
+ return v.strip() != ""
805
+ return isinstance(v, (int, float, bool))
806
+
807
+
808
+ def _template_default_params(template: dict, existing: list) -> list[dict]:
809
+ """After an action's binding-driven params are assembled, materialise each
810
+ template setting that is a defaulted output slot NO binding (nor a bespoke
811
+ builder — subprocess/decisional/doc-mapper) already supplied. Emits the
812
+ designer's own shape `{TabPropertyId, Value: <template default>, Variable: []}`,
813
+ deduped by `TabPropertyId` against the params already built.
814
+
815
+ ⚠ It is a pure APPEND, driven off the template: an action whose template has no
816
+ such setting yields `[]`, so its build is BYTE-IDENTICAL to before this fix
817
+ (verified by a byte-identical rebuild across the catalog). `Call Subprocess`'s `a03fe2` is already emitted
818
+ by `_build_subprocess`, so the dedupe makes this a no-op there — no double.
819
+ General over the catalog, not a `For Each` special-case."""
820
+ have = {str(p.get("TabPropertyId")) for p in existing}
821
+ seen: set = set()
822
+ out: list[dict] = []
823
+
824
+ def walk(settings):
825
+ for s in settings or []:
826
+ sid = s.get("id")
827
+ if sid and sid not in seen:
828
+ seen.add(sid)
829
+ if _is_template_default_output(s) and str(sid) not in have:
830
+ out.append({"TabPropertyId": sid, "Value": s.get("value"), "Variable": []})
831
+ val = s.get("value")
832
+ if isinstance(val, list): # side-pannel nested props
833
+ walk(val)
834
+ for tab in template.get("configuration", []):
835
+ walk(tab.get("settings", []))
836
+ return out
837
+
838
+
839
+ # -- nodes / ports ------------------------------------------------------------
840
+
841
+ def _config_value_from_param(value, variable):
842
+ """The DESIGNER reads each setting's `value` from CustomData.configuration; the
843
+ RUNTIME reads Parameters[]. They mirror each other EXCEPT a variable reference is
844
+ a raw variableId (with .attrId for attribute paths) in the config but a `<%N%>`
845
+ placeholder in the Parameter. Convert a Parameter (Value + Variable[]) to the
846
+ designer config value by replacing each `<%N%>` with its bound variable id."""
847
+ if not variable:
848
+ return value
849
+ idmap = {}
850
+ for v in variable:
851
+ if v.get("type") == "dataStoreTable": # Query Store chip (Data Store)
852
+ idmap[v.get("id")] = f"ds.{v.get('dataStoreId')}"
853
+ continue
854
+ if v.get("type") == "dataStoreColumn": # Query Store chip (column)
855
+ idmap[v.get("id")] = f"ds.{v.get('dataStoreId')}.{v.get('columnId')}"
856
+ continue
857
+ ref = v.get("variableId")
858
+ attr = v.get("attribute")
859
+ while attr:
860
+ ref = f"{ref}.{attr.get('attributeId')}"
861
+ attr = attr.get("nextAttribute")
862
+ idmap[v.get("id")] = ref
863
+
864
+ def repl(t):
865
+ if isinstance(t, str):
866
+ return re.sub(r"<%(\d+)%>",
867
+ lambda m: str(idmap.get(int(m.group(1)), m.group(0))), t)
868
+ if isinstance(t, list):
869
+ return [repl(x) for x in t]
870
+ if isinstance(t, dict):
871
+ return {k: repl(x) for k, x in t.items()}
872
+ return t
873
+ return repl(value)
874
+
875
+
876
+ def _ref_from_variable(vs):
877
+ """A param variable list -> the raw variableId (with .attrId for an attribute
878
+ path), the way the designer config stores a variable reference."""
879
+ if not vs:
880
+ return None
881
+ ref = vs[0].get("variableId")
882
+ attr = vs[0].get("attribute")
883
+ while attr:
884
+ ref = f"{ref}.{attr.get('attributeId')}"
885
+ attr = attr.get("nextAttribute")
886
+ return ref
887
+
888
+
889
+ def _docmapper_config(rows):
890
+ """document-mapper: runtime rows {id, source:{value,variable}, destination:{variableId}}
891
+ -> designer rows {id, process:<varId|literal>, document:<docVarId>}."""
892
+ out = []
893
+ for r in rows or []:
894
+ src = r.get("source") or {}
895
+ proc = _ref_from_variable(src.get("variable")) or src.get("value")
896
+ out.append({"id": r.get("id"), "process": proc,
897
+ "document": (r.get("destination") or {}).get("variableId")})
898
+ return out
899
+
900
+
901
+ def _operand_config(op):
902
+ """decisional operand: runtime {value:'<%N%>', variable:[{variableId,attr}]}
903
+ -> designer {variable:'', attribute:{id:'',nextAttribute:None}, value:<varId|literal>}.
904
+ Literal values are coerced to strings (the designer operand field is text)."""
905
+ op = op or {}
906
+ val = _ref_from_variable(op.get("variable"))
907
+ if val is None:
908
+ val = op.get("value", "")
909
+ val = "" if val is None else val if isinstance(val, str) else str(val)
910
+ return {"variable": "", "attribute": {"id": "", "nextAttribute": None}, "value": val}
911
+
912
+
913
+ def _decisional_cases_config(cases, ctx, aid=None):
914
+ """decisional-case: runtime {id, actionid, condition:[{operator, leftOperator,...}]}
915
+ -> designer {id, name, target, condition:[{id, uid, operator, leftOperator,...}]}.
916
+ The per-case name comes from the meta _build_decisional stashes (keyed by node id),
917
+ so a rule-based branch reads its condition label instead of "Case N"."""
918
+ meta = {m["id"]: m for m in (ctx.get("_dec_case_meta") or {}).get(aid, [])}
919
+ out = []
920
+ for i, c in enumerate(cases or []):
921
+ conds = []
922
+ for cnd in c.get("condition") or []:
923
+ conds.append({
924
+ "id": cnd.get("id"), "uid": _new_id(ctx), "operator": cnd.get("operator"),
925
+ "logicOperator": cnd.get("logicOperator", 1),
926
+ "leftOperator": _operand_config(cnd.get("leftOperator")),
927
+ "rightOperator": _operand_config(cnd.get("rightOperator")),
928
+ "auxOperator": {"variable": "", "attribute": {"id": "", "nextAttribute": None}, "value": ""},
929
+ # fields the designer requires to render/validate a condition
930
+ "value": None,
931
+ "rightOperandAsListRequired": False,
932
+ "operandsAsListOptional": True,
933
+ })
934
+ out.append({"id": c.get("id"),
935
+ "name": meta.get(c.get("id"), {}).get("name") or c.get("name") or f"Case {i + 1}",
936
+ "target": c.get("actionid"), "condition": conds})
937
+ return out
938
+
939
+
940
+ def _ai_decisional_cases_config(cases, ctx, aid):
941
+ """ai-decisional-case: runtime {id, actionid, condition:<str>} -> designer
942
+ {id, name, target, condition:<str>, internalId}. name/internalId come from the meta
943
+ stashed by _build_decisional (keyed by node id); fall back to defaults if absent."""
944
+ meta = {m["id"]: m for m in (ctx.get("_ai_case_meta") or {}).get(aid, [])}
945
+ out = []
946
+ for i, c in enumerate(cases or []):
947
+ m = meta.get(c.get("id"), {})
948
+ out.append({"id": c.get("id"), "name": m.get("name") or f"Case {i + 1}",
949
+ "target": c.get("actionid"), "condition": c.get("condition"),
950
+ "internalId": m.get("internalId") or _new_id(ctx)})
951
+ return out
952
+
953
+
954
+ # config settings whose DESIGNER value shape differs from the runtime Parameter and
955
+ # needs a bespoke transform (otherwise the designer shows the row empty + invalid)
956
+ def _apply_values_to_config(cfg_tree: list, params: list, ctx: dict, aid=None) -> None:
957
+ """Set each configuration setting's `value` to mirror its Parameter in the shape
958
+ the DESIGNER expects, so the action shows configured (the flow runs from
959
+ Parameters regardless). Most settings mirror the Parameter with <%N%>->varId;
960
+ document-mapper / decisional-case need a bespoke shape."""
961
+ byid: dict = {}
962
+
963
+ def index(settings):
964
+ for s in settings or []:
965
+ if s.get("id"):
966
+ byid[s["id"]] = s
967
+ if isinstance(s.get("value"), list):
968
+ index(s["value"])
969
+ for tab in cfg_tree:
970
+ index(tab.get("settings", []))
971
+ for p in params:
972
+ s = byid.get(p.get("TabPropertyId"))
973
+ if s is None:
974
+ continue
975
+ stype = s.get("type")
976
+ if stype == "document-mapper":
977
+ s["value"] = _docmapper_config(p.get("Value"))
978
+ elif stype == "data-store-mapper":
979
+ s["value"] = _ds_mapper_config(p.get("Value"))
980
+ elif stype == "data-store-decisional":
981
+ s["value"] = _ds_where_config(p.get("Value"), ctx)
982
+ elif stype == "decisional-case":
983
+ s["value"] = _decisional_cases_config(p.get("Value"), ctx, aid)
984
+ elif stype == "ai-decisional-case":
985
+ s["value"] = _ai_decisional_cases_config(p.get("Value"), ctx, aid)
986
+ elif stype == "process-inputs":
987
+ s["value"] = _subprocess_map_config(p.get("Value"), True)
988
+ elif stype == "process-outputs":
989
+ s["value"] = _subprocess_map_config(p.get("Value"), False)
990
+ elif stype == "map-parameters":
991
+ s["value"] = _qs_params_config(p.get("Value"))
992
+ else:
993
+ cv = _config_value_from_param(p.get("Value"), p.get("Variable"))
994
+ # DESIGNER number inputs bind a STRING: a raw int/float renders EMPTY, then
995
+ # the FE range-check reads Number("")=0 and flags "value between min and max"
996
+ # (even though the RUNTIME Parameter, which stays numeric, executes fine).
997
+ # PROCESIO's own templates store number settings as strings (e.g. "60").
998
+ if stype == "number" and isinstance(cv, (int, float)) and not isinstance(cv, bool):
999
+ cv = repr(cv) if isinstance(cv, float) else str(cv)
1000
+ s["value"] = cv
1001
+
1002
+
1003
+ # value-shapes the designer reads for the bespoke types (used by the build audit)
1004
+ _BESPOKE_REQUIRED = {"document-mapper": ("process", "document"),
1005
+ "map-parameters": ("destination",),
1006
+ "data-store-mapper": ("left", "right"),
1007
+ "data-store-decisional": ("condition",),
1008
+ "decisional-case": ("target", "condition"),
1009
+ "ai-decisional-case": ("target", "condition"),
1010
+ "process-inputs": ("subprocess", "process"),
1011
+ "process-outputs": ("subprocess", "process")}
1012
+
1013
+
1014
+ def _audit_config(actions: list) -> None:
1015
+ """Build-time guard: catch params whose DESIGNER config didn't get filled in the
1016
+ expected shape (the cause of 'action not configured properly' in the designer even
1017
+ when the flow runs). Raises UsageError listing each problem."""
1018
+ problems: list[str] = []
1019
+ for a in actions:
1020
+ cd = a.get("CustomData") or {}
1021
+ byid: dict = {}
1022
+
1023
+ def index(settings):
1024
+ for s in settings or []:
1025
+ if s.get("id"):
1026
+ byid[s["id"]] = s
1027
+ if isinstance(s.get("value"), list):
1028
+ index(s["value"])
1029
+ for tab in cd.get("configuration", []):
1030
+ index(tab.get("settings", []))
1031
+ an = a.get("ActionName")
1032
+ for p in a.get("Parameters", []):
1033
+ s = byid.get(p.get("TabPropertyId"))
1034
+ if s is None:
1035
+ problems.append(f"{an!r}: parameter {p.get('TabPropertyId')} has no config setting")
1036
+ continue
1037
+ stype = s.get("type")
1038
+ val = s.get("value")
1039
+ req = _BESPOKE_REQUIRED.get(stype)
1040
+ if req:
1041
+ rows = val if isinstance(val, list) else []
1042
+ if not all(isinstance(r, dict) and all(k in r for k in req) for r in rows):
1043
+ problems.append(f"{an!r}: {stype} config rows missing {req} (designer would show empty)")
1044
+ elif p.get("Value") not in (None, "", [], {}) and val is None:
1045
+ problems.append(f"{an!r}: setting {s.get('label')!r} has a value at runtime but empty in the designer")
1046
+ if problems:
1047
+ nl = chr(10)
1048
+ raise UsageError("process config audit failed - the designer would flag these "
1049
+ "as not configured:" + nl + " - " + (nl + " - ").join(problems))
1050
+
1051
+
1052
+ def _ensure_sql_bind_property(template: dict, params: list) -> list:
1053
+ """Give an Execute Query / Execute Command node its `Parameters config tab`, empty if unbound.
1054
+
1055
+ The builder emits only the properties a config names. The designer renders this one from the
1056
+ TEMPLATE, finds no value, and refuses the save with "Please make sure that the action is
1057
+ defined/configured properly" — so a SQL node built without it cannot be saved at all, bound or
1058
+ not. Keyed off the template NAME here because that is all the builder has; the two families
1059
+ have different bind ids and using the wrong one leaves every @param unbound at runtime.
1060
+ """
1061
+ from tools.procesio.flowmodel import sqlparam
1062
+
1063
+ family = (template.get("name") or "").strip().lower()
1064
+ if family == "execute query":
1065
+ pid = sqlparam.PID_BIND
1066
+ elif family == "execute command":
1067
+ pid = sqlparam.EC_PID_BIND
1068
+ else:
1069
+ return params
1070
+ if any(p.get("TabPropertyId") == pid for p in params):
1071
+ return params
1072
+ return params + [{"TabPropertyId": pid, "Variable": [], "Value": []}]
1073
+
1074
+
1075
+ def _ensure_engine_state_properties(template: dict, params: list) -> list:
1076
+ """Seed the ENGINE-STATE properties a template declares (`type: "ignore"`) with its own values.
1077
+
1078
+ These never appear in the designer and no config names them, so the builder used to drop them —
1079
+ and the engine then reads its own defaults. On a `For Each` that is fatal and silent: without
1080
+ `Action start time` (seeded `2010-01-01T00:00:00Z`) the elapsed time is measured from year one,
1081
+ so the loop reports "Foreach timeout exceeded!" on its first iteration whatever the cap. The
1082
+ template carries the correct seed for each one, which is why they are copied rather than
1083
+ invented. Only For Each and Call Subprocess declare any.
1084
+ """
1085
+ have = {p.get("TabPropertyId") for p in params}
1086
+ extra = []
1087
+ for cfg in template.get("configuration") or []:
1088
+ for s in cfg.get("settings") or []:
1089
+ if s.get("type") in _HIDDEN_SETTING_TYPES and s.get("id") not in have:
1090
+ extra.append({"TabPropertyId": s["id"], "Variable": [], "Value": s.get("value")})
1091
+ return params + extra if extra else params
1092
+
1093
+
1094
+ # Call API's two save-clean / run-fatal properties (PROCESIO-API-NOTES.md: "Verb is an OPTION GUID"
1095
+ # and "Call API needs Request Parameters even for a bodyless verb"). The catalog serves `Verb` with
1096
+ # no options and `Request Parameters` with no default, so neither the option resolver nor
1097
+ # `_ensure_input_defaults` can help; both are seeded here from the recovered platform constants.
1098
+ _CALL_API_VERB_GUIDS = {
1099
+ "GET": "3ab385bd-f8ae-b641-9176-e7db886aec01",
1100
+ "POST": "eb0b6e47-858e-fd43-a616-d8ffc1baec02",
1101
+ "PUT": "f0e5b463-9207-c44d-8ed5-937e5f4aec03",
1102
+ "PATCH": "2e1515c5-06e8-e24d-bc0d-b2c8ae1aec04",
1103
+ "DELETE": "cdf1a1fe-d4eb-e342-87d4-211c800aec05",
1104
+ }
1105
+ _CALL_API_VERB_SUFFIX = "8420f7790001"
1106
+ _CALL_API_REQPARAMS_SUFFIX = "8420f7790003"
1107
+
1108
+
1109
+ def _empty_request_parameters() -> dict:
1110
+ return {"body": {"type": "RAW", "value": {"BINARY": "", "FORM_DATA": [],
1111
+ "RAW": {"format": "json", "value": ""},
1112
+ "X_WWW_FORM_URLENCODED": []}},
1113
+ "headers": [], "queryParams": []}
1114
+
1115
+
1116
+ def _ensure_call_api_properties(template: dict, params: list) -> list:
1117
+ """Seed a `Call API` node's run-time-mandatory properties the config could not express.
1118
+
1119
+ `Verb` written as the word (`"GET"`) saves and validates, then dies at run time with
1120
+ `Http verb is invalid.`; a node with no `Request Parameters` at all dies with
1121
+ `CallApi has NULL value on request parameters.` (measured live 2026-09-21 building
1122
+ Chat Flow/Fetch WTB media). Resolve a verb NAME to its platform option guid, and add
1123
+ the empty payload structure when the caller bound no request parameters.
1124
+ """
1125
+ if (template.get("name") or "").strip().lower() != "call api":
1126
+ return params
1127
+ out = []
1128
+ have_req = False
1129
+ for p in params:
1130
+ pid = str(p.get("TabPropertyId") or "")
1131
+ if pid.endswith(_CALL_API_VERB_SUFFIX) and isinstance(p.get("Value"), str):
1132
+ word = p["Value"].strip().upper()
1133
+ if word in _CALL_API_VERB_GUIDS:
1134
+ p = {**p, "Value": _CALL_API_VERB_GUIDS[word]}
1135
+ if pid.endswith(_CALL_API_REQPARAMS_SUFFIX):
1136
+ have_req = True
1137
+ out.append(p)
1138
+ if not have_req:
1139
+ base = next((str(p["TabPropertyId"])[:-12] for p in params
1140
+ if str(p.get("TabPropertyId") or "").endswith(_CALL_API_VERB_SUFFIX)), None)
1141
+ if base is None:
1142
+ for cfg in template.get("configuration") or []:
1143
+ for s in cfg.get("settings") or []:
1144
+ subs = s["value"] if isinstance(s.get("value"), list) else [s]
1145
+ for sub in subs:
1146
+ if isinstance(sub, dict) and str(sub.get("id") or "").endswith(_CALL_API_REQPARAMS_SUFFIX):
1147
+ base = str(sub["id"])[:-12]
1148
+ if base is not None:
1149
+ out.append({"TabPropertyId": base + _CALL_API_REQPARAMS_SUFFIX, "Variable": [],
1150
+ "Value": _empty_request_parameters()})
1151
+ return out
1152
+
1153
+
1154
+ # The input-setting types whose template default the DESIGNER pre-fills and persists,
1155
+ # and which are safe to copy verbatim. `code-editor` is deliberately excluded: its
1156
+ # default is a placeholder function/script, and an unbound Code is a caller error we
1157
+ # do not want to paper over with a no-op body.
1158
+ _INPUT_DEFAULT_TYPES = {"check-box", "select", "number", "text"}
1159
+
1160
+
1161
+ def _ensure_input_defaults(template: dict, params: list) -> list:
1162
+ """Materialise the INPUT-side (direction 1) template defaults the designer pre-fills.
1163
+
1164
+ When an action is dropped in the designer, every input setting starts at its template
1165
+ default (Node `Timeout` 60, Format DateTime `Language` en-US, Export To CSV `Column
1166
+ delimiter` ",", ...) and that value is PERSISTED on save. The builder only emitted the
1167
+ params the caller bound, so an unbound default-bearing input was DROPPED and the engine
1168
+ then received a zero/empty value — silently wrong, and fatal for `Node`: an unbound
1169
+ `Timeout` runs as 00:00:00 and the action dies with "value ('00:00:00') must be greater
1170
+ than '00:00:00'" (measured live, B-048 cluster 2). Copy the template's own default for
1171
+ any safe scalar input the caller left unbound, so a headless-built node matches a
1172
+ designer-built one. Restricted to check-box/select/number/text with a non-empty default;
1173
+ `code-editor` placeholders and empty defaults are left out. A caller who genuinely wants
1174
+ an input empty binds it to "" explicitly, which lands in `params` and skips this.
1175
+ """
1176
+ have = {p.get("TabPropertyId") for p in params}
1177
+ extra = []
1178
+ for cfg in template.get("configuration") or []:
1179
+ for s in cfg.get("settings") or []:
1180
+ if (s.get("direction") == 1
1181
+ and s.get("type") in _INPUT_DEFAULT_TYPES
1182
+ and s.get("id") not in have
1183
+ and s.get("value") not in (None, "", [], {})):
1184
+ extra.append({"TabPropertyId": s["id"], "Variable": [], "Value": s.get("value")})
1185
+ return params + extra if extra else params
1186
+
1187
+
1188
+ def _action_node(aid, template, name, params, x, y, ctx, parent_id=None) -> dict:
1189
+ cfg_tree = copy.deepcopy(template.get("configuration", []))
1190
+ _apply_values_to_config(cfg_tree, params, ctx, aid)
1191
+ is_area = (template.get("shape") == "area")
1192
+ area = ({"width": 416, "height": 200, "x": 0, "y": 0} if is_area
1193
+ else {"width": 48, "height": 48, "x": 0, "y": 0})
1194
+ return {
1195
+ "Id": aid, "FlowId": ctx["flow_id"], "TemplateId": template["actionId"],
1196
+ "ParentId": parent_id, "VariableErrorId": None, "Status": 1, "Category": "",
1197
+ "ActionName": name, "ActionTemplateName": template.get("name") or name,
1198
+ "Ports": [], "Parameters": params,
1199
+ "CustomData": {
1200
+ "type": template.get("shape") or "square", "name": name,
1201
+ "icon": template.get("icon") or "", "position": {"x": x, "y": y},
1202
+ "configuration": cfg_tree,
1203
+ "inputPorts": template.get("inputPorts", 1),
1204
+ "outputPorts": template.get("outputPorts", 1),
1205
+ "wasDropped": True, "areaSize": area,
1206
+ },
1207
+ "IsTestable": bool(template.get("isTestable")), "IsDisabled": False,
1208
+ "BreakPoint": None, "TestValues": None, "ErrorMessage": "", "Events": None,
1209
+ }
1210
+
1211
+
1212
+ def _port(src, dst, ctx, extra=None) -> dict:
1213
+ p = {"Id": _new_id(ctx), "FlowId": ctx["flow_id"], "SourceId": src,
1214
+ "DestinationId": dst, "Type": 0, "State": 1, "Data": {}, "Errors": {}, "Config": {}}
1215
+ if extra:
1216
+ p.update(extra)
1217
+ return p
1218
+
1219
+
1220
+ # -- variables ----------------------------------------------------------------
1221
+
1222
+ def _variable(v: dict, ctx: dict) -> dict:
1223
+ name = v["name"]
1224
+ if str(v.get("direction", "")).strip().lower() == "processinfo":
1225
+ # Fixed shape: the model is the platform's and the value is written by the runtime.
1226
+ return {
1227
+ "Id": ctx["var_ids"][name.strip().lower()], "ContextId": None,
1228
+ "DataType": SYSTEM_DATA_MODEL_ID, "Type": 40, "Name": name,
1229
+ "DefaultValue": None, "IsList": False, "IsError": False, "IsRequired": False,
1230
+ }
1231
+ if "model" in v:
1232
+ ref = v["model"].strip()
1233
+ dt = ctx.get("models", {}).get(ref.lower()) or ref
1234
+ else:
1235
+ dt = refdata.primitive_type_id(v.get("type", "string"))
1236
+ direction = _DIRECTION[v.get("direction", "process").strip().lower()]
1237
+ return {
1238
+ "Id": ctx["var_ids"][name.strip().lower()], "ContextId": None, "DataType": dt,
1239
+ "Type": direction, "Name": name, "DefaultValue": v.get("default"),
1240
+ "IsList": bool(v.get("isList", False)), "IsError": bool(v.get("isError", False)),
1241
+ "IsRequired": bool(v.get("required", False)),
1242
+ }
1243
+
1244
+
1245
+ # -- main build ---------------------------------------------------------------
1246
+
1247
+ def _hoist_subprocess_literals(config: dict) -> dict:
1248
+ """Subprocess input mappings require the PARENT side to be a plain VARIABLE. PROCESIO
1249
+ tolerates a raw literal in a mapping's `process` field for **Call** Subprocess at
1250
+ runtime, but the designer marks a **Trigger** Subprocess with a literal input INVALID
1251
+ ("Mapping of required subprocess variable (X) is missing"), which blocks the launch
1252
+ (statusCode 373). For every literal subprocess input, synthesize a hidden process
1253
+ variable (DefaultValue = the literal, which IS delivered at runtime) and rewrite the
1254
+ binding to reference it. No-op when there are no literal subprocess inputs. NOTE: an
1255
+ attribute-path binding ({"var":x,"path":[...]}) also lands a dotted "varId.attrId" in
1256
+ the `process` field the designer dislikes — compute such values into a plain variable
1257
+ (e.g. a Node) before the subprocess call rather than passing a path.
1258
+ """
1259
+ def _literal(binding):
1260
+ if isinstance(binding, dict):
1261
+ if "var" in binding or "template" in binding:
1262
+ return (False, None)
1263
+ if "value" in binding:
1264
+ return (True, binding["value"])
1265
+ return (False, None)
1266
+ if isinstance(binding, (list, tuple)):
1267
+ return (False, None)
1268
+ return (True, binding) # bare scalar literal
1269
+ def _ptype(v):
1270
+ if isinstance(v, bool): return "boolean"
1271
+ if isinstance(v, int): return "integer"
1272
+ if isinstance(v, float): return "decimal"
1273
+ return "string"
1274
+ extra: list[dict] = []
1275
+ seen = {v["name"].strip().lower() for v in config.get("variables", [])}
1276
+ i = 0
1277
+ new_actions = []
1278
+ for a in config.get("actions", []):
1279
+ sp = a.get("subprocess")
1280
+ if not sp or not sp.get("inputs"):
1281
+ new_actions.append(a)
1282
+ continue
1283
+ new_inputs = dict(sp["inputs"])
1284
+ touched = False
1285
+ for sub_var_id, binding in list(new_inputs.items()):
1286
+ is_lit, lit = _literal(binding)
1287
+ if not is_lit:
1288
+ continue
1289
+ name = f"_sublit{i}"
1290
+ i += 1
1291
+ while name.lower() in seen:
1292
+ name = f"_sublit{i}"
1293
+ i += 1
1294
+ seen.add(name.lower())
1295
+ extra.append({"name": name, "type": _ptype(lit),
1296
+ "direction": "process", "default": lit})
1297
+ new_inputs[sub_var_id] = {"var": name}
1298
+ touched = True
1299
+ if touched:
1300
+ a = dict(a)
1301
+ a["subprocess"] = dict(sp)
1302
+ a["subprocess"]["inputs"] = new_inputs
1303
+ new_actions.append(a)
1304
+ if not extra:
1305
+ return config
1306
+ config = dict(config)
1307
+ config["actions"] = new_actions
1308
+ config["variables"] = list(config.get("variables", [])) + extra
1309
+ return config
1310
+
1311
+
1312
+ def build(config: dict, ctx: dict) -> dict:
1313
+ ctx = dict(ctx)
1314
+ ctx["flow_id"] = ctx.get("flow_id") or _new_id(ctx)
1315
+ config = _hoist_subprocess_literals(config)
1316
+
1317
+ # variables: assign ids first so params can reference them. On EDIT, reuse the
1318
+ # existing flow's variable ids (by name) so EXTERNAL references stay valid (a form
1319
+ # RUN_PROCESS inputMap/outputMap binds a process variable by id).
1320
+ _existing = ctx.get("existing_var_ids", {})
1321
+ var_ids = {v["name"].strip().lower():
1322
+ (_existing.get(v["name"].strip().lower()) or _new_id(ctx))
1323
+ for v in config.get("variables", [])}
1324
+ ctx["var_ids"] = var_ids
1325
+ variables = [_variable(v, ctx) for v in config.get("variables", [])]
1326
+
1327
+ # action nodes (auto Start/Stop unless the user defined their own — needed for
1328
+ # branching, where each branch must terminate at its own Stop: a Stop accepts
1329
+ # only one input port)
1330
+ user_actions = config.get("actions", [])
1331
+ ids = {a.get("id") or f"a{i}": a for i, a in enumerate(user_actions)}
1332
+ node_id = {cid: _new_id(ctx) for cid in ids}
1333
+ has_start = any((a.get("action") or "").strip().lower() == "start" for a in user_actions)
1334
+ has_stop = any((a.get("action") or "").strip().lower() == "stop" for a in user_actions)
1335
+ start_id = node_id["start"] = _new_id(ctx) if not has_start else None
1336
+ stop_id = node_id["stop"] = _new_id(ctx) if not has_stop else None
1337
+
1338
+ start_tpl = _resolve_template("Start", ctx)
1339
+ stop_tpl = _resolve_template("Stop", ctx)
1340
+ nodes = {}
1341
+ name_entries: list = [] # (cid, name, is_auto) for post-loop disambiguation
1342
+ order = list(ids.keys())
1343
+ branch_ports: list = []
1344
+ error_ports: list = [] # (cid, handler_cid)
1345
+ error_vars: list = [] # ErrorDataModel variables
1346
+ for i, cid in enumerate(order):
1347
+ a = ids[cid]
1348
+ tpl = _resolve_template(a["action"], ctx)
1349
+ counter = [0] # global <%N%> index shared across this action's params
1350
+ params = _action_parameters(tpl, a.get("params", {}), ctx, counter)
1351
+ if a.get("docMap"): # Map Document Data (Generate Document)
1352
+ dv = (ctx.get("doc_vars") or {}).get(cid, {})
1353
+ params = params + [_build_doc_mapper(tpl, a["docMap"], dv, var_ids, ctx, counter)]
1354
+ if a.get("dsMap"): # Data Store Set Values (Insert/Update)
1355
+ params = params + [_build_ds_mapper(tpl, a["dsMap"], var_ids, ctx, counter)]
1356
+ if a.get("dsWhere"): # Data Store Where (Select/Update/Delete)
1357
+ params = params + [_build_ds_where(tpl, a["dsWhere"], var_ids, ctx, counter)]
1358
+ if a.get("queryStore"): # Query Store (one SQL statement over Data Stores)
1359
+ params = params + _build_query_store(tpl, a["queryStore"], var_ids, ctx, counter)
1360
+ if a.get("branches"): # Decisional routing
1361
+ bparams, bports = _build_decisional(tpl, a, node_id, var_ids, ctx)
1362
+ params = params + bparams
1363
+ branch_ports.append((cid, bports))
1364
+ if a.get("subprocess"): # Call/Trigger Subprocess variable mapping
1365
+ params = params + _build_subprocess(tpl, a["subprocess"], var_ids, ctx, counter)
1366
+ # materialise the template's default-valued OUTPUT slots that no binding (nor a
1367
+ # bespoke builder above) supplied — the designer copies these verbatim and a
1368
+ # binding-driven builder drops them, which is why a programmatic For Each hangs.
1369
+ # Deduped against everything already built; a template with no
1370
+ # such setting appends nothing, so non-loop builds stay byte-identical.
1371
+ params = params + _template_default_params(tpl, params)
1372
+ _refuse_unreadable_code(tpl, params, a.get("action", cid))
1373
+ parent_id = None
1374
+ if a.get("parent"):
1375
+ pcid = a["parent"]
1376
+ if pcid not in node_id or node_id.get(pcid) is None:
1377
+ raise UsageError(f"action {cid!r} parent {pcid!r} is not an action id")
1378
+ if _is_foreach(a) and _is_foreach(ids.get(pcid, {})):
1379
+ raise UsageError(
1380
+ f"a For Each ({cid!r}) cannot be nested directly inside another For "
1381
+ f"Each ({pcid!r}); put a Call/Trigger Subprocess inside the loop and "
1382
+ f"place the inner For Each in that subprocess")
1383
+ parent_id = node_id[pcid]
1384
+ explicit = a.get("name")
1385
+ derived = None if explicit else naming.derive_action_name(a, tpl.get("name") or "")
1386
+ node_name = explicit or derived or tpl.get("name")
1387
+ # ONLY an enriched name is disambiguated; a template-name fallback (Stop, Node,
1388
+ # Generate GUID) stays shared across nodes, exactly as before auto-naming.
1389
+ name_entries.append((cid, node_name, derived is not None))
1390
+ params = _ensure_sql_bind_property(tpl, params)
1391
+ params = _ensure_engine_state_properties(tpl, params)
1392
+ params = _ensure_input_defaults(tpl, params)
1393
+ params = _ensure_call_api_properties(tpl, params)
1394
+ params = _ensure_query_store_properties(tpl, params)
1395
+ nodes[cid] = _action_node(node_id[cid], tpl, node_name,
1396
+ params, 100 + 300 * (i + 1), 300, ctx, parent_id)
1397
+ if a.get("onError"): # error port -> handler + capture error variable
1398
+ if (a.get("action") or "").strip().lower() in _NO_ERROR_PORT:
1399
+ raise UsageError(f"action {a['action']!r} has no error port "
1400
+ f"(Start/Stop/Join/For Each don't) — remove onError")
1401
+ handler = a["onError"]
1402
+ if handler not in node_id:
1403
+ raise UsageError(f"onError target {handler!r} is not an action id")
1404
+ ev_id = _new_id(ctx)
1405
+ error_vars.append({
1406
+ "Id": ev_id, "ContextId": None, "DataType": ERROR_MODEL, "Type": 20,
1407
+ "Name": f"{cid}_error", "DefaultValue": None, "IsList": False,
1408
+ "IsError": True, "IsRequired": False,
1409
+ })
1410
+ nodes[cid]["VariableErrorId"] = ev_id
1411
+ error_ports.append((cid, handler))
1412
+ if not has_start:
1413
+ nodes["start"] = _action_node(start_id, start_tpl, "Start", [], 100, 300, ctx)
1414
+ if not has_stop:
1415
+ nodes["stop"] = _action_node(stop_id, stop_tpl, "Stop", [],
1416
+ 100 + 300 * (len(order) + 1), 300, ctx)
1417
+ entry = "start" if not has_start else order[0]
1418
+
1419
+ # edges: explicit, or implicit linear start->...->stop
1420
+ edges = config.get("edges")
1421
+ if not edges:
1422
+ chain = (["start"] if not has_start else []) + order + (["stop"] if not has_stop else [])
1423
+ edges = [[chain[i], chain[i + 1]] for i in range(len(chain) - 1)]
1424
+ # A Decisional's OUTGOING routing is defined entirely by its `branches` (added below),
1425
+ # so it must never also get a linear/explicit outgoing edge - that double-ports the
1426
+ # node and the BE validator rejects it ("multiple incoming"/"duplicate connection").
1427
+ branch_srcs = {cid for cid, _ in branch_ports}
1428
+ # entry edge (null -> first node)
1429
+ nodes[entry]["Ports"].append(_port(NULL_GUID, node_id[entry], ctx))
1430
+ for e in edges:
1431
+ if isinstance(e, dict):
1432
+ frm, to = e["from"], e["to"]
1433
+ extra = {k: v for k, v in e.items() if k in ("Type", "Config", "Data")}
1434
+ else:
1435
+ frm, to = e[0], e[1]
1436
+ extra = None
1437
+ if frm not in node_id or to not in node_id:
1438
+ raise UsageError(f"edge references unknown action id(s): {frm!r}->{to!r}")
1439
+ if node_id[frm] is None or node_id[to] is None:
1440
+ raise UsageError(
1441
+ f"edge {frm!r}->{to!r} references an auto Start/Stop that wasn't created "
1442
+ f"(you defined your own Start/Stop action) — wire to your own action id")
1443
+ if frm in branch_srcs:
1444
+ continue # a Decisional routes only through its branch ports
1445
+ nodes[frm]["Ports"].append(_port(node_id[frm], node_id[to], ctx, extra))
1446
+ # branch (decisional) ports are defined by the action's `branches`, not edges
1447
+ for cid, bports in branch_ports:
1448
+ nodes[cid]["Ports"].extend(bports)
1449
+ # error ports (Type=1, Data isDefault=error) -> handler
1450
+ for cid, handler in error_ports:
1451
+ nodes[cid]["Ports"].append(_port(node_id[cid], node_id[handler], ctx,
1452
+ {"Type": 1, "Data": {"isDefault": "error"}}))
1453
+
1454
+ seq = (["start"] if not has_start else []) + order + (["stop"] if not has_stop else [])
1455
+ # auto-name un-named actions by what they do and make the derived names unique
1456
+ # within the flow. Cosmetic only (every reference is by id); explicit names untouched.
1457
+ for cid, nm in naming.disambiguate(name_entries).items():
1458
+ if nodes[cid]["ActionName"] != nm:
1459
+ nodes[cid]["ActionName"] = nm
1460
+ nodes[cid]["CustomData"]["name"] = nm
1461
+ actions = [nodes[cid] for cid in seq]
1462
+ _audit_config(actions)
1463
+ _engine_layout(actions, {cid: node_id[cid] for cid in seq if node_id.get(cid)},
1464
+ ctx, config) # final tidy layout (create) / scoped (edit)
1465
+ webhooks = _build_webhooks(config.get("webhooks", []), var_ids, ctx) + config.get("_webhooks", [])
1466
+ return {
1467
+ "Id": ctx["flow_id"], "ParentId": None, "Status": 1,
1468
+ "Title": config["title"], "Description": config.get("description", ""),
1469
+ "IsValid": True, "Active": True, "Timeout": int(config.get("timeout", 0)),
1470
+ "CurrentActionId": None, "DebugMode": False, "IsNotification": False,
1471
+ "Variables": variables + error_vars, "Actions": actions, "Webhooks": webhooks,
1472
+ "CustomResponse": (_custom_response(config.get("customResponse"), var_ids, ctx)
1473
+ or config.get("_customResponse")),
1474
+ "DataRetention": None, "CanvasData": None,
1475
+ }
1476
+
1477
+
1478
+ def keep_layout_requested() -> bool:
1479
+ """PROCESIO_KEEP_LAYOUT=1 turns the automatic full layout off for every save."""
1480
+ import os
1481
+ return os.environ.get("PROCESIO_KEEP_LAYOUT", "").strip().lower() in ("1", "true", "yes")
1482
+
1483
+
1484
+ def _resolve_subset(relayout, new_cids, all_cids):
1485
+ """Map a relayout directive to a list of config ids to re-tidy, or None for a full
1486
+ layout. new_cids/all_cids are config-local ids.
1487
+
1488
+ DEFAULT on edit = a FULL layout (classic engine + clustering), the same arrangement a
1489
+ create gets, so a process edited many times never drifts into an unarranged canvas.
1490
+ Keeping the current positions is the opt-out: relayout "new" (or --keep-layout, or
1491
+ PROCESIO_KEEP_LAYOUT=1) places only the new actions, for a hand-arranged canvas."""
1492
+ if relayout is None:
1493
+ return list(new_cids or []) if keep_layout_requested() else None
1494
+ if relayout == "all":
1495
+ return None
1496
+ if relayout in ("none", []):
1497
+ return []
1498
+ if relayout == "new":
1499
+ return list(new_cids or [])
1500
+ if isinstance(relayout, list):
1501
+ return [c for c in relayout if c in all_cids]
1502
+ if isinstance(relayout, dict):
1503
+ s = set(new_cids or []) if relayout.get("new") else set()
1504
+ for c in (relayout.get("actions") or []) + (relayout.get("edited") or []):
1505
+ if c in all_cids:
1506
+ s.add(c)
1507
+ return list(s)
1508
+ return new_cids
1509
+
1510
+
1511
+ def _engine_layout(actions, id_of, ctx, config):
1512
+ """Final layout step over the assembled actions.
1513
+
1514
+ A FULL layout (a fresh build, or an edit with relayout="all") is routed through
1515
+ `adapter.layout_flow(..., cluster=True)` -- the SAME path as `relayout-process` -- so a
1516
+ newly-built process gets the whole reference arrangement: clustering (long chains fold
1517
+ into stacked shelves = the "stacking" arrangement), the branch-placement conventions
1518
+ (error/default dead-end branches drop perpendicular), fan lanes, and cycle-awareness.
1519
+ Calling the bare `engine.layout` here left `CLUSTER=False` (its default) and skipped
1520
+ every adapter post-pass, so freshly-built processes came out flat / un-stacked.
1521
+
1522
+ A PARTIAL re-tidy (an edit that repositions only a subset) stays on the bare engine in
1523
+ `subset` mode, so every untouched action is left byte-stable."""
1524
+ from tools.procesio.layout import engine
1525
+ by_id = {a["Id"]: a for a in actions}
1526
+ existing = ctx.get("existing_positions")
1527
+ if existing: # apply carried-over positions before laying out
1528
+ for cid, pos in existing.items():
1529
+ a = by_id.get(id_of.get(cid))
1530
+ if a and pos and pos.get("x") is not None:
1531
+ a["CustomData"]["position"] = {"x": pos["x"], "y": pos["y"]}
1532
+ if existing is None:
1533
+ subset = None # CREATE -> full layout
1534
+ else:
1535
+ new_cids = [c for c in id_of if c not in existing]
1536
+ sub_cids = _resolve_subset(config.get("relayout"), new_cids, list(id_of))
1537
+ subset = None if sub_cids is None else [id_of[c] for c in sub_cids if c in id_of]
1538
+
1539
+ if subset is None:
1540
+ # FULL layout: go through the adapter so clustering + the branch/fan/cycle
1541
+ # conventions apply (bare engine.layout defaults CLUSTER=False). The adapter reads
1542
+ # a flow dict, writes positions/areaSize onto a COPY of the actions, and returns
1543
+ # that bundle; copy the results back onto the real actions by id. The builder's
1544
+ # ports already carry Type=1/Data.isDefault ("error"/"default"), so the branch
1545
+ # conventions fire correctly on a fresh build.
1546
+ from tools.procesio.layout import adapter
1547
+ # Hand the adapter a position-FREE copy so its Start-anchor (built for in-place
1548
+ # relayout of a LIVE process, to keep the user's viewport) is a no-op here: a fresh
1549
+ # build has no viewport to preserve, so the layout normalizes to the canvas margin
1550
+ # (the established fresh-build contract) instead of anchoring to the provisional seed
1551
+ # grid. Arrangement (clustering / branch / fan / cycle conventions) is unaffected --
1552
+ # it is computed from the graph, not from the input positions.
1553
+ probe = [{**a, "CustomData": {k: v for k, v in a["CustomData"].items()
1554
+ if k != "position"}} for a in actions]
1555
+ res = adapter.layout_flow({"Actions": probe}, cluster=True)
1556
+ laid = {a["Id"]: a for a in (res["bundle"].get("Actions") or [])}
1557
+ for a in actions:
1558
+ src = laid.get(a["Id"])
1559
+ if src is None:
1560
+ continue
1561
+ cd = src.get("CustomData") or {}
1562
+ if cd.get("position"):
1563
+ a["CustomData"]["position"] = dict(cd["position"])
1564
+ if cd.get("areaSize") is not None:
1565
+ a["CustomData"]["areaSize"] = dict(cd["areaSize"])
1566
+ return
1567
+
1568
+ # PARTIAL re-tidy: reposition only `subset` among fixed neighbours (byte-stable rest).
1569
+ enodes = [{"id": a["Id"], "width": a["CustomData"]["areaSize"]["width"],
1570
+ "height": a["CustomData"]["areaSize"]["height"], "parent_id": a.get("ParentId"),
1571
+ "kind": a["CustomData"]["type"], "position": a["CustomData"]["position"]}
1572
+ for a in actions]
1573
+ eedges = [{"source": p["SourceId"], "dest": p["DestinationId"], "type": p.get("Type", 0)}
1574
+ for a in actions for p in a["Ports"]
1575
+ if p["SourceId"] != NULL_GUID and p.get("DestinationId")]
1576
+ res = engine.layout(enodes, eedges, subset=subset)
1577
+ parent_of = {a["Id"]: a["ParentId"] for a in actions if a.get("ParentId") in res["areas"]}
1578
+ writes = engine.designer_writes(res["positions"], res["areas"], parent_of)
1579
+ targets = set(subset)
1580
+ for a in actions:
1581
+ w = writes.get(a["Id"])
1582
+ if w and a["Id"] in targets:
1583
+ a["CustomData"]["position"] = w["pos"]
1584
+ if w["areaSize"] is not None:
1585
+ a["CustomData"]["areaSize"] = w["areaSize"]
1586
+
1587
+
1588
+ def _custom_response(spec, var_ids: dict, ctx: dict):
1589
+ """The flow's synchronous webhook response (CustomResponseDto). Returns the
1590
+ value of a process variable to the caller: {Variable:{id:0, variableId}, Value:"<%0%>"}.
1591
+ spec: {"var": name} | {"value": literal} | a bare literal."""
1592
+ if spec is None:
1593
+ return None
1594
+ if isinstance(spec, dict) and "var" in spec:
1595
+ vid = var_ids.get(spec["var"].strip().lower())
1596
+ if not vid:
1597
+ raise UsageError(f"customResponse binds unknown variable {spec['var']!r}")
1598
+ return {"Variable": {"id": 0, "variableId": vid, "attribute": None}, "Value": "<%0%>"}
1599
+ val = spec.get("value") if isinstance(spec, dict) else spec
1600
+ return {"Variable": None, "Value": val}
1601
+
1602
+
1603
+ _WH_SOURCE = {"header": 1, "query": 2, "body": 3}
1604
+
1605
+
1606
+ def _build_webhooks(specs: list, var_ids: dict, ctx: dict) -> list:
1607
+ """Attach webhooks to the flow. Each spec: {webhookId, variables:[{name, source}]}
1608
+ binds a process variable to the webhook's Header(1)/Query(2)/Body(3)."""
1609
+ out = []
1610
+ for w in specs or []:
1611
+ wvars = []
1612
+ for b in w.get("variables", []):
1613
+ vid = var_ids.get(b["name"].strip().lower())
1614
+ if not vid:
1615
+ raise UsageError(f"webhook binds unknown variable {b['name']!r}")
1616
+ wvars.append({"VariableId": vid,
1617
+ "VariableType": _WH_SOURCE.get(str(b.get("source", "body")).strip().lower(), 3)})
1618
+ out.append({"Id": _new_id(ctx), "WebhookId": w["webhookId"],
1619
+ "WebhookVariables": wvars, "IsObsoleted": False,
1620
+ "FilterRules": {"Value": [], "Parameters": []}})
1621
+ return out
1622
+
1623
+
1624
+ # -- live context + transport -------------------------------------------------
1625
+
1626
+ _CATALOG_FETCH_SECONDS = 30
1627
+
1628
+
1629
+ def _fetch_live_catalog(client, seconds: int = _CATALOG_FETCH_SECONDS):
1630
+ """`GET /api/Actions?getFullAction=true` on a HARD wall-clock deadline.
1631
+
1632
+ That endpoint serialises every action's full configuration tree and is
1633
+ occasionally pathologically slow while the rest of the API stays sub-second.
1634
+ A `requests` timeout is per-socket-read, not total, so a trickling response
1635
+ can outlive it indefinitely — which means the caller's `except Exception`
1636
+ fallback to the bundled catalog would NEVER fire and the whole build hangs
1637
+ with no output. Run it on a daemon thread and abandon it at the deadline so
1638
+ the fallback is reachable. Losing the live catalog only costs custom /
1639
+ connector actions; every stock action is in the bundled catalog."""
1640
+ import threading
1641
+ box: dict = {}
1642
+
1643
+ def _work():
1644
+ try:
1645
+ box["r"] = client.get("/api/Actions", {"getFullAction": "true"},
1646
+ read_timeout=seconds, deadline=seconds)
1647
+ except Exception as exc: # noqa: BLE001 - surfaced to the caller below
1648
+ box["err"] = exc
1649
+
1650
+ t = threading.Thread(target=_work, daemon=True)
1651
+ t.start()
1652
+ t.join(seconds)
1653
+ if t.is_alive():
1654
+ raise TimeoutError(
1655
+ f"live action catalog fetch exceeded {seconds}s - using the bundled catalog")
1656
+ if "err" in box:
1657
+ raise box["err"]
1658
+ return box.get("r")
1659
+
1660
+
1661
+ _CUSTOM_FETCH_SECONDS = 20
1662
+
1663
+
1664
+ def _fetch_custom_actions(client, seconds: int = _CUSTOM_FETCH_SECONDS):
1665
+ """The workspace's CUSTOM (connector) actions, full config, on a wall-clock
1666
+ deadline: `GET /api/Actions/node?getFullAction=true&isCustom=true`.
1667
+
1668
+ This is the narrow, fast counterpart to `_fetch_live_catalog`. It serialises
1669
+ only the handful of connectors installed in the workspace, never the whole
1670
+ catalog, so it stays quick where the full fetch is occasionally pathologically
1671
+ slow. `prepare_ctx` merges its result into the catalog UNCONDITIONALLY, which
1672
+ is what lets a config reference ANY custom action in the workspace (by name or
1673
+ by actionId) with no per-action code - even when the full-catalog fetch times
1674
+ out and the build would otherwise be left with the bundled, built-in-only
1675
+ catalog. Same daemon-thread deadline as `_fetch_live_catalog`, for the same
1676
+ reason: a trickling response must not outlive the caller's fallback."""
1677
+ import threading
1678
+ box: dict = {}
1679
+
1680
+ def _work():
1681
+ try:
1682
+ box["r"] = client.get("/api/Actions/node",
1683
+ {"getFullAction": "true", "isCustom": "true"},
1684
+ read_timeout=seconds, deadline=seconds)
1685
+ except Exception as exc: # noqa: BLE001 - surfaced to the caller below
1686
+ box["err"] = exc
1687
+
1688
+ t = threading.Thread(target=_work, daemon=True)
1689
+ t.start()
1690
+ t.join(seconds)
1691
+ if t.is_alive():
1692
+ raise TimeoutError(f"custom-action fetch exceeded {seconds}s")
1693
+ if "err" in box:
1694
+ raise box["err"]
1695
+ return box.get("r")
1696
+
1697
+
1698
+ def prepare_ctx(client, config: dict) -> dict:
1699
+ """Fetch the live workspace action catalog (custom/connector actions included)
1700
+ and resolve any `model:` variable references to live data-model ids."""
1701
+ ctx: dict = {}
1702
+ # The catalog is the bundled built-ins plus whatever the live workspace adds.
1703
+ # Two live sources, fetched independently so one failing never costs the
1704
+ # other: (1) the full live catalog (built-ins + custom) - authoritative but
1705
+ # frequently slow, so it may time out to nothing; (2) the workspace's custom
1706
+ # /connector actions alone - small and fast. Merging (2) in unconditionally is
1707
+ # what makes ANY custom action installed in the workspace resolvable by name
1708
+ # or actionId, with no per-action code, even when (1) times out and would
1709
+ # otherwise leave only the built-in catalog.
1710
+ live: list = []
1711
+ try:
1712
+ r = _fetch_live_catalog(client)
1713
+ live = (r.get("actions") if isinstance(r, dict) else r) or []
1714
+ except Exception: # noqa: BLE001 - offline / slow: lean on bundled + the custom fetch
1715
+ live = []
1716
+ custom: list = []
1717
+ try:
1718
+ rc = _fetch_custom_actions(client)
1719
+ custom = (rc.get("actions") if isinstance(rc, dict) else rc) or []
1720
+ except Exception: # noqa: BLE001 - no custom actions reachable: built-ins still resolve
1721
+ custom = []
1722
+ ctx["catalog"] = catalog_index(extra=list(live) + list(custom))
1723
+ refs = {v["model"].strip().lower() for v in config.get("variables", []) if v.get("model")}
1724
+ if refs:
1725
+ # Platform models the workspace list never returns, so `{"model": "File"}` resolves by
1726
+ # name like a workspace model does. A workspace model of the same name still wins.
1727
+ models: dict[str, str] = dict(_BUILTIN_MODELS)
1728
+ r = client.get("/api/DataTypes", {"addProperties": False, "pageNumber": 1,
1729
+ "pageItemCount": 500, "includeProcesioEntries": True,
1730
+ "includeExternalEntries": True})
1731
+ for it in (r.get("pageItems") if isinstance(r, dict) else r) or []:
1732
+ nm = (it.get("name") or it.get("Name") or "").strip().lower()
1733
+ if nm:
1734
+ models[nm] = it.get("id") or it.get("Id")
1735
+ ctx["models"] = models
1736
+
1737
+ # An attribute path is written into `attribute.attributeId`, which the API parses as a Guid, so
1738
+ # the NAME a config uses has to be resolved to an id before the save. Fetch each referenced
1739
+ # model's attributes (recursively, so a nested path can descend) and index them by name.
1740
+ model_attrs: dict[str, dict] = {}
1741
+
1742
+ def index_model(model_id: str, seen: set) -> None:
1743
+ if not model_id or model_id in model_attrs or model_id in seen:
1744
+ return
1745
+ seen.add(model_id)
1746
+ try:
1747
+ d = client.get(f"/api/DataTypes/{model_id}")
1748
+ except Exception: # noqa: BLE001 - unreadable model -> paths fall back to pass-through
1749
+ return
1750
+ d = d.get("result", d) if isinstance(d, dict) else d
1751
+ if not isinstance(d, dict):
1752
+ # `GET /api/DataTypes/{x}` answers 200 with a null body for a value it does not know
1753
+ # (an unresolved model NAME lands on that route), so treat it as unreadable too.
1754
+ return
1755
+ entries = {}
1756
+ for a in (d.get("attributes") or []):
1757
+ nm = (a.get("name") or a.get("Name") or "").strip()
1758
+ if not nm:
1759
+ continue
1760
+ child = a.get("dataTypeId") or a.get("DataTypeId")
1761
+ entries[nm.lower()] = {"id": a.get("id") or a.get("Id"), "name": nm,
1762
+ "dataTypeId": child}
1763
+ if a.get("isDataModel") or a.get("IsDataModel"):
1764
+ index_model(child, seen)
1765
+ model_attrs[model_id] = entries
1766
+
1767
+ var_models: dict[str, str] = {}
1768
+ for v in config.get("variables", []):
1769
+ ref = (v.get("model") or "").strip()
1770
+ if str(v.get("direction", "")).strip().lower() == "processinfo":
1771
+ # Its model is fixed, so a {var: ProcessInfo, path: [workspaceId]} binding can be
1772
+ # resolved to an attribute id like any other model-typed variable.
1773
+ ref = SYSTEM_DATA_MODEL_ID
1774
+ if not ref:
1775
+ continue
1776
+ mid = (ctx.get("models") or {}).get(ref.lower()) or ref
1777
+ var_models[v["name"].strip().lower()] = mid
1778
+ index_model(mid, set())
1779
+ if var_models:
1780
+ ctx["var_models"] = var_models
1781
+ if model_attrs:
1782
+ ctx["model_attrs"] = model_attrs
1783
+
1784
+ # resolve document-template variables for any action that maps them (docMap)
1785
+ doc_vars: dict[str, dict] = {}
1786
+ for a in config.get("actions", []):
1787
+ if not a.get("docMap"):
1788
+ continue
1789
+ doc_id = (a.get("params", {}) or {}).get("Select Document Template")
1790
+ if isinstance(doc_id, dict):
1791
+ doc_id = doc_id.get("value")
1792
+ if not doc_id:
1793
+ continue
1794
+ try:
1795
+ d = client.get(f"/api/DocumentTemplate/{doc_id}")
1796
+ doc_vars[a.get("id")] = {(v.get("name") or "").strip().lower(): v.get("id")
1797
+ for v in (d.get("variables") or [])}
1798
+ except Exception: # noqa: BLE001
1799
+ doc_vars[a.get("id")] = {}
1800
+ if doc_vars:
1801
+ ctx["doc_vars"] = doc_vars
1802
+
1803
+ # resolve Data Store metadata (name/column-name -> id) for Query Store chips
1804
+ if any(a.get("queryStore") for a in config.get("actions", [])):
1805
+ ctx["datastore_meta"] = fetch_datastore_meta(client)
1806
+ return ctx
1807
+
1808
+
1809
+ def _extract_id(resp, dto):
1810
+ # POST /api/Projects returns empty; the flow keeps the client-supplied Id.
1811
+ return dto.get("Id") if isinstance(dto, dict) else None
1812
+
1813
+
1814
+ def _validate(client, dto, ctx):
1815
+ """validate@source oracle. POST /api/Projects/validate -> empty = valid; a
1816
+ non-empty body / 4xx is the validation error payload (returned, not raised, so
1817
+ a dry-run still yields the DTO + the reason)."""
1818
+ try:
1819
+ res = client.post("/api/Projects/validate", dto)
1820
+ except Exception as e: # noqa: BLE001
1821
+ return {"valid": False, "detail": getattr(e, "details", str(e))}
1822
+ if isinstance(res, dict) and res.get("raw_text", "") == "":
1823
+ return {"valid": True}
1824
+ return {"valid": not res, "detail": res}
1825
+
1826
+
1827
+ def _names(items, *keys):
1828
+ """Trimmed, non-empty names off a live-flow or DTO list, tolerating both casings."""
1829
+ out = []
1830
+ for it in items or []:
1831
+ if not isinstance(it, dict):
1832
+ continue
1833
+ for k in keys:
1834
+ v = it.get(k)
1835
+ if isinstance(v, str) and v.strip():
1836
+ out.append(v.strip())
1837
+ break
1838
+ return out
1839
+
1840
+
1841
+ def _removal_gate(dto, ctx):
1842
+ """Refuse a desired-state save that would DROP actions or variables the live flow has.
1843
+
1844
+ A desired-state config replaces the definition, so anything it omits is deleted. The
1845
+ payload cannot say whether that was meant: a config with 4 actions against a live flow
1846
+ of 13 looks identical whether the author intended to remove 9 or had lost track of the
1847
+ flow. So this does not judge the removal - it makes the consequence visible BEFORE it
1848
+ is paid, and asks for a statement of intent (--allow-remove).
1849
+
1850
+ Why it is worth a gate: a write here is not recoverable, and both writers delete
1851
+ silently. `PUT /api/Projects` persists even when it answers HTTP 400 (by design - an
1852
+ unfinished process must be saveable), and the validation gate only asks whether the
1853
+ RESULT is valid, which a truncated flow usually still is. Seen live: an incomplete
1854
+ write cut a 13-action flow to 2, and the next step - reading that state and rebuilding
1855
+ from a partial picture - dropped 8 more variables.
1856
+ """
1857
+ if ctx.get("_allow_remove"):
1858
+ return
1859
+ flow = ctx.get("_live_flow") or {}
1860
+ live_actions = flow.get("actions") or flow.get("Actions") or []
1861
+ live_vars = flow.get("variables") or flow.get("Variables") or []
1862
+
1863
+ # Error ports are generated, not authored: they come and go with their owning action.
1864
+ # Keep SOURCE order - the list is read by a human or a model deciding whether the
1865
+ # removal matches intent, and a set would reshuffle it differently on every run.
1866
+ live_var_names = [
1867
+ (v.get("name") or v.get("Name") or "").strip()
1868
+ for v in live_vars
1869
+ if isinstance(v, dict) and (v.get("name") or v.get("Name"))
1870
+ and not (v.get("isError") or v.get("IsError"))]
1871
+
1872
+ kept_actions = set(_names(dto.get("Actions"), "Name", "name", "ActionName", "actionName"))
1873
+ kept_vars = set(_names(dto.get("Variables"), "Name", "name"))
1874
+
1875
+ gone_actions = [n for n in dict.fromkeys(
1876
+ _names(live_actions, "name", "Name", "actionName", "ActionName"))
1877
+ if n not in kept_actions]
1878
+ gone_vars = [n for n in dict.fromkeys(live_var_names) if n not in kept_vars]
1879
+
1880
+ if not gone_actions and not gone_vars:
1881
+ return
1882
+
1883
+ parts = []
1884
+ if gone_actions:
1885
+ parts.append(f"{len(gone_actions)} action(s): " + ", ".join(gone_actions[:12])
1886
+ + ("…" if len(gone_actions) > 12 else ""))
1887
+ if gone_vars:
1888
+ parts.append(f"{len(gone_vars)} variable(s): " + ", ".join(gone_vars[:12])
1889
+ + ("…" if len(gone_vars) > 12 else ""))
1890
+ raise RemovalBlocked(
1891
+ "refusing to write: this config omits items the live process has, so saving it "
1892
+ "would REMOVE them - " + "; ".join(parts)
1893
+ + ". Re-read the process and include them, or pass --allow-remove if the removal "
1894
+ "is intended.",
1895
+ {"actions": gone_actions, "variables": gone_vars})
1896
+
1897
+
1898
+ def _save_gate(client, dto, ctx):
1899
+ """Front-end (designer) + back-end validation gate. Runs before EVERY process
1900
+ save (create + edit); raises errors.ValidationBlocked on blocking errors unless
1901
+ ctx['_force']. See handlers/fevalidate.pre_save_validate for the FE->BE order.
1902
+
1903
+ It also STAMPS the verdict onto dto["IsValid"] before returning, which is why it
1904
+ takes the dto rather than just reporting. build() sets that field to a hardcoded
1905
+ True, and the platform never computes it - it stores whatever the body carries. So
1906
+ without the stamp every save asserts the process is valid, and a deliberate --force
1907
+ save of a half-built process is filed as a good one. Both create and edit run this,
1908
+ so stamping here covers every desired-state save from one place.
1909
+ """
1910
+ from tools.procesio.handlers.fevalidate import pre_save_validate
1911
+ report = pre_save_validate(client, dto, force=bool(ctx.get("_force")),
1912
+ include_types=not ctx.get("_no_types"))
1913
+ fe_clean = (report.get("fe") or {}).get("clean") is not False
1914
+ be_ok = (report.get("be") or {}).get("valid") is not False
1915
+ dto["IsValid"] = bool(fe_clean and be_ok)
1916
+ return report
1917
+
1918
+
1919
+ def _edit_ctx(client, resource_id, config, ctx):
1920
+ """Everything an edit carries over from the LIVE flow. Shared with the dry-run preview, so
1921
+ `--dry-run` shows the ids and positions the edit would actually write rather than a create's."""
1922
+ ctx = dict(ctx)
1923
+ ctx["flow_id"] = resource_id
1924
+ flow: dict = {}
1925
+ try: # preserve variable ids across edits
1926
+ cur = client.get(f"/api/Projects/{resource_id}")
1927
+ flow = cur.get("flow", cur) if isinstance(cur, dict) else {}
1928
+ ctx["existing_var_ids"] = {
1929
+ (v.get("name") or "").strip().lower(): v.get("id")
1930
+ for v in (flow.get("variables") or [])
1931
+ if v.get("name") and not v.get("isError")}
1932
+ except Exception: # noqa: BLE001
1933
+ pass
1934
+ try: # preserve canvas positions across edits
1935
+ from collections import defaultdict, deque
1936
+ live = flow.get("actions") or flow.get("Actions") or []
1937
+ byname = defaultdict(deque)
1938
+ for la in live:
1939
+ cd = la.get("customData") or la.get("CustomData") or {}
1940
+ byname[(la.get("actionName") or la.get("name")
1941
+ or la.get("ActionName") or "")].append(cd.get("position"))
1942
+ existing = {}
1943
+ for ca in config.get("actions", []):
1944
+ _tnm = _resolve_template(ca["action"], ctx).get("name")
1945
+ nm = ca.get("name") or naming.derive_action_name(ca, _tnm) or _tnm
1946
+ if byname[nm] and ca.get("id"):
1947
+ existing[ca["id"]] = byname[nm].popleft()
1948
+ for auto, nm in (("start", "Start"), ("stop", "Stop")):
1949
+ if byname[nm]:
1950
+ existing[auto] = byname[nm].popleft()
1951
+ ctx["existing_positions"] = {k: v for k, v in existing.items() if v}
1952
+ except Exception: # noqa: BLE001
1953
+ pass
1954
+ ctx["_live_flow"] = flow # the edit still needs the live CanvasData blob
1955
+ return ctx
1956
+
1957
+
1958
+ def _edit(client, resource_id, config, ctx):
1959
+ ctx = _edit_ctx(client, resource_id, config, ctx)
1960
+ dto = build(config, ctx)
1961
+ # Preserve the designer canvas blob across a desired-state edit. build() resets
1962
+ # CanvasData to None (a fresh process has none), but an edit must NOT drop the live
1963
+ # value: it carries the viewport pan/zoom AND the decorative Shapes, which persist
1964
+ # under canvasData.shapes (canvas-engine PRC-4391). Dropping it wipes every shape on
1965
+ # the process. Live API is camelCase (canvasData); the PUT DTO is PascalCase.
1966
+ flow = ctx.get("_live_flow") or {}
1967
+ live_canvas = flow.get("canvasData")
1968
+ if live_canvas is None:
1969
+ live_canvas = flow.get("CanvasData")
1970
+ if live_canvas is not None:
1971
+ dto["CanvasData"] = live_canvas
1972
+ _removal_gate(dto, ctx)
1973
+ _save_gate(client, dto, ctx)
1974
+ client.put("/api/Projects", dto)
1975
+ return client.get(f"/api/Projects/{resource_id}")
1976
+
1977
+
1978
+ COMPONENT = Component(
1979
+ name="process",
1980
+ description="PROCESIO process (flow): variables + actions wired into a runnable graph.",
1981
+ dir=DIR,
1982
+ build=build,
1983
+ create_endpoint=("POST", "/api/Projects"),
1984
+ get_path="/api/Projects/{id}",
1985
+ extract_id=_extract_id,
1986
+ prepare_ctx=prepare_ctx,
1987
+ validate=_validate,
1988
+ edit=_edit,
1989
+ edit_ctx=_edit_ctx,
1990
+ save_gate=_save_gate,
1991
+ )