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,1009 @@
1
+ """Form template builder — PURE config -> FormTemplateDto.
2
+
3
+ A form's layout lives in `Data.elements` (plaintext control tree). PROVEN LIVE
4
+ (2026-06-24): the runtime renders from `elements` — the encrypted `Data.code`
5
+ blob is NOT required (a form with code="" and modified elements rendered the
6
+ modified elements). So forms are fully buildable server-side.
7
+
8
+ Approach (template-merge): for each control in config, clone a captured golden
9
+ element of that type (dto/form/elements/<type>.json) so every config field stays
10
+ valid, assign fresh ids, and override the semantic configs (label, name, required,
11
+ placeholder, options, default, submit, ...). theme/messages/dataModel/variables
12
+ come from a captured Data shell.
13
+
14
+ API (verified live):
15
+ - Create: POST /api/FormTemplate (FormTemplateDto) -> {id} ; client supplies Id
16
+ - Get: GET /api/FormTemplate/{id}
17
+ - Edit: PUT /api/FormTemplate
18
+ - Publish/state: Status=1 (PUBLISHED) + State=true on the body, or PATCH /api/FormTemplate/{id}?state=
19
+ - Render: create a CustomUrl, open forms.procesio.app/{tinyUrl}
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import copy
24
+ import json
25
+ import re
26
+ import uuid
27
+ from functools import lru_cache
28
+ from pathlib import Path
29
+
30
+ from tools.procesio.client import VARIABLES_ONLY
31
+ from tools.procesio.dto.form import fieldpath
32
+ from tools.procesio.dto.framework import Component
33
+ from tools.procesio.errors import UsageError
34
+
35
+ DIR = Path(__file__).resolve().parent
36
+ _ELEMENTS = DIR / "elements"
37
+
38
+ _FORM_TYPE = {"usertask": 0, "general": 1}
39
+
40
+ # config convenience key -> the element config `key` it sets
41
+ _OVERRIDE_KEYS = {
42
+ "label", "placeholder", "tooltip", "info-text", "infoText", "defaultValue",
43
+ "required", "readonly", "visible", "rows", "mode", "multiple", "searchable",
44
+ "clearable", "regex", "submit", "disabledIfFormIsInvalid", "variation", "hasNow",
45
+ "description", "src", "url", "alt", "title", "width", "height", "linkUrl",
46
+ "maxFileSize", "canAdd", "canRemove", "addText", "min", "max", "outlined",
47
+ # task / assignment (section, tabs, table, approval controls)
48
+ "assignee", "assigneeReplacement", "approver", "approverReplacement",
49
+ }
50
+
51
+ # control event -> (config key holding the events, the event object `type`).
52
+ # VERIFIED 2026-06-25 against ui-builder source: trigger.ts TriggerKey enum (config keys),
53
+ # FieldEvents.component.vue (EventType<->key pairing), and the EventType enum in
54
+ # model/config/events/index.ts. (Fixed: open/close=SIDE_PANEL_*, step*=STEPPER_*,
55
+ # message*=CHAT_*, approvalsubmit=*_APPROVE_OR_REJECT.)
56
+ _EVENT_KEY = {
57
+ "click": "onClickEvents", "input": "onInputEvents", "change": "onInputEvents",
58
+ "focus": "onFocusEvents", "blur": "onBlurEvents", "hover": "onHoverEvents",
59
+ "ready": "onReadyEvents",
60
+ "open": "onOpenEvents", "close": "onCloseEvents",
61
+ "tabchange": "onTabChangeEvents", "rowadded": "onTableRowAddedEvents",
62
+ "rowdeleted": "onTableRowDeletedEvents", "paginated": "onPaginationChangedEvents",
63
+ "stepchange": "onStepChangeEvents", "nextstep": "onNextStepEvents",
64
+ "previousstep": "onPreviousStepEvents",
65
+ "beforeapprove": "onBeforeApproveEvents", "afterapprove": "onAfterApproveEvents",
66
+ "beforereject": "onBeforeRejectEvents", "afterreject": "onAfterRejectEvents",
67
+ "beforeapprovalsubmit": "onBeforeApprovalSubmitEvents",
68
+ "afterapprovalsubmit": "onAfterApprovalSubmitEvents",
69
+ "messagesfetch": "onMessagesFetchEvents", "messagesent": "onMessageSentEvents",
70
+ }
71
+ _EVENT_TYPE = {
72
+ "click": "CLICK", "input": "INPUT", "change": "INPUT", "focus": "FOCUS",
73
+ "blur": "BLUR", "hover": "HOVER", "ready": "READY",
74
+ "open": "SIDE_PANEL_OPEN", "close": "SIDE_PANEL_CLOSE", "tabchange": "TAB_CHANGE",
75
+ "rowadded": "TABLE_ROW_ADDED", "rowdeleted": "TABLE_ROW_DELETED",
76
+ "paginated": "TABLE_PAGINATED",
77
+ "stepchange": "STEPPER_STEP_CHANGE", "nextstep": "STEPPER_NEXT_STEP",
78
+ "previousstep": "STEPPER_PREVIOUS_STEP",
79
+ "beforeapprove": "BEFORE_APPROVE", "afterapprove": "AFTER_APPROVE",
80
+ "beforereject": "BEFORE_REJECT", "afterreject": "AFTER_REJECT",
81
+ "beforeapprovalsubmit": "BEFORE_APPROVE_OR_REJECT",
82
+ "afterapprovalsubmit": "AFTER_APPROVE_OR_REJECT",
83
+ "messagesfetch": "CHAT_MESSAGES_FETCH", "messagesent": "CHAT_MESSAGE_SENT",
84
+ }
85
+ _KEY_ALIASES = {"infoText": "info-text", "default": "defaultValue"}
86
+
87
+
88
+ def _new_id(ctx) -> str:
89
+ return (ctx.get("new_id") or (lambda: str(uuid.uuid4())))()
90
+
91
+
92
+ @lru_cache(maxsize=None)
93
+ def _golden(kind: str) -> dict:
94
+ path = _ELEMENTS / f"{kind}.json"
95
+ if not path.exists():
96
+ avail = sorted(p.stem for p in _ELEMENTS.glob("*.json"))
97
+ raise UsageError(f"unknown form control type {kind!r}; available: {avail}")
98
+ return json.loads(path.read_text(encoding="utf-8"))
99
+
100
+
101
+ @lru_cache(maxsize=1)
102
+ def _shell() -> dict:
103
+ return json.loads((DIR / "data_shell.json").read_text(encoding="utf-8"))
104
+
105
+
106
+ def control_types() -> list[str]:
107
+ return sorted(p.stem for p in _ELEMENTS.glob("*.json"))
108
+
109
+
110
+ def _slug(s: str) -> str:
111
+ return re.sub(r"[^A-Za-z0-9_]", "", (s or "").title().replace(" ", "")) or "field"
112
+
113
+
114
+ def _field_slug(s: str) -> str:
115
+ """An auto-generated field NAME: a single lowercase word. A designer save lowercases
116
+ and camelCases around underscores, so a single lowercase word survives it unchanged
117
+ and formlint.lint_field_names does not flag it. Human-readable text stays in the
118
+ field's label, not its name (_slug, which keeps case for flowgraph display, is left
119
+ for that)."""
120
+ return re.sub(r"[^a-z0-9]", "", (s or "").lower()) or "field"
121
+
122
+
123
+ def _options(raw):
124
+ """Normalize options to [{name, value}]."""
125
+ out = []
126
+ for o in raw or []:
127
+ if isinstance(o, dict):
128
+ out.append({"name": o.get("name", o.get("label", o.get("value"))),
129
+ "value": o.get("value", o.get("name"))})
130
+ else:
131
+ out.append({"name": str(o), "value": str(o)})
132
+ return out
133
+
134
+
135
+ def _set_config(configs: list, key: str, value, ctx) -> None:
136
+ for c in configs:
137
+ if c.get("key") == key:
138
+ c["value"] = value
139
+ return
140
+
141
+
142
+ @lru_cache(maxsize=1)
143
+ def _style_groups() -> list:
144
+ """Resolved theme groups (label, types, {cssVariable: {label,type}}) from the shell.
145
+ Used for per-element styling: the runtime applies an element's `style` config (a
146
+ CSSProperty[] of ENABLED overrides) on top of the theme."""
147
+ groups = []
148
+ for g in _shell().get("theme") or []:
149
+ props = {p["cssVariable"]: {"label": p.get("label", ""), "type": p.get("type")}
150
+ for p in g.get("properties") or [] if p.get("cssVariable")}
151
+ groups.append((g.get("label"), set(g.get("types") or []), props))
152
+ return groups
153
+
154
+
155
+ # Element alignment feature: three flex properties on container elements, each keyed by
156
+ # an element-scope SUFFIX. The 18 real vars are --fd/--jc/--ai × these 6 suffixes:
157
+ # --fd-<suffix> flex-direction ("Inner direction")
158
+ # --jc-<suffix> justify-content ("Inner main axis")
159
+ # --ai-<suffix> align-items ("Inner cross axis")
160
+ # Element type -> its alignment suffix (ui-builder theme.ts registrations).
161
+ _ALIGNMENT_SUFFIX = {
162
+ "section": "section", "list": "list-item", "columns": "column",
163
+ "stepper": "step", "tabs": "tab", "side-panel": "side-panel",
164
+ }
165
+
166
+
167
+ def _alignment_props(el_type: str) -> dict:
168
+ """The flex-alignment style vars a container element accepts. Additive to the golden
169
+ theme groups so alignment is usable before the golden is refreshed from the launched
170
+ form-theme.ts; when the golden ships them natively the golden's definition wins (it
171
+ carries the proper dropdown type + option list). Values pass through (standard CSS,
172
+ e.g. flex-direction row|column, justify-content flex-start|center|space-between,
173
+ align-items flex-start|center|stretch)."""
174
+ suffix = _ALIGNMENT_SUFFIX.get(el_type)
175
+ if not suffix:
176
+ return {}
177
+ return {
178
+ f"--fd-{suffix}": {"label": "Inner direction", "type": None},
179
+ f"--jc-{suffix}": {"label": "Inner main axis", "type": None},
180
+ f"--ai-{suffix}": {"label": "Inner cross axis", "type": None},
181
+ }
182
+
183
+
184
+ def _style_props_for(el_type: str):
185
+ """cssVariable -> {label,type} stylable on this element type, or None. Mirrors the
186
+ frontend getCssGroupByElementType: the FIRST theme group whose `types` include the
187
+ type (so e.g. select uses the Input group), plus the alignment vars for container
188
+ types. Golden definitions win over the additive alignment fallback."""
189
+ for _label, types, props in _style_groups():
190
+ if el_type in types:
191
+ extra = _alignment_props(el_type)
192
+ return {**extra, **props} if extra else props
193
+ return None
194
+
195
+
196
+ def _apply_element_style(spec: dict, el_type: str, configs: list, ctx) -> None:
197
+ """Set the element's `style` config from spec['style'] = {cssVariable: value} (or a
198
+ list of {cssVariable,value[,enabled]}). Stored as CSSProperty[] with enabled:true; the
199
+ runtime applies only enabled props. Unknown vars fail loud (no silent drop)."""
200
+ raw = spec.get("style")
201
+ if not raw:
202
+ return
203
+ props = _style_props_for(el_type)
204
+ if props is None:
205
+ raise UsageError(f"form control {el_type!r} has no stylable properties")
206
+ pairs = raw.items() if isinstance(raw, dict) else (
207
+ (d.get("cssVariable"), d.get("value")) for d in raw)
208
+ items = []
209
+ for cssvar, value in pairs:
210
+ meta = props.get(cssvar)
211
+ if not meta:
212
+ raise UsageError(f"{el_type}: unknown style variable {cssvar!r}; "
213
+ f"valid: {sorted(props)}")
214
+ items.append({"label": meta["label"], "value": str(value),
215
+ "cssVariable": cssvar, "type": meta["type"], "enabled": True})
216
+ _set_config(configs, "style", items, ctx)
217
+
218
+
219
+ _FIELDS_NS = fieldpath.FIELDS_NS # platform "fields" container id (constant across forms)
220
+
221
+
222
+ def _map_rows(rows, ctx) -> list:
223
+ """Normalize an inputMap/outputMap/mapping list to [{id,left,right}]. A side that
224
+ names a form field (registered in ctx['field_paths']) is resolved to its full
225
+ value-path formId.FIELDS.elementId.valueConfigId; process-variable ids and
226
+ literals pass through."""
227
+ paths = ctx.get("field_paths", {})
228
+
229
+ def resolve(v):
230
+ if isinstance(v, str) and v in paths:
231
+ return paths[v]
232
+ return v if isinstance(v, str) else str(v)
233
+ out = []
234
+ for i, m in enumerate(rows or []):
235
+ left = m.get("to", m.get("left"))
236
+ right = m.get("from", m.get("value", m.get("right", "")))
237
+ out.append({"id": i, "left": resolve(left), "right": resolve(right)})
238
+ return out
239
+
240
+
241
+ def _proc_map(rows, ctx, pvars: dict, kind: str) -> list:
242
+ """RUN_PROCESS input/output map. One side names a PROCESS VARIABLE (resolved to its
243
+ GUID via pvars — the backend requires the id, not the name; live-confirmed 2026-06-29),
244
+ the other names a FORM FIELD (resolved to its value-path) or a literal/process-var id.
245
+ Real exports show BOTH maps as left=process-variable-GUID, right=form-field-path
246
+ (verified across OMS/Diligentic 2026-06-29) -- the orientation is the SAME for input
247
+ and output; only which spec key names the process var differs:
248
+ kind='in': {to: procVar, from: formField} -> left=procVarId(to), right=fieldPath(from)
249
+ kind='out': {to: formField, from: procVar} -> left=procVarId(from), right=fieldPath(to)"""
250
+ paths = ctx.get("field_paths", {})
251
+
252
+ def field(v):
253
+ return paths.get(v, v if isinstance(v, str) else str(v))
254
+
255
+ def pvar(v):
256
+ return pvars.get(v, v if isinstance(v, str) else str(v))
257
+ out = []
258
+ for i, m in enumerate(rows or []):
259
+ a = m.get("to", m.get("left"))
260
+ b = m.get("from", m.get("value", m.get("right", "")))
261
+ if kind == "in": # process input <- form field
262
+ out.append({"id": i, "left": pvar(a), "right": field(b)})
263
+ else: # form field <- process output (process var still on the LEFT)
264
+ out.append({"id": i, "left": pvar(b), "right": field(a)})
265
+ return out
266
+
267
+
268
+ def _ds_map(rows) -> list:
269
+ """A DataStore operation input/output map: a list of DataStoreMapItem
270
+ {id, left, right}. Accepts rows as {left,right} or {to,from} and passes the two
271
+ sides through unchanged (their orientation is DataStore-op specific and confirmed
272
+ live), only assigning the row id."""
273
+ out = []
274
+ for i, m in enumerate(rows or []):
275
+ if not isinstance(m, dict):
276
+ continue
277
+ left = m.get("left", m.get("to"))
278
+ right = m.get("right", m.get("from", m.get("value")))
279
+ out.append({"id": m.get("id", i), "left": left, "right": right})
280
+ return out
281
+
282
+
283
+ def _trigger(ev: dict, ctx) -> dict:
284
+ """The {action, config} for one event trigger.
285
+ do = map | process | js | form | datastore."""
286
+ do = (ev.get("do") or "").strip().lower()
287
+ if do in ("process", "run_process"):
288
+ pid = ev["processId"]
289
+ pvars = (ctx.get("process_vars") or {}).get(pid, {})
290
+ return {"action": "RUN_PROCESS", "config": {
291
+ "processId": pid, "syncRun": bool(ev.get("syncRun", False)),
292
+ "mapLatestTriggerOnly": bool(ev.get("mapLatestTriggerOnly", False)),
293
+ "inputMap": _proc_map(ev.get("inputs"), ctx, pvars, "in"),
294
+ "outputMap": _proc_map(ev.get("outputs"), ctx, pvars, "out"),
295
+ "conditions": [], "areConditionsConfigured": True}}
296
+ if do in ("js", "javascript", "run_javascript"):
297
+ return {"action": "RUN_JAVASCRIPT", "config": {"code": ev.get("code", "")}}
298
+ if do in ("map", "map_form_data"):
299
+ return {"action": "MAP_FORM_DATA", "config": {"mapping": _map_rows(ev.get("mapping"), ctx)}}
300
+ if do in ("form", "trigger_form", "run_form", "open_form"):
301
+ # EventAction.TRIGGER_FORM; config = EventTriggerFormConfig {formId, mapping, ...}
302
+ # (verified vs ui-builder model/config/events/index.ts — NOT "RUN_FORM"/inputMap).
303
+ return {"action": "TRIGGER_FORM", "config": {
304
+ "formId": ev.get("formId"),
305
+ "mapping": _map_rows(ev.get("mapping") or ev.get("inputs"), ctx)}}
306
+ if do in ("datastore", "data_store", "run_data_store_operation", "run_data_store"):
307
+ # EventAction.RUN_DATA_STORE_OPERATION; config = EventRunDataStoreOperationConfig
308
+ # {dataStoreId, operation, inputMap, outputMap, filters?, areFiltersConfigured?}
309
+ # (ui-builder model/config/events/index.ts). operation = DataStoreOperation
310
+ # READ/ADD/UPDATE/DELETE; filters apply to every op except ADD. The map item is
311
+ # {id, left, right} (DataStoreMapItem); the operation wire encoding + map
312
+ # orientation must be re-verified live. See PROCESIO-RESOURCE-MODEL-NOTES.md.
313
+ cfg = {
314
+ "dataStoreId": ev.get("dataStoreId"),
315
+ "operation": ev.get("operation"),
316
+ "inputMap": _ds_map(ev.get("inputs") or ev.get("inputMap")),
317
+ "outputMap": _ds_map(ev.get("outputs") or ev.get("outputMap")),
318
+ }
319
+ filters = ev.get("filters")
320
+ if filters is not None:
321
+ cfg["filters"] = filters
322
+ cfg["areFiltersConfigured"] = bool(filters)
323
+ else:
324
+ cfg["areFiltersConfigured"] = bool(ev.get("areFiltersConfigured", False))
325
+ return {"action": "RUN_DATA_STORE_OPERATION", "config": cfg}
326
+ raise UsageError(
327
+ f"unknown event trigger do={ev.get('do')!r} (use map|process|js|form|datastore)")
328
+
329
+
330
+ def _apply_events(spec: dict, configs: list, ctx) -> None:
331
+ """Attach control events. Each spec event: {on, do, ...}. Grouped by the
332
+ control's event config key as {debounce, events:[{id,type,action,config}]}."""
333
+ grouped: dict[str, list] = {}
334
+ for ev in spec.get("events") or []:
335
+ on = (ev.get("on") or "click").strip().lower()
336
+ key = _EVENT_KEY.get(on)
337
+ if not key:
338
+ raise UsageError(f"unknown event on={ev.get('on')!r}; known: {sorted(_EVENT_KEY)}")
339
+ obj = {"id": _new_id(ctx), "type": _EVENT_TYPE[on]}
340
+ obj.update(_trigger(ev, ctx))
341
+ grouped.setdefault(key, []).append(obj)
342
+ for key, objs in grouped.items():
343
+ _set_config(configs, key, {"debounce": 0, "events": objs}, ctx)
344
+
345
+
346
+ def _resolve_late_field_refs(elements: list, ctx) -> None:
347
+ """Second pass over built elements: resolve event map rows that named a form field
348
+ declared AFTER the element carrying the event.
349
+
350
+ `ctx['field_paths']` is filled as elements are built, so a trigger placed above its
351
+ target fields keeps the raw field NAME in the map instead of the value-path. That is
352
+ the normal layout for any form that shows results (submit button above the results
353
+ panel), and the failure is silent: the API accepts the row, the designer renders it
354
+ blank, and the fields never populate. Runs once every path is known, so ordering in
355
+ the config no longer changes the wiring."""
356
+ paths = ctx.get("field_paths") or {}
357
+ if not paths:
358
+ return
359
+
360
+ def fix(rows) -> None:
361
+ for row in rows or []:
362
+ if not isinstance(row, dict):
363
+ continue
364
+ for side in ("left", "right"):
365
+ v = row.get(side)
366
+ if isinstance(v, str) and v in paths:
367
+ row[side] = paths[v]
368
+
369
+ for el in elements:
370
+ for cfg in el.get("configs") or []:
371
+ val = cfg.get("value")
372
+ if not isinstance(val, dict):
373
+ continue
374
+ for ev in val.get("events") or []:
375
+ ecfg = (ev or {}).get("config") or {}
376
+ for key in ("inputMap", "outputMap", "mapping"):
377
+ fix(ecfg.get(key))
378
+
379
+
380
+ _HEADER = {"heading"}
381
+ _FOOTER = {"button"}
382
+ _CLEAR_DEFAULT = ("placeholder", "tooltip", "info-text") # reset golden leftovers
383
+
384
+
385
+ def _config_value(el: dict, key: str):
386
+ return next((c.get("value") for c in el.get("configs", []) if c.get("key") == key), None)
387
+
388
+
389
+ # Controls whose field-source config is NOT named `value`. A file-viewer holds its file
390
+ # in `src` (the platform's own empty state for it reads "Field source not set"), and
391
+ # _VALUE_TYPE already types file-viewer as File — so without this the control gets a
392
+ # data-model attribute but no field path, and an outputMap row targeting it silently
393
+ # keeps the raw field name instead of resolving to a value path.
394
+ #
395
+ # Shared with the surgical event editor (handlers/form_events.py), which has to CHECK the
396
+ # same paths against a live form: one rule, one place. Two copies would be a rule nothing
397
+ # enforces, and a wrong value path is accepted by the platform without any error at all.
398
+ _VALUE_CONFIG_KEY = fieldpath.VALUE_CONFIG_KEY
399
+ _value_key = fieldpath.value_key
400
+
401
+
402
+ _SKIP_OVERRIDE = {"type", "name", "options", "children", "configs", "events",
403
+ "columns", "cells", "hasHeader", "hasIndexColumn"}
404
+
405
+
406
+ def _build_element(spec: dict, ctx: dict, out: list) -> dict:
407
+ """Build one element (flat) into `out`, recursing children with parentId set.
408
+ PROCESIO uses a FLAT elements list + parentId (NOT nested `children`)."""
409
+ kind = spec.get("type")
410
+ if not kind:
411
+ raise UsageError("each form element needs a 'type'")
412
+ if kind == "table":
413
+ return _build_table(spec, ctx, out)
414
+ el = copy.deepcopy(_golden(kind))
415
+ el["id"] = _new_id(ctx)
416
+ el.pop("children", None)
417
+ el["parentId"] = ctx.get("parent_id")
418
+ section = el.get("section")
419
+ if section not in ("header", "body", "footer"):
420
+ section = "header" if kind in _HEADER else "footer" if kind in _FOOTER else "body"
421
+ el["section"] = section
422
+ configs = el.get("configs", [])
423
+ for c in configs:
424
+ c["id"] = _new_id(ctx)
425
+ if c.get("key") == "visible":
426
+ c["value"] = True
427
+ if c.get("key") in ("required", "readonly"):
428
+ c["value"] = False
429
+ if c.get("key") in _CLEAR_DEFAULT:
430
+ c["value"] = ""
431
+ field = spec.get("name") or _field_slug(spec.get("label") or kind)
432
+ _set_config(configs, "name", field, ctx)
433
+ _set_config(configs, "id", field, ctx)
434
+ val_cfg = next((c for c in configs if c.get("key") == _value_key(kind)), None)
435
+ if val_cfg and ctx.get("form_id"):
436
+ # The field-path value segment MUST be the element's OWN `value` config id.
437
+ # Verified against real forms (OMS, eTransport): the data-model value-attr id
438
+ # equals the element's value config id. A fresh id here resolves to "Unknown"
439
+ # in the process-designer trigger-map "Form variable" selector.
440
+ vaid = val_cfg["id"]
441
+ ctx.setdefault("value_attr_ids", {})[el["id"]] = vaid
442
+ ctx.setdefault("field_paths", {})[field] = fieldpath.build(
443
+ ctx["dm_root_id"], el["id"], vaid)
444
+ for ck, cv in spec.items():
445
+ if ck in _SKIP_OVERRIDE:
446
+ continue
447
+ key = _KEY_ALIASES.get(ck, ck)
448
+ if key in _OVERRIDE_KEYS:
449
+ _set_config(configs, key, cv, ctx)
450
+ for ck, cv in (spec.get("configs") or {}).items():
451
+ _set_config(configs, ck, cv, ctx)
452
+ if "optionsSource" in spec:
453
+ src = spec["optionsSource"] or {}
454
+ st = {"json": "JSON", "static-list": "static-list", "url": "URL"}.get(
455
+ str(src.get("type", "static-list")).lower(), "static-list")
456
+ val = src.get("value")
457
+ _set_config(configs, "sourceType", st, ctx)
458
+ _set_config(configs, "sourceValue",
459
+ _options(val) if st == "static-list" and isinstance(val, list) else val, ctx)
460
+ elif "options" in spec:
461
+ _set_config(configs, "sourceType", "static-list", ctx)
462
+ _set_config(configs, "sourceValue", _options(spec["options"]), ctx)
463
+ _apply_events(spec, configs, ctx)
464
+ _apply_element_style(spec, kind, configs, ctx)
465
+ # array-valued control -> its `value` defaults to an empty array + is typed isList,
466
+ # so the runtime emits/sends an ARRAY (not a single object/null). See _value_is_list.
467
+ if val_cfg is not None and _value_is_list(el):
468
+ val_cfg["value"] = []
469
+ val_cfg["isList"] = True
470
+ out.append(el)
471
+ # children -> FLAT with parentId; columns/tabs keep an ordered child-name list
472
+ child_els = [_build_element(ch, {**ctx, "parent_id": el["id"]}, out)
473
+ for ch in spec.get("children") or []]
474
+ if kind == "columns":
475
+ _set_config(configs, "columns", [_config_value(ce, "name") for ce in child_els], ctx)
476
+ elif kind == "tabs":
477
+ _set_config(configs, "tabs", [_config_value(ce, "name") for ce in child_els], ctx)
478
+ return el
479
+
480
+
481
+ def _build_table(spec: dict, ctx: dict, out: list) -> dict:
482
+ """A table = the table element + a static-table-row + the row's cell controls,
483
+ wired by `childrenIdPerColumn` (colId -> [cellElementId]). All FLAT in `out`."""
484
+ el = copy.deepcopy(_golden("table"))
485
+ el["id"] = _new_id(ctx)
486
+ el.pop("children", None)
487
+ el["parentId"] = ctx.get("parent_id")
488
+ el["section"] = "body"
489
+ configs = el.get("configs", [])
490
+ for c in configs:
491
+ c["id"] = _new_id(ctx)
492
+ if c.get("key") == "visible":
493
+ c["value"] = True
494
+ name = spec.get("name") or _field_slug(spec.get("label") or "table")
495
+ _set_config(configs, "name", name, ctx)
496
+ _set_config(configs, "id", name, ctx)
497
+ if spec.get("label") is not None:
498
+ _set_config(configs, "label", spec["label"], ctx)
499
+ _set_config(configs, "hasHeader", bool(spec.get("hasHeader", True)), ctx)
500
+ _set_config(configs, "hasIndexColumn", bool(spec.get("hasIndexColumn", False)), ctx)
501
+ cols, col_ids = [], {}
502
+ for col in spec.get("columns", []):
503
+ cid = _new_id(ctx)
504
+ col_ids[col["key"]] = cid
505
+ cols.append({"id": cid, "key": col["key"], "label": col.get("label", col["key"])})
506
+ _set_config(configs, "tableColumnsSourceType", "static-list", ctx)
507
+ _set_config(configs, "tableColumnsSourceValue", cols, ctx)
508
+ out.append(el)
509
+ # the row: dynamic-table-row (user can add/remove rows) when spec.dynamic, else a
510
+ # fixed static-table-row. Cells live in childrenIdPerColumn[colId] either way.
511
+ dynamic = bool(spec.get("dynamic"))
512
+ row = copy.deepcopy(_golden("dynamic-table-row" if dynamic else "static-table-row"))
513
+ row["id"] = _new_id(ctx)
514
+ row.pop("children", None)
515
+ row["parentId"] = el["id"]
516
+ row["section"] = "body"
517
+ rcfg = row.get("configs", [])
518
+ for c in rcfg:
519
+ c["id"] = _new_id(ctx)
520
+ rowname = name + "row"
521
+ _set_config(rcfg, "name", rowname, ctx)
522
+ _set_config(rcfg, "id", rowname, ctx)
523
+ out.append(row)
524
+ cipc: dict = {}
525
+ for col in spec.get("columns", []):
526
+ cell = col.get("cell")
527
+ if not cell:
528
+ continue
529
+ ce = _build_element(dict(cell), {**ctx, "parent_id": row["id"]}, out)
530
+ cipc[col_ids[col["key"]]] = [ce["id"]]
531
+ _set_config(rcfg, "childrenIdPerColumn", cipc, ctx)
532
+ if dynamic:
533
+ # the dynamic row carries the add/remove affordances + its own column mirror
534
+ _set_config(rcfg, "canAdd", True, ctx)
535
+ _set_config(rcfg, "canRemove", True, ctx)
536
+ _set_config(rcfg, "addText", spec.get("addText", "Add row"), ctx)
537
+ _set_config(rcfg, "hasHeader", bool(spec.get("hasHeader", True)), ctx)
538
+ _set_config(rcfg, "hasIndexColumn", bool(spec.get("hasIndexColumn", False)), ctx)
539
+ _set_config(rcfg, "tableColumnsSourceType", "static-list", ctx)
540
+ _set_config(rcfg, "tableColumnsSourceValue", cols, ctx)
541
+ _apply_events(spec, rcfg, ctx) # onTableRowAdded/Deleted events, if any
542
+ _set_config(configs, "rows", [rowname], ctx)
543
+ return el
544
+
545
+
546
+ def _build_code(config: dict, ctx: dict) -> str:
547
+ """Form-level CSS + JavaScript (the designer's 'Switch to code' editor) -> the
548
+ encrypted `Data.code` blob. Accepts `css`/`javascript` (or a `code:{css,javascript}`
549
+ object). Empty -> "" (forms render fine without it; verified live). The AES key comes
550
+ from ctx['form_code_key'] (tests) or the key stored for the call's environment
551
+ (form_code_keys: one per installation)."""
552
+ code = config.get("code") if isinstance(config.get("code"), dict) else {}
553
+ css = code.get("css", config.get("css"))
554
+ js = code.get("javascript", config.get("javascript", config.get("js")))
555
+ if not (css or js):
556
+ return ""
557
+ key = ctx.get("form_code_key")
558
+ if not key:
559
+ # The key stored for THIS call's environment, never another installation's:
560
+ # a blob encrypted with the wrong key blanks the form.
561
+ from tools.procesio import form_code_keys
562
+ key = form_code_keys.key_for(ctx.get("form_code_env"))
563
+ from tools.procesio.dto.form import code_cipher
564
+ return code_cipher.encrypt_code(js or "", css or "", key)
565
+
566
+
567
+ def _apply_theme(theme: list, overrides: dict) -> list:
568
+ """Apply {cssVariable: value} overrides onto the shell theme (deep-copied)."""
569
+ if not overrides:
570
+ return theme
571
+ theme = copy.deepcopy(theme)
572
+ norm = {(k if k.startswith("--") else f"--{k}"): v for k, v in overrides.items()}
573
+ for section in theme or []:
574
+ for prop in section.get("properties", []):
575
+ cv = prop.get("cssVariable")
576
+ if cv in norm:
577
+ prop["value"] = norm[cv]
578
+ return theme
579
+
580
+
581
+ # DARK_PALETTE — the 16 dark-mode color variables (ui-builder theme.ts DARK_PALETTE).
582
+ # A form carries both a light and a dark palette; the runtime swaps by themeMode
583
+ # (store state themeMode/activeThemeMode; getPaletteStyleSheet injects the active one).
584
+ # Persisted INSIDE the single `theme` array on the COLORS group as {light, dark}
585
+ # (see _apply_dark). Emitted only when the caller opts in via config `dark`/`themeDark`.
586
+ _DARK_PALETTE = {
587
+ "--c-primary": "#4663f5", "--c-primary-variant": "#8aa0ff",
588
+ "--c-error": "#ff6b6b", "--c-success": "#32d583", "--c-info": "#53b1fd",
589
+ "--c-neutral--900": "#f8fafd", "--c-neutral--800": "#f2f5fa",
590
+ "--c-neutral--700": "#c8cfdb", "--c-neutral--600": "#b0b8c4",
591
+ "--c-neutral--500": "#98a0ae", "--c-neutral--400": "#3b4557",
592
+ "--c-neutral--300": "#2a3241", "--c-neutral--200": "#222936",
593
+ "--c-neutral--100": "#1a202b", "--c-neutral--50": "#0b0e14",
594
+ "--c-neutral--0": "#141922",
595
+ }
596
+
597
+
598
+ def _apply_dark(theme_list: list, overrides: dict) -> list:
599
+ """Enable dark mode on a built theme: turn the COLORS group's `properties` into a
600
+ {light, dark} ModeProperties object (the shape ui-builder's getPaletteStyleSheet reads).
601
+ light = the theme-applied color props; dark = the same props with values from the 16-var
602
+ DARK_PALETTE plus any caller `themeDark` overrides, keyed by cssVariable. Every other
603
+ group keeps a flat CSSProperty[]. themeMode is editor-only and never persisted."""
604
+ theme_list = copy.deepcopy(theme_list)
605
+ dark_vals = {**_DARK_PALETTE, **(overrides or {})}
606
+ dark_vals = {(k if k.startswith("--") else f"--{k}"): v for k, v in dark_vals.items()}
607
+ for group in theme_list or []:
608
+ if str(group.get("label", "")).strip().lower() == "colors":
609
+ light = group.get("properties")
610
+ if isinstance(light, list):
611
+ dark = [{**pr, "value": dark_vals.get(pr.get("cssVariable"), pr.get("value"))}
612
+ for pr in light]
613
+ group["properties"] = {"light": light, "dark": dark}
614
+ break
615
+ return theme_list
616
+
617
+
618
+ _STR = "0317bfee-b2f5-4bde-bfe8-121212121214"
619
+ _BOOL = "0317bfee-b2f5-4bde-bfe8-121212121210"
620
+ _INT = "0317bfee-b2f5-4bde-bfe8-121212121211"
621
+ _DT = "0317bfee-b2f5-4bde-bfe8-121212121218"
622
+ _FILE = "10c6ac59-3929-49e6-99dc-121212121219"
623
+ _JSON = "0317bfee-b2f5-4bde-bfe8-121212121220"
624
+ # PROCESIO's generic Object type. A select's STATIC option list is a list of these in
625
+ # the data model (designer capture, 2026-10-01).
626
+ _OBJECT = "0317bfee-b2f5-4bde-bfe8-121212121221"
627
+ _BOOL_CFG = {"visible", "readonly", "required", "submit", "multiple", "searchable",
628
+ "clearable", "hasNow", "disabledIfFormIsInvalid", "canAdd", "canRemove",
629
+ "outlined", "disabled"}
630
+ _VALUE_TYPE = {"number-input": _INT, "checkbox": _BOOL, "datetime-input": _DT,
631
+ "file-upload": _FILE, "file-viewer": _FILE, "chat": _JSON}
632
+ # Controls whose `value` is a JSON ARRAY -> DM attr isList:true + value config default [].
633
+ # Inherently array-valued: chat + the array elements (list/table/dynamic-table-row).
634
+ # Array-valued ONLY when multiple:true: file-upload/select. Confirmed by Liza (PROCESIO):
635
+ # a multiple file-upload MUST be isList + default [] or the form sends a single object and
636
+ # a List<File> process input fails at launch ("... item is not an array: StartObject").
637
+ _VALUE_LIST = {"chat", "list", "table", "dynamic-table-row"}
638
+ _MULTIPLE_ARRAY_TYPES = {"file-upload", "select"}
639
+
640
+ def _value_is_list(el: dict) -> bool:
641
+ t = el.get("type")
642
+ if t in _VALUE_LIST:
643
+ return True
644
+ if t in _MULTIPLE_ARRAY_TYPES:
645
+ return bool(next((c.get("value") for c in el.get("configs", [])
646
+ if c.get("key") == "multiple"), None))
647
+ return False
648
+ # Configs that get NO data-model attribute. `sourceType` / `sourceValue` used to be here,
649
+ # grouped with the layout configs, but the designer writes both into the data model, and
650
+ # only data-model attributes reach `ProcesioForm` in form JS: with them skipped, a script
651
+ # on an API-built form could never read a select's options (measured live 2026-10-01;
652
+ # FORM-DEV-GUIDE/08, "A script reads a select's options and gets nothing").
653
+ _SKIP_DM_ATTR = {"options", "childrenIdPerColumn",
654
+ "tableColumnsSourceValue", "tableColumnsSourceType", "rows", "columns",
655
+ "tabs", "style"}
656
+
657
+
658
+ def _attr_name(key: str) -> str:
659
+ """Data-model attribute name = camelCase of the config key (PROCESIO compiles
660
+ `info-text` -> `infoText`). Single-word keys pass through unchanged."""
661
+ parts = str(key).split("-")
662
+ return parts[0] + "".join(p[:1].upper() + p[1:] for p in parts[1:] if p)
663
+
664
+
665
+ def _dm_attr(name, aid, type_id, parent, is_list=False):
666
+ return {"id": aid, "name": name, "displayName": name, "dataTypeId": type_id,
667
+ "isDataModel": False, "isList": is_list, "isProcesio": False,
668
+ "isPublic": False, "parentDataTypeId": parent, "jsonProperty": None,
669
+ "attributes": [], "hidden": False}
670
+
671
+
672
+ def _dm_shape(el: dict, c: dict) -> tuple[str, bool] | None:
673
+ """(dataTypeId, isList) of the data-model attribute one element config gets, or None
674
+ when it gets none. Shared by form-create and form-add-element, so the two can never
675
+ disagree about what the designer would write.
676
+
677
+ `sourceValue` follows a designer capture: a STATIC option list is a list of Objects.
678
+ A JSON or URL source has not been captured from the designer, so its `sourceValue`
679
+ stays out rather than be given a guessed type. `sourceType` is a plain string."""
680
+ k = c.get("key")
681
+ if not k or k.endswith("Events") or k in _SKIP_DM_ATTR:
682
+ return None
683
+ if k == "sourceValue":
684
+ return (_OBJECT, True) if isinstance(c.get("value"), list) else None
685
+ vkey = _value_key(el.get("type"))
686
+ if k == vkey:
687
+ return _VALUE_TYPE.get(el.get("type"), _STR), _value_is_list(el)
688
+ return (_BOOL if k in _BOOL_CFG else _STR), False
689
+
690
+
691
+ def _build_data_model(form_id: str, elements: list, ctx: dict) -> dict:
692
+ """Build the form's `Data.dataModel` mirror. PROCESIO backs every form control
693
+ with a data-model attribute: root "form" (id=form_id) -> `fields` container
694
+ (id=_FIELDS_NS) -> one sub-model per element (id=elementId) -> an attribute per
695
+ config (incl. `value`). RUN_PROCESS/MAP triggers reference fields by the path
696
+ formId.fields.elementId.valueAttrId, and the runtime stores the field value in
697
+ that attribute. Without this, the designer shows raw guids and values don't flow."""
698
+ dm = copy.deepcopy(_shell().get("dataModel") or {})
699
+ dm["id"] = form_id
700
+ dm["name"] = dm.get("name") or "form"
701
+ attrs = dm.get("attributes") or []
702
+ fields = next((a for a in attrs if a.get("id") == _FIELDS_NS), None)
703
+ if fields is None:
704
+ fields = {"id": _FIELDS_NS, "dataTypeId": _FIELDS_NS, "name": "fields",
705
+ "displayName": "fields", "isDataModel": True, "isList": False,
706
+ "isProcesio": False, "isPublic": True, "jsonProperty": None,
707
+ "attributes": []}
708
+ attrs.append(fields)
709
+ dm["attributes"] = attrs
710
+ fields["parentDataTypeId"] = form_id
711
+ val_ids = ctx.get("value_attr_ids", {})
712
+ submodels = []
713
+ for el in elements:
714
+ eid = el["id"]
715
+ nm = next((c.get("value") for c in el.get("configs", []) if c.get("key") == "name"), None) or eid
716
+ sub = {"id": eid, "dataTypeId": eid, "name": nm, "displayName": nm,
717
+ "isDataModel": True, "isList": False, "isProcesio": False,
718
+ "isPublic": False, "parentDataTypeId": _FIELDS_NS, "jsonProperty": None,
719
+ "attributes": []}
720
+ for c in el.get("configs", []):
721
+ shape = _dm_shape(el, c)
722
+ if shape is None:
723
+ continue
724
+ # The data-model attribute id MUST equal the element's config id for that
725
+ # key (verified against real forms: every fields sub-model attr id == the
726
+ # element's config id; 15/16 in OMS vs 0/16 with fresh ids -> "Unknown" in
727
+ # the trigger-map UI). Reuse the config's own id.
728
+ aid = c.get("id") or _new_id(ctx)
729
+ sub["attributes"].append(
730
+ _dm_attr(_attr_name(c["key"]), aid, shape[0], eid, shape[1]))
731
+ submodels.append(sub)
732
+ fields["attributes"] = submodels
733
+ # Every top-level attribute (instance, flow, fields, preventFormSubmit, browserTitle — the
734
+ # set the designer's generateFormDataModel writes) hangs off the root.
735
+ for a in attrs:
736
+ a["parentDataTypeId"] = form_id
737
+ return dm
738
+
739
+
740
+ # The designer writes "1" for a form it creates (user-playground Builder.component.vue: a new
741
+ # form gets FormDataModelAttributeFormatVersion.VERSION_1 = "same attribute as name"). A form
742
+ # with NO version is read as the legacy "0" format, and the designer's next save lower-cases
743
+ # every field attribute (measured on QA 2026-09-25: `RequestText` -> `requesttext`), which
744
+ # breaks any form script that addresses the field by its real name. This builder names field
745
+ # attributes exactly as the element name, i.e. format "1", so it must say so.
746
+ _DM_FORMAT_VERSION = "1"
747
+
748
+ # Who works a step and who approves it, read the way the designer does before it saves
749
+ # (ui-builder form-builder/assignee::getElementAssignees and approval::getElementApprovers).
750
+ _ASSIGNEE_KEYS = ("assignee", "assigneeReplacement", "additionalAssignment")
751
+ _APPROVER_KEYS = ("approver", "approverReplacement", "additionalApprovers")
752
+ _ASSIGNEE, _APPROVER = 1, 2
753
+
754
+
755
+ def _unique_ids(values) -> list:
756
+ users: list = []
757
+ for uid in values:
758
+ if isinstance(uid, str) and uid.strip() and uid not in users:
759
+ users.append(uid)
760
+ return users
761
+
762
+
763
+ def _step_users(el: dict, keys: tuple) -> list:
764
+ values: list = []
765
+ for key in keys:
766
+ value = _config_value(el, key)
767
+ values.extend(value if isinstance(value, list) else [value])
768
+ return _unique_ids(values)
769
+
770
+
771
+ def _hidden_by_config(el: dict) -> bool:
772
+ # ui-builder elementAvailability::isElementHiddenByConfig: hidden only when `visible` is
773
+ # present and falsy.
774
+ visible = next((c.get("value") for c in el.get("configs", []) if c.get("key") == "visible"),
775
+ None)
776
+ return any(c.get("key") == "visible" for c in el.get("configs", [])) and not visible
777
+
778
+
779
+ def _approver_steps(el: dict, by_id: dict) -> list:
780
+ """[(stepId, users)] for one element, as ui-builder approval::getElementApproversByStepId.
781
+
782
+ A dynamic table row does not approve as itself: each entry of its `approvalSteps` with a
783
+ `rowIndex` is its own step (id = the step id, users = approver + approverReplacement), and
784
+ only while the row sits in a table and is not hidden (model/config/table/approvals.ts::
785
+ getDynamicRowApprovalSteps). Every other element is one step under its own id.
786
+ """
787
+ if el.get("type") != "dynamic-table-row":
788
+ return [(el["id"], _step_users(el, _APPROVER_KEYS))]
789
+ parent = by_id.get(el.get("parentId"))
790
+ if not parent or parent.get("type") != "table" or _hidden_by_config(el):
791
+ return []
792
+ steps = _config_value(el, "approvalSteps") or []
793
+ out = []
794
+ for step in steps if isinstance(steps, list) else []:
795
+ if not isinstance(step, dict) or step.get("rowIndex") is None or not step.get("id"):
796
+ continue
797
+ replacement = step.get("approverReplacement") or []
798
+ out.append((step["id"], _unique_ids([step.get("approver"), *replacement])))
799
+ return out
800
+
801
+
802
+ def derive_assignees(elements: list) -> list:
803
+ """The form's top-level `Assignees`, built like the designer's save.
804
+
805
+ ASSIGNEE (1) steps first, then APPROVER (2) steps, `orderId` counting 1, 2, 3 within each
806
+ list, a step only when it has users, each step id once, users sent as `{userId}` only (the
807
+ platform fills in the names). Mirrors ui-builder utils/formBuilder/payload.ts::
808
+ getFormBuilderUsersFullData and user-playground Builder.component.vue::save. Checked against
809
+ 19 designer-saved forms on QA (2026-10-05), dynamic-row approvals included: identical.
810
+ The designer also drops users who are not in the workspace; that needs a live lookup and is
811
+ not done here.
812
+ """
813
+ by_id = {e.get("id"): e for e in elements}
814
+ out: list = []
815
+ for kind in (_ASSIGNEE, _APPROVER):
816
+ order, seen = 0, set()
817
+ for el in elements:
818
+ steps = ([(el["id"], _step_users(el, _ASSIGNEE_KEYS))] if kind == _ASSIGNEE
819
+ else _approver_steps(el, by_id))
820
+ for step_id, users in steps:
821
+ if not users or step_id in seen:
822
+ continue
823
+ seen.add(step_id)
824
+ order += 1
825
+ out.append({"stepId": step_id, "orderId": order, "type": kind,
826
+ "users": [{"userId": u} for u in users]})
827
+ return out
828
+
829
+
830
+ def _assignee_key(rows) -> list:
831
+ """Assignees compared by what routes a step (names and key casing ignored)."""
832
+ out = []
833
+ for r in rows or []:
834
+ g = {str(k).lower(): v for k, v in r.items()}
835
+ users = [str({str(k).lower(): v for k, v in u.items()}.get("userid")) for u in g.get("users") or []]
836
+ out.append((str(g.get("stepid")), int(g.get("orderid") or 0), int(g.get("type") or 0), tuple(users)))
837
+ return out
838
+
839
+
840
+ def reconcile_assignees(current: list, before: list, after: list) -> tuple[list, dict | None]:
841
+ """(the Assignees to save, a report or None) for a surgical change from `before` to `after`
842
+ elements.
843
+
844
+ The designer re-derives the list on every save, so a surgical edit that echoes the old list
845
+ leaves a removed approver still routed, or a new approval control unrouted. Only a change
846
+ that alters the derived routing re-derives it, and then the WHOLE list is saved as the
847
+ designer would save it; any other edit keeps `current` byte-for-byte, so it never rewrites
848
+ routing it did not touch."""
849
+ derived = derive_assignees(after)
850
+ if _assignee_key(derive_assignees(before)) == _assignee_key(derived):
851
+ return current, None
852
+ return derived, {"rederived": True, "before": _assignee_key(current),
853
+ "after": _assignee_key(derived)}
854
+
855
+
856
+ def build(config: dict, ctx: dict) -> dict:
857
+ ctx = dict(ctx)
858
+ ctx["form_id"] = ctx.get("form_id") or _new_id(ctx)
859
+ shell = _shell()
860
+ # The designer's field-name resolver walks from the "form" handle variable
861
+ # (Data.variables[0]) into Data.dataModel. So the dataModel ROOT id, the
862
+ # field-path first segment, AND variables[0].id MUST be the SAME id, else a stored
863
+ # field path can't be turned back into its name -> input shows the raw guid, output
864
+ # shows blank (live-confirmed: variables[0]=65396456 but a fresh root id => unresolved).
865
+ # The shell ships the FORM-scope root (scopes:["FORM"]) as that shared id; derive the
866
+ # root from it so the three always agree. It is still != the form template id (what the
867
+ # earlier "root != form id" fix required), so both invariants hold.
868
+ _form_var = (shell.get("variables") or [{}])[0]
869
+ ctx["dm_root_id"] = (ctx.get("dm_root_id") or _form_var.get("dataType")
870
+ or _form_var.get("id") or _new_id(ctx))
871
+ ctx["field_paths"] = {}
872
+ ctx["value_attr_ids"] = {}
873
+ elements: list = []
874
+ for e in config.get("elements", []):
875
+ _build_element(e, ctx, elements)
876
+ _resolve_late_field_refs(elements, ctx)
877
+ data = {
878
+ "code": _build_code(config, ctx),
879
+ "theme": _apply_theme(shell.get("theme"), config.get("theme")),
880
+ "events": shell.get("events", []),
881
+ "images": shell.get("images", {"logo": None, "header": None}),
882
+ "elements": elements,
883
+ "messages": shell.get("messages"),
884
+ "dataModel": _build_data_model(ctx["dm_root_id"], elements, ctx),
885
+ "variables": shell.get("variables"),
886
+ "browserTitle": config.get("browserTitle", config["name"]),
887
+ "hideBranding": bool(config.get("hideBranding", False)),
888
+ "dataModelAttributeFormatVersion": _DM_FORMAT_VERSION,
889
+ }
890
+ if config.get("chainConfig") is not None:
891
+ data["chainConfig"] = config["chainConfig"]
892
+ # Dark theme (opt-in): the dark palette is persisted INSIDE the single `theme` array —
893
+ # the COLORS group's `properties` becomes a {light, dark} ModeProperties object (verified
894
+ # vs ui-builder main: getPaletteStyleSheet reads properties.light/.dark). themeMode is
895
+ # editor-only and never saved. Emitted only when the caller opts in via `dark`/`themeDark`,
896
+ # so light-only forms are byte-identical.
897
+ if config.get("dark") or config.get("themeDark") is not None or config.get("themeMode") == "dark":
898
+ data["theme"] = _apply_dark(data["theme"], config.get("themeDark"))
899
+ # Guard the field-name-resolution invariant (see dm_root_id above): the dataModel
900
+ # root and every field-path root must equal the form handle variable id, or the
901
+ # designer renders process<->form maps as raw guids/blanks. Fail loudly here so a
902
+ # future change can't silently reintroduce the bug.
903
+ _fv = (data.get("variables") or [{}])[0].get("id")
904
+ if data["dataModel"].get("id") != _fv:
905
+ raise UsageError(f"form data-model root {data['dataModel'].get('id')!r} must "
906
+ f"equal the form handle variable id {_fv!r}")
907
+ _bad = [p for p in ctx["field_paths"].values() if not str(p).startswith(f"{_fv}.")]
908
+ if _bad:
909
+ raise UsageError(f"form field paths must start with the form root {_fv!r}: {_bad[:3]}")
910
+ # An explicit `assignees` list still wins verbatim; otherwise derive it the way the
911
+ # designer's save does, so an approver actually receives the approval step.
912
+ assignees = config["assignees"] if "assignees" in config else derive_assignees(elements)
913
+ # The designer saves a form that involves users as a USER_TASK. Only fill the gap when
914
+ # the caller did not choose a type.
915
+ if "type" in config:
916
+ ftype = config["type"]
917
+ else:
918
+ involves_users = bool(assignees) or any(e.get("type") == "assignee" for e in elements)
919
+ ftype = "usertask" if involves_users else "general"
920
+ return {
921
+ "Id": ctx["form_id"],
922
+ "Name": config["name"],
923
+ "IsPrivate": bool(config.get("isPrivate", False)),
924
+ "Type": _FORM_TYPE.get(str(ftype).strip().lower(), ftype),
925
+ "Status": 1 if config.get("publish", True) else 0,
926
+ "State": bool(config.get("enabled", True)),
927
+ "Assignees": assignees,
928
+ "Data": data,
929
+ "CustomUrl": None,
930
+ }
931
+
932
+
933
+ def _collect_process_ids(elements: list, out: set) -> None:
934
+ for e in elements or []:
935
+ for ev in e.get("events") or []:
936
+ if (ev.get("do") or "").strip().lower() in ("process", "run_process") and ev.get("processId"):
937
+ out.add(ev["processId"])
938
+ _collect_process_ids(e.get("children"), out)
939
+ for col in e.get("columns") or []:
940
+ if col.get("cell"):
941
+ _collect_process_ids([col["cell"]], out)
942
+
943
+
944
+ def _prepare_ctx(client, config: dict) -> dict:
945
+ """Impure pre-build step (has the live client): resolve every process referenced by a
946
+ RUN_PROCESS event to its variable name->id map, so the builder can wire inputMap/
947
+ outputMap by GUID (the backend rejects variable names). Fail loud on a bad processId.
948
+ Also records the call's environment, which decides the form-code-key."""
949
+ env = getattr(client, "env", None)
950
+ ctx: dict = {"form_code_env": env.get("name") if isinstance(env, dict) else None}
951
+ pids: set = set()
952
+ _collect_process_ids(config.get("elements"), pids)
953
+ if not pids:
954
+ return ctx
955
+ pvars: dict = {}
956
+ for pid in pids:
957
+ try:
958
+ body = client.get(f"/api/Projects/{pid}", VARIABLES_ONLY)
959
+ except Exception as e: # noqa: BLE001
960
+ raise UsageError(f"RUN_PROCESS references process {pid!r} which could not be "
961
+ f"fetched ({e}); check the processId.")
962
+ if isinstance(body, dict) and "flow" not in body and isinstance(body.get("result"), dict):
963
+ body = body["result"]
964
+ flow = body.get("flow") if isinstance(body, dict) else None
965
+ src = flow if isinstance(flow, dict) else (body if isinstance(body, dict) else {})
966
+ variables = src.get("variables") or []
967
+ pvars[pid] = {v.get("name"): v.get("id") for v in variables if v.get("name")}
968
+ ctx["process_vars"] = pvars
969
+ return ctx
970
+
971
+
972
+ def _extract_id(resp, dto):
973
+ if isinstance(resp, dict) and (resp.get("id") or resp.get("Id")):
974
+ return resp.get("id") or resp.get("Id")
975
+ if isinstance(resp, str) and resp.strip():
976
+ return resp.strip().strip('"')
977
+ return dto.get("Id") if isinstance(dto, dict) else None
978
+
979
+
980
+ def _edit(client, resource_id, config, ctx):
981
+ ctx = dict(ctx)
982
+ ctx["form_id"] = resource_id
983
+ dto = build(config, ctx)
984
+ client.put("/api/FormTemplate", dto)
985
+ return client.get(f"/api/FormTemplate/{resource_id}")
986
+
987
+
988
+ def _lint(resource) -> list[str]:
989
+ """Non-blocking formlint warnings for a built DTO (`Data`) or a fetched form (`data`)."""
990
+ from tools.procesio import formlint
991
+ if not isinstance(resource, dict):
992
+ return []
993
+ return formlint.lint_form_data(resource.get("Data") or resource.get("data"))
994
+
995
+
996
+ COMPONENT = Component(
997
+ name="form",
998
+ description="PROCESIO form template: a control tree (elements) rendered at runtime.",
999
+ dir=DIR,
1000
+ build=build,
1001
+ create_endpoint=("POST", "/api/FormTemplate"),
1002
+ get_path="/api/FormTemplate/{id}",
1003
+ extract_id=_extract_id,
1004
+ prepare_ctx=_prepare_ctx,
1005
+ edit=_edit,
1006
+ patch_action="form-update",
1007
+ add_action="form-add-element",
1008
+ lint=_lint,
1009
+ )