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,1311 @@
1
+ """Pure, deterministic layered graph layout (Sugiyama) tuned for PROCESIO readability.
2
+
3
+ Pipeline per coordinate space:
4
+ 1. longest-path layering (x = layer * DX) — left-to-right, sources at the left.
5
+ 2. dummy nodes on every edge spanning >1 layer, so long edges route as straight
6
+ horizontal lanes through dummies instead of cutting across the canvas.
7
+ 3. crossing-minimization: weighted-median ordering sweeps + a transpose (adjacent
8
+ swap) step, keeping the lowest-crossing order seen.
9
+ 4. y-coordinate straightening: median relaxation pulls each node onto its neighbours'
10
+ median y (dummies included), so a chain lands on ONE row and parallel chains become
11
+ parallel straight lanes; per-layer min-gap keeps lanes apart.
12
+ 5. error-port targets drop to a lane below the happy path.
13
+ 6. compaction via a tight DX/DY; For-Each container bodies laid out recursively.
14
+
15
+ No PROCESIO imports, no I/O. Same input -> same output (all sorts are id-stable, all
16
+ iteration counts fixed).
17
+ """
18
+ from __future__ import annotations
19
+
20
+ from collections import defaultdict
21
+
22
+ DEFAULTS = {
23
+ "DX": 160, "DY": 180, "NODE_W": 48, "NODE_H": 48,
24
+ "AREA_PAD": 40, "MARGIN": 80, "ERR_GAP": 200,
25
+ "ORDER_ITERS": 8, "RELAX_ITERS": 16, "DUMMY_H": 1, "FOLD_MAX": 5,
26
+ "DY_AFTER_DECISIONAL": 240,
27
+ "FE_START_PAD": 32, "FE_TOP": 78, "FE_RIGHT_PAD": 100, "FE_BOTTOM_PAD": 125,
28
+ "CLUSTER": False, "CLUSTER_MIN": 5, "CLUSTER_WRAP": 4,
29
+ "MINIMIZE_CROSSINGS": True, "MINCROSS_ITERS": 6, "MINCROSS_RANGE": 6,
30
+ "MINCROSS_MOVE_PENALTY": 0.25, "MINCROSS_LEN_PENALTY": 0.0,
31
+ }
32
+
33
+
34
+ def _node(n: dict) -> dict:
35
+ pos = n.get("position") or {"x": n.get("x", 0.0), "y": n.get("y", 0.0)}
36
+ return {
37
+ "id": n["id"],
38
+ "w": n.get("width") or n.get("size", {}).get("w") or DEFAULTS["NODE_W"],
39
+ "h": n.get("height") or n.get("size", {}).get("h") or DEFAULTS["NODE_H"],
40
+ "parent_id": n.get("parent_id"),
41
+ "kind": n.get("kind") or n.get("shape"),
42
+ "pos": {"x": float(pos.get("x", 0.0)), "y": float(pos.get("y", 0.0))},
43
+ }
44
+
45
+
46
+ def _median(xs: list[float]) -> float:
47
+ s = sorted(xs)
48
+ n = len(s)
49
+ if not n:
50
+ return 0.0
51
+ if n % 2:
52
+ return float(s[n // 2])
53
+ return (s[n // 2 - 1] + s[n // 2]) / 2.0
54
+
55
+
56
+ def _longest_path_layers(ids: list[str], edges: list[tuple[str, str]]) -> dict[str, int]:
57
+ """layer(v) = longest path from any source to v. Cycle-safe (bounded relaxation;
58
+ a back-edge simply stops extending). A second ALAP pass then pulls single-successor
59
+ chains RIGHT, adjacent to their join: a short branch that merges into a long one used
60
+ to keep its early (ASAP) layers, stretching its join edge across the canvas (Networky
61
+ v2: Map Route A -> Join Route spanned 8 layers / 1450px). Pulling u to
62
+ min(layer(succ))-1 when ALL of u's out-edges point right keeps the graph valid and
63
+ makes branch tails land next to their merge point."""
64
+ layer = {i: 0 for i in ids}
65
+ for _ in range(len(ids) + 1):
66
+ changed = False
67
+ for s, d in edges:
68
+ if layer[s] + 1 > layer[d]:
69
+ layer[d] = layer[s] + 1
70
+ changed = True
71
+ if not changed:
72
+ break
73
+ # ALAP pull (bounded sweeps; skips back-edges which would point left). Branch HEADS stay
74
+ # anchored at their split (a node whose predecessor fans out keeps its ASAP layer), so a
75
+ # short branch hugs its join at the TAIL while its head remains next to the decisional -
76
+ # the slack becomes one long parallel bypass edge instead of a head edge crossing lanes.
77
+ out = defaultdict(list)
78
+ inc = defaultdict(list)
79
+ for s, d in edges:
80
+ out[s].append(d)
81
+ inc[d].append(s)
82
+ branch_heads = {u for u in ids if any(len(out.get(p, [])) > 1 for p in inc.get(u, []))}
83
+ for _ in range(len(ids) + 1):
84
+ changed = False
85
+ for u in ids:
86
+ if u in branch_heads:
87
+ continue
88
+ succs = [d for d in out.get(u, []) if layer[d] > layer[u]]
89
+ if not succs or len(succs) != len(out.get(u, [])):
90
+ continue # sink, or has a back/flat edge - leave anchored
91
+ want = min(layer[d] for d in succs) - 1
92
+ if want > layer[u]:
93
+ layer[u] = want
94
+ changed = True
95
+ if not changed:
96
+ break
97
+ return layer
98
+
99
+
100
+ def _back_edges(ids, edges) -> set:
101
+ """Back-edges of the graph via iterative DFS (an edge to a node still on the DFS
102
+ stack = a cycle's return edge). Deterministic in order."""
103
+ idset = set(ids)
104
+ succ = defaultdict(list)
105
+ for s, d in edges:
106
+ if s in idset and d in idset:
107
+ succ[s].append(d)
108
+ color = {i: 0 for i in ids} # 0 white, 1 on-stack, 2 done
109
+ back = set()
110
+ for root in ids:
111
+ if color[root] != 0:
112
+ continue
113
+ color[root] = 1
114
+ stack = [(root, iter(succ.get(root, [])))]
115
+ while stack:
116
+ u, it = stack[-1]
117
+ nxt = None
118
+ for v in it:
119
+ if color[v] == 0:
120
+ nxt = v
121
+ break
122
+ if color[v] == 1:
123
+ back.add((u, v))
124
+ if nxt is None:
125
+ color[u] = 2
126
+ stack.pop()
127
+ else:
128
+ color[nxt] = 1
129
+ stack.append((nxt, iter(succ.get(nxt, []))))
130
+ return back
131
+
132
+
133
+ def _layer_inversions(orderL: list, orderL1: list, down: dict) -> int:
134
+ idx1 = {n: i for i, n in enumerate(orderL1)}
135
+ seq = [idx1[m] for n in orderL for m in down.get(n, []) if m in idx1]
136
+ inv = 0
137
+ for i in range(len(seq)):
138
+ si = seq[i]
139
+ for j in range(i + 1, len(seq)):
140
+ if si > seq[j]:
141
+ inv += 1
142
+ return inv
143
+
144
+
145
+ def _total_crossings(order: dict, down: dict, maxL: int) -> int:
146
+ return sum(_layer_inversions(order.get(L, []), order.get(L + 1, []), down)
147
+ for L in range(maxL))
148
+
149
+
150
+ def _wmedian(order: dict, L: int, adj_layer: int, neigh: dict) -> None:
151
+ idx_adj = {n: i for i, n in enumerate(order.get(adj_layer, []))}
152
+ cur = {n: i for i, n in enumerate(order[L])}
153
+
154
+ def key(n):
155
+ ps = sorted(idx_adj[x] for x in neigh.get(n, []) if x in idx_adj)
156
+ return _median(ps) if ps else float(cur[n])
157
+ order[L] = sorted(order[L], key=lambda n: (key(n), str(n)))
158
+
159
+
160
+ def _transpose(order: dict, down: dict, maxL: int) -> None:
161
+ def local(L):
162
+ c = _layer_inversions(order.get(L, []), order.get(L + 1, []), down)
163
+ if L > 0:
164
+ c += _layer_inversions(order.get(L - 1, []), order.get(L, []), down)
165
+ return c
166
+ improved = True
167
+ guard = 0
168
+ while improved and guard < 8:
169
+ improved = False
170
+ guard += 1
171
+ for L in range(maxL + 1):
172
+ ol = order[L]
173
+ for i in range(len(ol) - 1):
174
+ before = local(L)
175
+ ol[i], ol[i + 1] = ol[i + 1], ol[i]
176
+ if local(L) < before:
177
+ improved = True
178
+ else:
179
+ ol[i], ol[i + 1] = ol[i + 1], ol[i]
180
+
181
+
182
+ def _place(order_L: list, desired: dict, ue: dict, de: dict, gap: float) -> dict:
183
+ """Assign y to a layer's nodes as close as possible to `desired` while keeping the
184
+ given order with a minimum gap. Solves it as isotonic regression (pool-adjacent-
185
+ violators) on the gap-shifted target, so a chain stays straight and there is no
186
+ systematic drift. `ue`/`de` are per-node up/down half-extents (asymmetric: a folded
187
+ decisional reserves its fold's height on the fold side)."""
188
+ n = len(order_L)
189
+ if not n:
190
+ return {}
191
+ sep = [0.0] * n
192
+ for i in range(1, n):
193
+ sep[i] = de[order_L[i - 1]] + gap + ue[order_L[i]]
194
+ cum = [0.0] * n
195
+ for i in range(1, n):
196
+ cum[i] = cum[i - 1] + sep[i]
197
+ # fit a non-decreasing sequence to t[i] = desired - cum[i]; PAVA with mean pools
198
+ pools = [] # [sum, count, mean]
199
+ for i in range(n):
200
+ ti = desired[order_L[i]] - cum[i]
201
+ pools.append([ti, 1, ti])
202
+ while len(pools) >= 2 and pools[-2][2] > pools[-1][2]:
203
+ s2, c2, _ = pools.pop()
204
+ s1, c1, _ = pools.pop()
205
+ s, c = s1 + s2, c1 + c2
206
+ pools.append([s, c, s / c])
207
+ z = []
208
+ for s, c, _m in pools:
209
+ z += [s / c] * c
210
+ return {order_L[i]: z[i] + cum[i] for i in range(n)}
211
+
212
+
213
+ def _layout_layer(nodes: list[dict], typed_edges: list[tuple[str, str, int]], o: dict,
214
+ extents: dict | None = None) -> dict:
215
+ ids = [n["id"] for n in nodes]
216
+ if not ids:
217
+ return {"positions": {}, "bbox": {"minX": 0, "minY": 0, "maxX": 0, "maxY": 0}}
218
+ sizes = {n["id"]: (n["w"], n["h"]) for n in nodes}
219
+ idset = set(ids)
220
+ norm = [(s, d) for (s, d, t) in typed_edges if s in idset and d in idset and s != d and t == 0]
221
+ errs = [(s, d) for (s, d, t) in typed_edges if s in idset and d in idset and s != d and t == 1]
222
+ err_targets = {d for _s, d in errs}
223
+ all_edges = list(dict.fromkeys(norm + errs))
224
+
225
+ bk = _back_edges(ids, all_edges)
226
+ layer = _longest_path_layers(ids, [e for e in all_edges if e not in bk])
227
+
228
+ # working node geometry (reals + dummies) keyed by id
229
+ vlayer = {i: layer[i] for i in ids}
230
+ vh = {i: sizes[i][1] for i in ids}
231
+ # dummy chains for normal edges that span more than one layer
232
+ segs: list[tuple[str, str]] = []
233
+ dn = [0]
234
+
235
+ def new_dummy(L):
236
+ did = f"__d{dn[0]}"
237
+ dn[0] += 1
238
+ vlayer[did] = L
239
+ vh[did] = o["DUMMY_H"]
240
+ return did
241
+ for s, d in norm:
242
+ ls, ld = layer[s], layer[d]
243
+ if ld - ls == 1:
244
+ segs.append((s, d))
245
+ elif ld - ls > 1:
246
+ prev = s
247
+ for L in range(ls + 1, ld):
248
+ nd = new_dummy(L)
249
+ segs.append((prev, nd))
250
+ prev = nd
251
+ segs.append((prev, d))
252
+ # edges that don't go strictly one+ layers right (cycles/flat) are dropped from
253
+ # routing; they remain as ports the designer draws, just not laid-out lanes.
254
+
255
+ down: dict = defaultdict(list)
256
+ up: dict = defaultdict(list)
257
+ for a, b in segs: # a in layer L, b in layer L+1 by construction
258
+ down[a].append(b)
259
+ up[b].append(a)
260
+
261
+ layers: dict = defaultdict(list)
262
+ for i in vlayer:
263
+ layers[vlayer[i]].append(i)
264
+ maxL = max(layers) if layers else 0
265
+ order = {L: sorted(layers[L], key=str) for L in range(maxL + 1)}
266
+
267
+ # crossing-minimization: median sweeps + transpose, keep the best order
268
+ best = {L: list(order[L]) for L in order}
269
+ best_c = _total_crossings(order, down, maxL)
270
+ for it in range(o["ORDER_ITERS"]):
271
+ if it % 2 == 0:
272
+ for L in range(1, maxL + 1):
273
+ _wmedian(order, L, L - 1, up)
274
+ else:
275
+ for L in range(maxL - 1, -1, -1):
276
+ _wmedian(order, L, L + 1, down)
277
+ _transpose(order, down, maxL)
278
+ c = _total_crossings(order, down, maxL)
279
+ if c < best_c:
280
+ best_c = c
281
+ best = {L: list(order[L]) for L in order}
282
+ order = best
283
+
284
+ # asymmetric per-node extents: a folded decisional reserves its fold's height on the
285
+ # fold side (extents[id] = (up_extra, down_extra)) so the core spaces it to fit and the
286
+ # fold can sit adjacent.
287
+ ext = extents or {}
288
+ ue = {i: vh[i] / 2 + ext.get(i, (0.0, 0.0))[0] for i in vlayer}
289
+ de = {i: vh[i] / 2 + ext.get(i, (0.0, 0.0))[1] for i in vlayer}
290
+ # y-coordinates: stack by order, then relax toward neighbour medians (straightening)
291
+ y: dict = {}
292
+ for L in range(maxL + 1):
293
+ cy = 0.0
294
+ for nid in order[L]:
295
+ y[nid] = cy + ue[nid]
296
+ cy = y[nid] + de[nid] + o["DY"]
297
+ for it in range(o["RELAX_ITERS"]):
298
+ down_sweep = (it % 2 == 0) # align to predecessors, then to successors
299
+ seq = range(1, maxL + 1) if down_sweep else range(maxL - 1, -1, -1)
300
+ for L in seq:
301
+ desired = {}
302
+ for nid in order[L]:
303
+ nb = up.get(nid, []) if down_sweep else down.get(nid, [])
304
+ desired[nid] = _median([y[x] for x in nb]) if nb else y[nid]
305
+ y.update(_place(order[L], desired, ue, de, o["DY"]))
306
+
307
+ # error-port targets: drop to a lane below the happy path
308
+ main_ys = [y[i] for i in ids if i not in err_targets]
309
+ if err_targets and main_ys:
310
+ base = max(main_ys) + o["ERR_GAP"]
311
+ for k, i in enumerate(sorted(err_targets, key=lambda i: vlayer[i])):
312
+ y[i] = base + k * o["DY"]
313
+
314
+ # x by column: spaced by the edge gap (DX - NODE_W) so WIDE nodes (containers) get
315
+ # room; for normal 48px nodes this is identical center-to-center spacing as before.
316
+ col_w = {}
317
+ for i in ids:
318
+ col_w[vlayer[i]] = max(col_w.get(vlayer[i], o["NODE_W"]), sizes[i][0])
319
+ gap = o["DX"] - o["NODE_W"]
320
+ col_x, prev_x, prev_w = {}, None, None
321
+ for L in range(maxL + 1):
322
+ w = col_w.get(L, o["NODE_W"])
323
+ col_x[L] = w / 2 if prev_x is None else prev_x + prev_w / 2 + gap + w / 2
324
+ prev_x, prev_w = col_x[L], w
325
+ positions = {i: {"x": float(col_x[vlayer[i]]), "y": float(y[i])} for i in ids}
326
+ xs = [p["x"] for p in positions.values()]
327
+ ys = [p["y"] for p in positions.values()]
328
+ return {"positions": positions,
329
+ "bbox": {"minX": min(xs), "minY": min(ys), "maxX": max(xs), "maxY": max(ys)}}
330
+
331
+
332
+ # ---------------------------------------------------------------------------
333
+ # partial / subset mode (re-tidy only some nodes; the rest stay put)
334
+ # ---------------------------------------------------------------------------
335
+
336
+ def _avoid_overlap(x, y, w, h, positions, by_id, o, exclude) -> float:
337
+ def collides(cy):
338
+ for oid, op in positions.items():
339
+ if oid in exclude:
340
+ continue
341
+ ow, oh = by_id[oid]["w"], by_id[oid]["h"]
342
+ if abs(op["x"] - x) < (w + ow) / 2 and abs(op["y"] - cy) < (h + oh) / 2 + 1:
343
+ return True
344
+ return False
345
+ if not collides(y):
346
+ return y
347
+ for k in range(1, 80):
348
+ for cand in (y + k * o["DY"], y - k * o["DY"]):
349
+ if not collides(cand):
350
+ return cand
351
+ return y
352
+
353
+
354
+ def _frames(ns: list[dict], by_id: dict, positions: dict, o: dict) -> dict:
355
+ children: dict = defaultdict(list)
356
+ for n in ns:
357
+ pid = n["parent_id"]
358
+ if pid and pid in by_id:
359
+ children[pid].append(n["id"])
360
+ areas = {}
361
+ for cid, kids in children.items():
362
+ xs = [positions[k]["x"] for k in kids]
363
+ ys = [positions[k]["y"] for k in kids]
364
+ ws = [by_id[k]["w"] for k in kids]
365
+ hs = [by_id[k]["h"] for k in kids]
366
+ minx = min(x - w / 2 for x, w in zip(xs, ws))
367
+ maxx = max(x + w / 2 for x, w in zip(xs, ws))
368
+ miny = min(yy - hh / 2 for yy, hh in zip(ys, hs))
369
+ maxy = max(yy + hh / 2 for yy, hh in zip(ys, hs))
370
+ areas[cid] = {"x": minx - o["AREA_PAD"], "y": miny - o["AREA_PAD"],
371
+ "width": (maxx - minx) + 2 * o["AREA_PAD"],
372
+ "height": (maxy - miny) + 2 * o["AREA_PAD"]}
373
+ return areas
374
+
375
+
376
+ def _partial(ns: list[dict], edges: list[dict], by_id: dict, subset: list[str], o: dict) -> dict:
377
+ positions = {n["id"]: dict(n["pos"]) for n in ns}
378
+ idset = set(by_id)
379
+ sub = [nid for nid in subset if nid in idset]
380
+ norm = [(e["source"], e["dest"]) for e in edges if int(e.get("type", 0) or 0) == 0]
381
+ pred: dict = defaultdict(list)
382
+ succ: dict = defaultdict(list)
383
+ for s, d in norm:
384
+ if s in idset and d in idset and s != d:
385
+ succ[s].append(d)
386
+ pred[d].append(s)
387
+ for s in sorted(sub, key=lambda i: (positions[i]["x"], i)):
388
+ xs_p = [positions[p]["x"] for p in pred[s]]
389
+ xs_q = [positions[q]["x"] for q in succ[s]]
390
+ if xs_p and xs_q:
391
+ lo, hi = max(xs_p), min(xs_q)
392
+ tx = (lo + hi) / 2 if hi - lo >= o["DX"] else lo + o["DX"]
393
+ elif xs_p:
394
+ tx = max(xs_p) + o["DX"]
395
+ elif xs_q:
396
+ tx = min(xs_q) - o["DX"]
397
+ else:
398
+ tx = positions[s]["x"]
399
+ ys = [positions[n]["y"] for n in pred[s] + succ[s]]
400
+ ty = _median(ys) if ys else positions[s]["y"]
401
+ ty = _avoid_overlap(tx, ty, by_id[s]["w"], by_id[s]["h"], positions, by_id, o, exclude={s})
402
+ positions[s] = {"x": tx, "y": ty}
403
+ areas = _frames(ns, by_id, positions, o)
404
+ xs = [p["x"] for p in positions.values()] or [0]
405
+ ys = [p["y"] for p in positions.values()] or [0]
406
+ return {"positions": positions, "areas": areas,
407
+ "bbox": {"minX": min(xs), "minY": min(ys), "maxX": max(xs), "maxY": max(ys)}}
408
+
409
+
410
+ # ---------------------------------------------------------------------------
411
+ # branch-aware top-level layout (PROCESIO reading conventions)
412
+ # ---------------------------------------------------------------------------
413
+
414
+ def _reach(starts, adj: dict) -> set:
415
+ seen = set(starts)
416
+ stack = list(starts)
417
+ while stack:
418
+ for m in adj.get(stack.pop(), []):
419
+ if m not in seen:
420
+ seen.add(m)
421
+ stack.append(m)
422
+ return seen
423
+
424
+
425
+ def _deadend_chains(ids, succ, indeg, outdeg):
426
+ """Maximal private chains b1->..->terminal that hang off a branch node (out-degree
427
+ >= 2) and end in a terminal (out-degree 0) without rejoining (every node in the
428
+ chain has in-degree 1). These fold into a vertical tributary."""
429
+ idset = set(ids)
430
+ used = set()
431
+ chains = []
432
+ for b in ids:
433
+ if outdeg.get(b, 0) < 2:
434
+ continue
435
+ for s in succ.get(b, []):
436
+ if s in used or s not in idset:
437
+ continue
438
+ chain, cur, ok = [], s, True
439
+ while True:
440
+ if cur in used or indeg.get(cur, 0) != 1 or cur not in idset:
441
+ ok = False
442
+ break
443
+ chain.append(cur)
444
+ nxt = [x for x in succ.get(cur, []) if x in idset]
445
+ if not nxt:
446
+ break # terminal reached
447
+ if len(nxt) > 1:
448
+ ok = False
449
+ break
450
+ cur = nxt[0]
451
+ if ok and chain and outdeg.get(chain[-1], 0) == 0:
452
+ used.update(chain)
453
+ chains.append((b, chain))
454
+ return chains
455
+
456
+
457
+ def _layout_main(nodes: list[dict], typed_edges: list[tuple[str, str, int]], o: dict) -> dict:
458
+ ids = [n["id"] for n in nodes]
459
+ if not ids:
460
+ return {"positions": {}, "bbox": {"minX": 0, "minY": 0, "maxX": 0, "maxY": 0}}
461
+ by_id = {n["id"]: n for n in nodes}
462
+ idset = set(ids)
463
+ norm = [(s, d) for (s, d, t) in typed_edges if t == 0 and s in idset and d in idset and s != d]
464
+ errs = [(s, d) for (s, d, t) in typed_edges if t == 1 and s in idset and d in idset and s != d]
465
+ succ = defaultdict(list)
466
+ pred = defaultdict(list)
467
+ for s, d in norm:
468
+ succ[s].append(d)
469
+ pred[d].append(s)
470
+ err_targets = {d for _s, d in errs}
471
+ sources = [i for i in ids if not pred.get(i) and i not in err_targets]
472
+ # error region = everything at-or-after an error-port target. The main-flow reach STOPS AT
473
+ # error targets: a normal edge may point INTO one (e.g. a script-error gate's case edge into
474
+ # the shared error Join) but is never traversed THROUGH it. Without the stop, one normal edge
475
+ # into the error sink pulled the whole notify subtree into the main graph and shredded the
476
+ # layout (Networky v2 2026-07-05: 14 edges into one Join laid out mid-canvas).
477
+ main_reach = set(sources)
478
+ _stack = [s for s in sources if s not in err_targets]
479
+ while _stack:
480
+ for m in succ.get(_stack.pop(), []):
481
+ if m not in main_reach:
482
+ main_reach.add(m)
483
+ if m not in err_targets: # reachable as an endpoint, not traversed through
484
+ _stack.append(m)
485
+ error_region = _reach(err_targets, succ) - (main_reach - err_targets)
486
+ main_ids = [i for i in ids if i not in error_region]
487
+ mset = set(main_ids)
488
+ msucc = defaultdict(list)
489
+ mpred = defaultdict(list)
490
+ for s, d in norm:
491
+ if s in mset and d in mset:
492
+ msucc[s].append(d)
493
+ mpred[d].append(s)
494
+ outdeg = {i: len(msucc.get(i, [])) for i in main_ids}
495
+ indeg = {i: len(mpred.get(i, [])) for i in main_ids}
496
+ chains = _deadend_chains(main_ids, msucc, indeg, outdeg)
497
+ chains = [(b, ch) for b, ch in chains if len(ch) <= o["FOLD_MAX"]]
498
+ # at most ONE folded chain per branch node: several dead-end chains folding onto the same
499
+ # column stack on top of each other (Networky v2: the AI Router's 5-way fan collapsed into
500
+ # two overlapping piles). The shortest chain folds; the rest stay in the normal layered flow.
501
+ _best: dict = {}
502
+ for b, ch in chains:
503
+ cur = _best.get(b)
504
+ if cur is None or (len(ch), ch) < (len(cur), cur):
505
+ _best[b] = ch
506
+ chains = sorted(_best.items())
507
+ folded = {n for _b, ch in chains for n in ch}
508
+
509
+ # core = the spine + rejoining branches (everything except folded + error region)
510
+ core_ids = [i for i in main_ids if i not in folded]
511
+ cset = set(core_ids)
512
+ core_edges = [(s, d, 0) for s, d in norm if s in cset and d in cset]
513
+ core_nodes = [by_id[i] for i in core_ids]
514
+
515
+ # pass 1: rough core layout (no reservation) to choose each fold's direction by free space
516
+ pos1 = _layout_layer(core_nodes, core_edges, o)["positions"]
517
+ fold_dir, extents = {}, {}
518
+ for b, ch in chains:
519
+ if b not in pos1:
520
+ continue
521
+ bx, by = pos1[b]["x"], pos1[b]["y"]
522
+ col = [pos1[i]["y"] for i in core_ids if i != b and abs(pos1[i]["x"] - bx) < o["DX"] / 2]
523
+ above = [yv for yv in col if yv < by]
524
+ below = [yv for yv in col if yv > by]
525
+ gap_up = (by - max(above)) if above else float("inf")
526
+ gap_dn = (min(below) - by) if below else float("inf")
527
+ fh = o["DY_AFTER_DECISIONAL"] + (len(ch) - 1) * o["DY"] + o["NODE_H"] / 2
528
+ if gap_up >= gap_dn: # more (or equal) room above -> fold up; tie -> up
529
+ fold_dir[b], extents[b] = -1, (fh, 0.0)
530
+ else: # more room below -> fold down
531
+ fold_dir[b], extents[b] = 1, (0.0, fh)
532
+
533
+ # pass 2: core layout reserving the fold height beside each folded decisional, so the
534
+ # fold sits ADJACENT to its decisional (short edge) instead of at the column extreme
535
+ pos = dict(_layout_layer(core_nodes, core_edges, o, extents=extents)["positions"])
536
+ for b, ch in chains:
537
+ if b not in pos:
538
+ continue
539
+ bx, by = pos[b]["x"], pos[b]["y"]
540
+ step = fold_dir.get(b, -1)
541
+ yy = by
542
+ for idx, node in enumerate(ch):
543
+ yy += step * (o["DY_AFTER_DECISIONAL"] if idx == 0 else o["DY"])
544
+ pos[node] = {"x": bx, "y": yy}
545
+
546
+ # error region -> a parallel lane BELOW everything. Each weakly-CONNECTED COMPONENT of the
547
+ # region anchors under the MEDIAN x of ITS OWN entry sources (error-port sources AND normal
548
+ # edges into the region, e.g. script-error gates). One shared anchor dragged independent
549
+ # error rails toward each other and stretched entry edges across the canvas (Networky v2:
550
+ # 3.5k-px edges into a shared error Join; still wrong after splitting the flow into two
551
+ # rails, because both rails were laid out and anchored as one block).
552
+ if error_region:
553
+ main_maxY = max((p["y"] for p in pos.values()), default=0.0)
554
+ # weakly-connected components over the region's own edges
555
+ nbr = defaultdict(set)
556
+ for s, dd in norm:
557
+ if s in error_region and dd in error_region:
558
+ nbr[s].add(dd)
559
+ nbr[dd].add(s)
560
+ unseen = set(error_region)
561
+ comps = []
562
+ while unseen:
563
+ root = unseen.pop()
564
+ comp = {root}
565
+ stack = [root]
566
+ while stack:
567
+ for m in nbr.get(stack.pop(), ()):
568
+ if m in unseen:
569
+ unseen.discard(m)
570
+ comp.add(m)
571
+ stack.append(m)
572
+ comps.append(comp)
573
+ err_comps = comps
574
+ for comp in comps:
575
+ er_edges = [(s, dd, 0) for s, dd in norm if s in comp and dd in comp]
576
+ erpos = _layout_layer([by_id[i] for i in comp], er_edges, o)["positions"]
577
+ # rotate the rail VERTICAL (chain descends) so its internal edges are drops that
578
+ # cannot cross the horizontal main flow or a decisional fan above it
579
+ order = sorted(erpos, key=lambda i: (erpos[i]["x"], erpos[i]["y"]))
580
+ for k, i in enumerate(order):
581
+ erpos[i] = {"x": 0.0, "y": k * o["DY"]}
582
+ feeders = [s for s, dd in errs if dd in comp and s in pos and s not in error_region]
583
+ feeders += [s for s, dd in norm if dd in comp and s in pos and s not in error_region]
584
+ if feeders:
585
+ anchor_x = _median([pos[s]["x"] for s in feeders])
586
+ else:
587
+ anchor_x = min((p["x"] for p in pos.values()), default=0.0)
588
+ shift_y = (main_maxY + o["ERR_GAP"])
589
+ for i, p in erpos.items():
590
+ pos[i] = {"x": anchor_x + p["x"], "y": p["y"] + shift_y}
591
+
592
+ # loop closure: a back-edge (u -> v where u ends up RIGHT of v) is a retry/loop return.
593
+ # Place the loop-tail chain in a lane below everything and align u under v, so the
594
+ # return reads as a short vertical instead of a long diagonal across the canvas.
595
+ back = sorted(e for e in _back_edges(ids, norm + errs) if e[0] in pos and e[1] in pos)
596
+ if back:
597
+ fpred: dict = defaultdict(list) # full normal adjacency (incl. error region)
598
+ fout: dict = defaultdict(int)
599
+ for s, d in norm:
600
+ fpred[d].append(s)
601
+ fout[s] += 1
602
+ lane_y = max(p["y"] for p in pos.values()) + o["DY"]
603
+ for u, v in back:
604
+ chain = [u]
605
+ cur, branch = u, None
606
+ while True:
607
+ ps = fpred.get(cur, [])
608
+ if len(ps) != 1:
609
+ break
610
+ p = ps[0]
611
+ if fout.get(p, 0) >= 2 or p in chain:
612
+ branch = p
613
+ break
614
+ chain.append(p)
615
+ cur = p
616
+ chain.reverse() # [nearest-branch .. u]
617
+ bx = pos[branch]["x"] if branch in pos else pos[chain[0]]["x"]
618
+ vx = pos[v]["x"]
619
+ n = len(chain)
620
+ for i, node in enumerate(chain):
621
+ tt = i / (n - 1) if n > 1 else 1.0
622
+ pos[node] = {"x": bx + (vx - bx) * tt, "y": lane_y}
623
+
624
+ # off-spine movable sub-regions (for the optional crossing-min pass to relocate as units)
625
+ regions = ([set(c) for c in err_comps] if error_region else []) + [set(ch) for _b, ch in chains]
626
+
627
+ xs = [p["x"] for p in pos.values()] or [0]
628
+ ys = [p["y"] for p in pos.values()] or [0]
629
+ return {"positions": pos, "regions": regions,
630
+ "bbox": {"minX": min(xs), "minY": min(ys), "maxX": max(xs), "maxY": max(ys)}}
631
+
632
+
633
+ # ---------------------------------------------------------------------------
634
+ # public entry: full layout (with For-Each container recursion) or partial
635
+ # ---------------------------------------------------------------------------
636
+
637
+ def _spine_runs(positions: dict, nodes: list, typed: list, o: dict) -> list:
638
+ """Detect maximal horizontal SPINE runs (>= CLUSTER_MIN), left to right. The spine follows
639
+ the same-row successor, so a decisional with a dead-end side branch does NOT break the run."""
640
+ DY = o["DY"]
641
+ elig = {n["id"] for n in nodes
642
+ if not n.get("parent_id") and n.get("kind") != "area" and n["id"] in positions}
643
+ fsucc: dict = defaultdict(list)
644
+ for s, d, t in typed:
645
+ if t == 0 and s in positions and d in positions and s != d:
646
+ fsucc[s].append(d)
647
+ spine_next: dict = {}
648
+ used: set = set()
649
+ for a in elig:
650
+ cands = [b for b in fsucc.get(a, [])
651
+ if b in elig and abs(positions[b]["y"] - positions[a]["y"]) < DY * 0.5
652
+ and positions[b]["x"] > positions[a]["x"] + 1]
653
+ if cands:
654
+ b = min(cands, key=lambda z: positions[z]["x"])
655
+ spine_next[a] = b
656
+ used.add(b)
657
+ starts = [u for u in elig if u in spine_next and u not in used]
658
+ no_clus = o.get("NO_CLUSTER_IDS") or set() # branch-managed chains: the adapter's perpendicular
659
+ indeg: dict = defaultdict(int) # post-pass owns them, so folding them just wastes work
660
+ for s, d, t in typed:
661
+ if t == 0 and s in positions and d in positions and s != d:
662
+ indeg[d] += 1
663
+ runs: list = []
664
+ claimed: set = set()
665
+ for s in sorted(starts, key=lambda i: positions[i]["x"]):
666
+ if s in claimed:
667
+ continue
668
+ run = [s]
669
+ cur = s
670
+ while cur in spine_next and spine_next[cur] not in claimed and spine_next[cur] not in run:
671
+ cur = spine_next[cur]
672
+ run.append(cur)
673
+ claimed |= set(run)
674
+ # split at MERGE nodes (indeg>1): where other flow arrives is a natural fold boundary --
675
+ # The reference layout folds the private stretch BEFORE a Join and leaves the merge onward in-flow.
676
+ seg: list = []
677
+ segs: list = []
678
+ for nid in run:
679
+ if indeg.get(nid, 0) > 1 and seg:
680
+ segs.append(seg)
681
+ seg = []
682
+ seg.append(nid)
683
+ if seg:
684
+ segs.append(seg)
685
+ for sg in segs:
686
+ if len(sg) >= o["CLUSTER_MIN"] and not (no_clus and set(sg) & no_clus):
687
+ runs.append(sg)
688
+ return runs
689
+
690
+
691
+ def _fold_run(positions: dict, run: list, nodes: list, typed: list, o: dict,
692
+ mode: str = "shelves") -> dict | None:
693
+ """Fold ONE spine run and return NEW positions (or None when `mode` doesn't apply).
694
+
695
+ Two shapes, learned from the reference hand-arrangements:
696
+
697
+ - "ufold" (mid-spine runs -- the exit continues into downstream flow): a vertical
698
+ U-pocket hanging BELOW the flow line. Columns descend then ascend (alternating), and
699
+ the final ascending column is bottom-anchored so the EXIT lands back ON the spine row
700
+ -- downstream keeps its row and simply compacts LEFT (his ANAF fold: 7 nodes, 2
701
+ columns, 785px reclaimed). Only for stub-free runs with a real downstream.
702
+ - "shelves" (terminal tails): row-major boustrophedon shelves wrapping downward
703
+ (approved on Start/Callback). Dead-end side stubs travel with their parent and hang
704
+ DOWN into the gap reserved beneath each shelf.
705
+
706
+ Rows below the run (within its x-band) are pushed down to make room; downstream of the
707
+ exit is translated to follow it. Purely additive room -> the caller's 'no worse
708
+ crossings/overlaps' guard decides whether to keep the fold."""
709
+ positions = dict(positions)
710
+ DX, DY = o["DX"], o["DY"]
711
+ # reachability must follow ALL edge types: an error-port region is still downstream flow --
712
+ # leaving it out of the translation set tears it from its anchors when everything else slides.
713
+ fsucc: dict = defaultdict(list)
714
+ for s, d, t in typed:
715
+ if s in positions and d in positions and s != d:
716
+ fsucc[s].append(d)
717
+ rs = set(run)
718
+ x0, y0 = positions[run[0]]["x"], positions[run[0]]["y"]
719
+ exit_old = dict(positions[run[-1]])
720
+ down, stack = set(), [run[-1]]
721
+ while stack:
722
+ for m in fsucc.get(stack.pop(), []):
723
+ if m not in down and m not in rs:
724
+ down.add(m)
725
+ stack.append(m)
726
+ attached: dict = {}
727
+ for rn in run:
728
+ sub: list = []
729
+ st = [m for m in fsucc.get(rn, []) if m not in rs and m not in down]
730
+ seen = set(st)
731
+ while st:
732
+ m = st.pop()
733
+ sub.append(m)
734
+ for k in fsucc.get(m, []):
735
+ if k not in rs and k not in down and k not in seen:
736
+ seen.add(k)
737
+ st.append(k)
738
+ if sub:
739
+ attached[rn] = [(m, positions[m]["x"] - positions[rn]["x"],
740
+ positions[m]["y"] - positions[rn]["y"]) for m in sub]
741
+
742
+ target: dict = {}
743
+ if mode == "ufold":
744
+ # mid-spine only: needs a downstream to keep on-row, and side stubs would collide
745
+ # inside the tight pocket -- those runs fall through to shelves.
746
+ if not down or attached:
747
+ return None
748
+ Dmax = max(2, o["CLUSTER_WRAP"])
749
+ C = max(2, -(-len(run) // Dmax))
750
+ if C % 2: # even column count => last column ASCENDS,
751
+ C += 1 # so the exit can land back on the spine row
752
+ D = -(-len(run) // C)
753
+ # the pocket must hang into EMPTY space (the reference ANAF fold does): shoving occupants
754
+ # aside to make room tears shared clusters apart (seen on Start's Join cluster).
755
+ px_lo, px_hi = x0 - DX * 0.5, x0 + (C - 1) * DX + DX * 0.5
756
+ py_hi = y0 + (D - 1) * DY + DY * 0.5
757
+ for nid, p in positions.items():
758
+ if nid in rs or nid in down:
759
+ continue
760
+ if px_lo <= p["x"] <= px_hi and y0 + DY * 0.5 < p["y"] <= py_hi:
761
+ return None
762
+ cols = [run[k:k + D] for k in range(0, len(run), D)]
763
+ for j, col in enumerate(cols):
764
+ x = x0 + j * DX
765
+ if j % 2 == 0: # descending column
766
+ for r, nid in enumerate(col):
767
+ target[nid] = (x, y0 + r * DY)
768
+ else: # ascending column, bottom-anchored to end at row 0
769
+ for r, nid in enumerate(col):
770
+ target[nid] = (x, y0 + (len(col) - 1 - r) * DY)
771
+ # the downstream slide must land in FREE space: if the translated downstream would
772
+ # collide with a parallel branch that does NOT move, de-overlap tears the collision
773
+ # apart (seen on Start). The reference ANAF pocket works because everything downstream of
774
+ # the run moves together -- nothing is left to collide with.
775
+ tsx = target[run[-1]][0] - exit_old["x"]
776
+ tsy = target[run[-1]][1] - exit_old["y"]
777
+ stay = [p for nid, p in positions.items() if nid not in rs and nid not in down]
778
+ for nid in down:
779
+ nx2, ny2 = positions[nid]["x"] + tsx, positions[nid]["y"] + tsy
780
+ if any(abs(q["x"] - nx2) < DX * 0.5 and abs(q["y"] - ny2) < DY * 0.5 for q in stay):
781
+ return None
782
+ block_h = 0.0 # pocket verified empty -> nothing to push
783
+ x_lo, x_hi = px_lo, px_hi
784
+ else:
785
+ W = max(2, o["CLUSTER_WRAP"])
786
+ shelves = [run[i:i + W] for i in range(0, len(run), W)]
787
+ yy = y0
788
+ for si, shelf in enumerate(shelves):
789
+ depth = 0.0
790
+ for rn in shelf:
791
+ for (_m, _dx, dy) in attached.get(rn, []):
792
+ depth = max(depth, abs(dy)) # branches all hang DOWN into the reserved gap
793
+ # odd (right-to-left) shelves are RIGHT-ALIGNED: a short remainder sits under the
794
+ # END of the previous shelf (short vertical drop), not carriage-returned to the
795
+ # left (the reference ANAF fix: the terminal Stop goes below the last node, +480 right).
796
+ seq = shelf if si % 2 == 0 else list(reversed(shelf))
797
+ base = 0 if si % 2 == 0 else (W - len(shelf))
798
+ for ci, rn in enumerate(seq):
799
+ target[rn] = (x0 + (base + ci) * DX, yy)
800
+ yy += DY + depth
801
+ block_h = (yy - DY) - y0
802
+ x_lo, x_hi = x0 - DX * 0.5, x0 + (W - 1) * DX + DX * 0.5
803
+
804
+ # MAKE ROOM: push rows strictly below the run's row, within the fold's x-band, down by
805
+ # the fold's added height. Uniform shift preserves relative order -> no new crossings.
806
+ moved = rs | down | {m for lst in attached.values() for (m, _a, _b) in lst}
807
+ if block_h > 0:
808
+ for nid in list(positions):
809
+ if nid in moved:
810
+ continue
811
+ px, py = positions[nid]["x"], positions[nid]["y"]
812
+ if py > y0 + DY * 0.5 and x_lo <= px <= x_hi:
813
+ positions[nid] = {"x": px, "y": py + block_h}
814
+ for rn in run:
815
+ nx, ny = target[rn]
816
+ for (m, dx, dy) in attached.get(rn, []):
817
+ positions[m] = {"x": nx + dx, "y": ny + abs(dy)} # hang the branch downward
818
+ positions[rn] = {"x": nx, "y": ny}
819
+ sx = positions[run[-1]]["x"] - exit_old["x"]
820
+ sy = positions[run[-1]]["y"] - exit_old["y"]
821
+ for nid in down:
822
+ positions[nid] = {"x": positions[nid]["x"] + sx, "y": positions[nid]["y"] + sy}
823
+ return positions
824
+
825
+
826
+ def _cluster_chains(positions: dict, nodes: list, typed: list, o: dict) -> dict:
827
+ """OPTIONAL (off by default): fold every long horizontal spine run into compact shelves.
828
+ Non-incremental (folds all runs); layout() drives an incremental, guard-checked variant for
829
+ live use so that a fold which would add crossings is skipped instead of poisoning the rest."""
830
+ positions = dict(positions)
831
+ for run in _spine_runs(positions, nodes, typed, o):
832
+ positions = _fold_run(positions, run, nodes, typed, o)
833
+ return positions
834
+
835
+
836
+ def _seg_int(a: dict, b: dict, c: dict, d: dict) -> bool:
837
+ """Do segments a-b and c-d properly intersect?"""
838
+ def orient(p, q, r):
839
+ return (q["y"] - p["y"]) * (r["x"] - q["x"]) - (q["x"] - p["x"]) * (r["y"] - q["y"])
840
+ o1, o2 = orient(a, b, c), orient(a, b, d)
841
+ o3, o4 = orient(c, d, a), orient(c, d, b)
842
+ return (o1 > 0) != (o2 > 0) and (o3 > 0) != (o4 > 0)
843
+
844
+
845
+ def _loop_curl(rset: set, typed: list, P: dict, o: dict) -> dict | None:
846
+ """If a region is a retry/error LOOP -- it has an ENTRY edge from the main flow AND a
847
+ RETURN edge back to the main flow (e.g. an error handler that retries by looping to a
848
+ Join) -- lay it as two vertical columns aligned to its anchors: the loop tail rises above
849
+ its RETURN target, the decisional + the fail path rise above the ENTRY source. Both
850
+ boundary edges become vertical, so they don't cross and the decisional reads in-flow.
851
+ Returns {id: {x, y}} for every region node, or None when the pattern doesn't apply."""
852
+ from collections import deque
853
+ DY = o["DY"]
854
+ rsucc: dict = defaultdict(list)
855
+ entry = ret = None
856
+ for (s, d, tp) in typed:
857
+ if s not in P or d not in P:
858
+ continue
859
+ if s in rset and d in rset and tp == 0:
860
+ rsucc[s].append(d)
861
+ elif s not in rset and d in rset:
862
+ if entry is None or tp == 1: # prefer the error edge as the entry
863
+ entry = (s, d)
864
+ elif s in rset and d not in rset and ret is None:
865
+ ret = (s, d)
866
+ if not entry or not ret:
867
+ return None
868
+ entry_node, src_main = entry[1], entry[0]
869
+ exit_node, ret_main = ret[0], ret[1]
870
+ if entry_node not in rset or exit_node not in rset:
871
+ return None
872
+ ax, ay = P[src_main]["x"], P[src_main]["y"]
873
+ bx, by = P[ret_main]["x"], P[ret_main]["y"]
874
+ if abs(ax - bx) < o["DX"] / 2: # need two distinct columns
875
+ return None
876
+ par = {entry_node: None}
877
+ q = deque([entry_node])
878
+ while q:
879
+ u = q.popleft()
880
+ for v in rsucc.get(u, []):
881
+ if v not in par:
882
+ par[v] = u
883
+ q.append(v)
884
+ if exit_node not in par:
885
+ return None
886
+ path = []
887
+ c = exit_node
888
+ while c is not None:
889
+ path.append(c)
890
+ c = par[c]
891
+ path.reverse() # [entry_node .. exit_node]
892
+ loop_tail = [n for n in path if n != entry_node]
893
+ fail = [n for n in rset if n not in loop_tail and n != entry_node]
894
+ fail_order: list = []
895
+ c = entry_node
896
+ fpool = set(fail)
897
+ while True:
898
+ nxt = [v for v in rsucc.get(c, []) if v in fpool and v not in fail_order]
899
+ if not nxt:
900
+ break
901
+ fail_order.append(nxt[0])
902
+ c = nxt[0]
903
+ for n in fail: # any unreached fail nodes
904
+ if n not in fail_order:
905
+ fail_order.append(n)
906
+ out = {entry_node: {"x": ax, "y": ay - 2 * DY}}
907
+ for k, n in enumerate(fail_order): # fail path stacked above the decisional
908
+ out[n] = {"x": ax, "y": ay - (3 + k) * DY}
909
+ for k, n in enumerate(reversed(loop_tail)): # loop tail rises above its return target
910
+ out[n] = {"x": bx, "y": by - (1 + k) * DY}
911
+ return out if set(out) == rset else None
912
+
913
+
914
+ def _minimize_crossings(positions: dict, nodes: list, typed: list, o: dict, movable: set,
915
+ regions: list | None = None) -> dict:
916
+ """OPTIONAL (off by default): best-effort edge-crossing reduction, in two phases.
917
+
918
+ PHASE A -- region re-placement: each off-spine sub-region (the error region, dead-end
919
+ folds) is RE-LAID-OUT compactly (a chain collapses to one row) and offered up FLUSH just
920
+ above or just below the main body; whichever placement has the fewest crossings wins, and
921
+ the region is only replaced if that is STRICTLY better than leaving it where the base put
922
+ it. This is what lets an error/retry loop that the base drops into a low lane (where its
923
+ connectors cross a long skip-edge) move to a tidy lane on the crossing-free side, close to
924
+ the flow -- matching how a human re-arranges it. Compact + flush, so no far teleport and no
925
+ empty gap. It never overlaps other nodes/frames.
926
+
927
+ PHASE B -- bounded, distance-penalized per-node local search: each movable top-level node
928
+ is nudged within a small grid neighbourhood, keeping the move that most lowers
929
+ `crossings + PENALTY * grid-steps-from-start`. Distance being a REAL cost keeps every move
930
+ LOCAL (e.g. sliding a skip-edge endpoint one row), never a teleport.
931
+
932
+ Counts ALL drawn edges (normal + error). Strictly-better-only + monotone bounded score =>
933
+ never worse than the input, and it terminates. Containers/children stay fixed. Not zero-
934
+ guaranteed -- structural crossings a compact/local move can't remove (a container-exit edge)
935
+ are left as-is by design."""
936
+ by_id = {n["id"]: n for n in nodes}
937
+ edges = [(s, d) for s, d, t in typed
938
+ if t in (0, 1) and s in positions and d in positions and s != d]
939
+ norm = [(s, d, t) for (s, d, t) in typed if t == 0]
940
+ inc: dict = defaultdict(list)
941
+ for i, (s, d) in enumerate(edges):
942
+ inc[s].append(i)
943
+ inc[d].append(i)
944
+ P = positions
945
+
946
+ def rp(nid):
947
+ # RENDER point of a node for crossing tests: the designer attaches edges to an area
948
+ # (For-Each) frame at its TOP-LEFT (the frame's stored position), not the node centre
949
+ # that `positions` holds. Using the centre miscounts every container-boundary edge by
950
+ # (w/2, h/2). All non-area nodes render where they sit.
951
+ p = P[nid]
952
+ if by_id.get(nid, {}).get("kind") == "area":
953
+ return {"x": p["x"] - by_id[nid]["w"] / 2, "y": p["y"] - by_id[nid]["h"] / 2}
954
+ return p
955
+
956
+ def box_hit(nid, x, y, oid):
957
+ return (abs(P[oid]["x"] - x) < (by_id[nid]["w"] + by_id[oid]["w"]) / 2
958
+ and abs(P[oid]["y"] - y) < (by_id[nid]["h"] + by_id[oid]["h"]) / 2)
959
+
960
+ def overlaps(nid, x, y):
961
+ return any(oid != nid and box_hit(nid, x, y, oid) for oid in P)
962
+
963
+ def total_cross():
964
+ n = 0
965
+ for i in range(len(edges)):
966
+ s, d = edges[i]
967
+ a, b = rp(s), rp(d)
968
+ for j in range(i + 1, len(edges)):
969
+ s2, d2 = edges[j]
970
+ if s2 in (s, d) or d2 in (s, d):
971
+ continue
972
+ if _seg_int(a, b, rp(s2), rp(d2)):
973
+ n += 1
974
+ return n
975
+
976
+ # PHASE A: re-lay each off-spine region compactly, place it FLUSH above/below on the side
977
+ # that cuts the most crossings; replace only if strictly better (else leave base untouched).
978
+ for region in (regions or []):
979
+ region = [r for r in region if r in P]
980
+ if not region:
981
+ continue
982
+ rset = set(region)
983
+ others = [i for i in P if i not in rset]
984
+ if not others:
985
+ continue
986
+ base_c = total_cross()
987
+ snap = {i: dict(P[i]) for i in region}
988
+ rnodes = [by_id[i] for i in region if i in by_id]
989
+ redges = [(s, d, tp) for (s, d, tp) in norm if s in rset and d in rset]
990
+ try:
991
+ rel = _layout_layer(rnodes, redges, o)["positions"]
992
+ except Exception: # noqa: BLE001
993
+ rel = {}
994
+ rel = {i: rel[i] for i in region if i in rel}
995
+ if len(rel) != len(region):
996
+ continue
997
+ rminx = min(p["x"] for p in rel.values())
998
+ rminy = min(p["y"] for p in rel.values())
999
+ rh = max(p["y"] for p in rel.values()) - rminy
1000
+ cur_minx = min(snap[i]["x"] for i in region)
1001
+ mtop = min(P[i]["y"] for i in others)
1002
+ mbot = max(P[i]["y"] for i in others)
1003
+
1004
+ def place(yoff):
1005
+ for i in region:
1006
+ P[i] = {"x": cur_minx + (rel[i]["x"] - rminx),
1007
+ "y": yoff + (rel[i]["y"] - rminy)}
1008
+
1009
+ def region_clear():
1010
+ return not any(box_hit(i, P[i]["x"], P[i]["y"], oid)
1011
+ for i in region for oid in others)
1012
+
1013
+ # candidate placements: compact FLUSH above/below, plus a LOOP-CURL for retry loops.
1014
+ candidates = []
1015
+ for yoff in (mtop - o["DY"] - rh, mbot + o["DY"]):
1016
+ candidates.append({i: {"x": cur_minx + (rel[i]["x"] - rminx),
1017
+ "y": yoff + (rel[i]["y"] - rminy)} for i in region})
1018
+ curl = _loop_curl(rset, typed, P, o)
1019
+ if curl:
1020
+ candidates.append(curl)
1021
+
1022
+ best_c, best = base_c, snap
1023
+ for cand in candidates:
1024
+ for i in region:
1025
+ P[i] = cand[i]
1026
+ c = total_cross() if region_clear() else float("inf")
1027
+ if c < best_c - 1e-9:
1028
+ best_c, best = c, {i: dict(P[i]) for i in region}
1029
+ for i in region:
1030
+ P[i] = best[i]
1031
+
1032
+ # PHASE B: bounded, distance-penalized per-node local search (fine-tune).
1033
+ PEN = o.get("MINCROSS_MOVE_PENALTY", 0.5)
1034
+ R = o.get("MINCROSS_RANGE", 6)
1035
+ DX, DY = o["DX"], o["DY"]
1036
+ region_nodes = set().union(*[set(r) for r in (regions or [])]) if regions else set()
1037
+ startp = {nid: dict(P[nid]) for nid in movable if nid in P}
1038
+ # Phase A owns each region's compact internal layout; Phase B must NOT drag a region node
1039
+ # (e.g. a decisional) out of place chasing a crossing -- it only fine-tunes free nodes.
1040
+ order = sorted(m for m in movable if m in P and inc.get(m) and m not in region_nodes)
1041
+
1042
+ def disp():
1043
+ # QUADRATIC per-node displacement: a 1-step nudge is cheap (fine-tuning, this phase's
1044
+ # job) but a multi-row flight is prohibitive -- Phase B must never fling a node far
1045
+ # from its neighbours to shave one crossing (rule: locality beats a single crossing;
1046
+ # seen on 0GetInfo where found? was launched 4 rows above its own row).
1047
+ return sum((abs(P[m]["x"] - startp[m]["x"]) / DX + abs(P[m]["y"] - startp[m]["y"]) / DY) ** 2
1048
+ for m in startp)
1049
+
1050
+ LEN = o.get("MINCROSS_LEN_PENALTY", 0.15)
1051
+
1052
+ def elen():
1053
+ # total edge length (render points, in grid units) -- a READABILITY term: prefers the
1054
+ # crossing fix that keeps edges SHORT, so a node is never flung downstream of its own
1055
+ # successors (reversing/lengthening its edges) just to shave a crossing.
1056
+ return sum((abs(rp(s)["x"] - rp(d)["x"]) + abs(rp(s)["y"] - rp(d)["y"])) / DX
1057
+ for s, d in edges)
1058
+
1059
+ # FLOW MONOTONICITY: a crossing fix must never point an edge BACKWARD -- a node stays
1060
+ # right of its normal-edge predecessors and left of its successors (back/loop edges
1061
+ # exempt). Rule: readability of the left-to-right flow beats any crossing saved.
1062
+ mono_edges = [(s, d) for s, d in edges if s in P and d in P]
1063
+ mono_back = _back_edges(list(P), mono_edges)
1064
+ mono_pred: dict = defaultdict(list)
1065
+ mono_succ: dict = defaultdict(list)
1066
+ for s, d in mono_edges:
1067
+ if (s, d) in mono_back:
1068
+ continue
1069
+ mono_succ[s].append(d)
1070
+ mono_pred[d].append(s)
1071
+
1072
+ def mono_ok(nid, x):
1073
+ return (all(x >= P[p]["x"] - DX * 0.25 for p in mono_pred.get(nid, []))
1074
+ and all(x <= P[s2]["x"] + DX * 0.25 for s2 in mono_succ.get(nid, [])))
1075
+
1076
+ for _ in range(o.get("MINCROSS_ITERS", 6)):
1077
+ improved = False
1078
+ for nid in order:
1079
+ cur = dict(P[nid])
1080
+ best_score = total_cross() + PEN * disp() + (LEN * elen() if LEN else 0.0)
1081
+ best_p = cur
1082
+ for kx in range(-R, R + 1):
1083
+ for ky in range(-R, R + 1):
1084
+ if kx == 0 and ky == 0:
1085
+ continue
1086
+ x, y = cur["x"] + kx * DX, cur["y"] + ky * DY
1087
+ if overlaps(nid, x, y) or not mono_ok(nid, x):
1088
+ continue
1089
+ P[nid] = {"x": x, "y": y}
1090
+ sc = total_cross() + PEN * disp() + (LEN * elen() if LEN else 0.0)
1091
+ if sc < best_score - 1e-9:
1092
+ best_score, best_p = sc, {"x": x, "y": y}
1093
+ P[nid] = best_p
1094
+ if best_p != cur:
1095
+ improved = True
1096
+ if not improved:
1097
+ break
1098
+ return positions
1099
+
1100
+
1101
+ def _count_crossings(positions: dict, edges: list, by_id: dict) -> int:
1102
+ """Render-faithful crossing count: area (For-Each) nodes attach edges at their frame
1103
+ top-left (centre - w/2, h/2), everything else at its point. `edges` = (src, dst) pairs."""
1104
+ def rp(nid):
1105
+ p = positions[nid]
1106
+ n = by_id.get(nid)
1107
+ if n and n.get("kind") == "area":
1108
+ return {"x": p["x"] - n["w"] / 2, "y": p["y"] - n["h"] / 2}
1109
+ return p
1110
+ es = [(s, d) for (s, d) in edges if s in positions and d in positions and s != d]
1111
+ n = 0
1112
+ for i in range(len(es)):
1113
+ s, d = es[i]
1114
+ a, b = rp(s), rp(d)
1115
+ for j in range(i + 1, len(es)):
1116
+ s2, d2 = es[j]
1117
+ if s2 in (s, d) or d2 in (s, d):
1118
+ continue
1119
+ if _seg_int(a, b, rp(s2), rp(d2)):
1120
+ n += 1
1121
+ return n
1122
+
1123
+
1124
+ def _count_overlaps(positions: dict, by_id: dict, ids) -> int:
1125
+ """Number of overlapping node pairs among `ids` (box test on node w/h)."""
1126
+ ii = [i for i in ids if i in positions]
1127
+ n = 0
1128
+ for a in range(len(ii)):
1129
+ for b in range(a + 1, len(ii)):
1130
+ i, j = ii[a], ii[b]
1131
+ if (abs(positions[i]["x"] - positions[j]["x"]) < (by_id[i]["w"] + by_id[j]["w"]) / 2
1132
+ and abs(positions[i]["y"] - positions[j]["y"]) < (by_id[i]["h"] + by_id[j]["h"]) / 2):
1133
+ n += 1
1134
+ return n
1135
+
1136
+
1137
+ def _resolve_overlaps(positions: dict, by_id: dict, movable: set, o: dict) -> dict:
1138
+ """Deterministic final safety net: if a movable top-level node overlaps another node or a
1139
+ container frame, nudge it DOWN a row until clear. A NO-OP when nothing overlaps, so clean
1140
+ layouts stay byte-identical. Guarantees the 'no node overlaps' invariant that independent
1141
+ off-spine lane placements (error region + dead-end chains dropped into the same lane) can
1142
+ otherwise violate. Grid-aligned (pushes by DY), so alignment is preserved."""
1143
+ DY = o["DY"]
1144
+ order = sorted((m for m in movable if m in positions),
1145
+ key=lambda i: (positions[i]["y"], positions[i]["x"], str(i)))
1146
+
1147
+ def hit(nid, oid):
1148
+ p, q = positions[nid], positions[oid]
1149
+ return (abs(p["x"] - q["x"]) < (by_id[nid]["w"] + by_id[oid]["w"]) / 2
1150
+ and abs(p["y"] - q["y"]) < (by_id[nid]["h"] + by_id[oid]["h"]) / 2)
1151
+
1152
+ for _ in range(len(order) + 5):
1153
+ moved = False
1154
+ for nid in order:
1155
+ clash = [oid for oid in positions if oid != nid and hit(nid, oid)]
1156
+ if clash:
1157
+ drop = max(positions[oid]["y"] for oid in clash) + DY
1158
+ positions[nid] = {"x": positions[nid]["x"], "y": drop}
1159
+ moved = True
1160
+ if not moved:
1161
+ break
1162
+ return positions
1163
+
1164
+
1165
+ def layout(nodes: list[dict], edges: list[dict], opts: dict | None = None,
1166
+ subset: list[str] | None = None) -> dict:
1167
+ """Lay out a node/edge graph. Returns {positions:{id:{x,y}}, areas:{id:{x,y,width,
1168
+ height}}, bbox}. nodes: {id, width?, height?, parent_id?, kind?, position?}. edges:
1169
+ {source, dest, type?} (type 1 = error edge -> target drops to a low lane).
1170
+
1171
+ `subset` -> reposition ONLY those node ids among their fixed neighbours (re-tidy mode);
1172
+ every other node keeps its current position."""
1173
+ o = {**DEFAULTS, **(opts or {})}
1174
+ ns = [_node(n) for n in nodes]
1175
+ by_id = {n["id"]: n for n in ns}
1176
+ if subset is not None:
1177
+ return _partial(ns, edges, by_id, subset, o)
1178
+ typed = [(e["source"], e["dest"], int(e.get("type", 0) or 0)) for e in edges]
1179
+
1180
+ children: dict = defaultdict(list)
1181
+ top: list[dict] = []
1182
+ for n in ns:
1183
+ pid = n["parent_id"]
1184
+ (children[pid].append(n) if pid and pid in by_id else top.append(n))
1185
+
1186
+ child_rel: dict = {}
1187
+ for cid, kids in children.items():
1188
+ kid_ids = {k["id"] for k in kids}
1189
+ sub = [(s, d, t) for (s, d, t) in typed if s in kid_ids and d in kid_ids]
1190
+ res = _layout_layer(kids, sub, o)
1191
+ b = res["bbox"]
1192
+ # reserve room at the LEFT for the implicit ForEach-Start (+ one DX) and a top pad;
1193
+ # min right/bottom pad. Evidence-based (corpus of 39 For-Each + the reference arrangement).
1194
+ fe_left = o["DX"] + o["FE_START_PAD"]
1195
+ offx, offy = fe_left - b["minX"], o["FE_TOP"] - b["minY"]
1196
+ for k in kids:
1197
+ p = res["positions"][k["id"]]
1198
+ child_rel[k["id"]] = {"x": p["x"] + offx, "y": p["y"] + offy}
1199
+ by_id[cid]["w"] = (b["maxX"] - b["minX"]) + fe_left + o["FE_RIGHT_PAD"]
1200
+ by_id[cid]["h"] = (b["maxY"] - b["minY"]) + o["FE_TOP"] + o["FE_BOTTOM_PAD"]
1201
+
1202
+ top_ids = {n["id"] for n in top}
1203
+ top_edges = [(s, d, t) for (s, d, t) in typed if s in top_ids and d in top_ids]
1204
+ res = _layout_main(top, top_edges, o)
1205
+ top0 = dict(res["positions"])
1206
+ regions = res.get("regions", [])
1207
+ all_pairs = [(s, d) for (s, d, _t) in typed]
1208
+
1209
+ def _place_children(top_pos):
1210
+ """Place container children on a top-level placement; return (full_pos, areas)."""
1211
+ pos = dict(top_pos)
1212
+ ars: dict = {}
1213
+ for cid, kids in children.items():
1214
+ cpos = pos.get(cid, {"x": 0.0, "y": 0.0})
1215
+ fx = cpos["x"] - by_id[cid]["w"] / 2
1216
+ fy = cpos["y"] - by_id[cid]["h"] / 2
1217
+ for k in kids:
1218
+ rel = child_rel[k["id"]]
1219
+ pos[k["id"]] = {"x": fx + rel["x"], "y": fy + rel["y"]}
1220
+ ars[cid] = {"x": fx, "y": fy, "width": by_id[cid]["w"], "height": by_id[cid]["h"]}
1221
+ return pos, ars
1222
+
1223
+ def _mincross_top(top_pos):
1224
+ """Min-crossings (needs children placed for faithful crossing counts) -> TOP-LEVEL
1225
+ positions only. Min-crossings never moves children or containers, so the top-level slice
1226
+ is enough to re-place children afterward."""
1227
+ full, _ars = _place_children(top_pos)
1228
+ if o.get("MINIMIZE_CROSSINGS"):
1229
+ mv = {n["id"] for n in ns if not n["parent_id"] and n["kind"] != "area" and n["id"] in full}
1230
+ full = _minimize_crossings(full, ns, typed, o, mv, regions)
1231
+ return {n["id"]: full[n["id"]] for n in top if n["id"] in full}
1232
+
1233
+ def _deoverlap(pos):
1234
+ dv = {n["id"] for n in ns if not n["parent_id"] and n["kind"] != "area" and n["id"] in pos}
1235
+ return _resolve_overlaps(pos, by_id, dv, o)
1236
+
1237
+ def _final_score(pos):
1238
+ return (_count_overlaps(pos, by_id, list(pos)), _count_crossings(pos, all_pairs, by_id))
1239
+
1240
+ # Optional passes IN ORDER: base layout -> MINIMIZE_CROSSINGS -> CLUSTERING (LAST) -> de-overlap.
1241
+ # (Cycle-aware collapse wraps this whole call from the adapter, so it runs first of all.)
1242
+ top_mc = _mincross_top(top0)
1243
+ positions, areas = _place_children(top_mc)
1244
+ positions = _deoverlap(positions)
1245
+ if o.get("CLUSTER"):
1246
+ # Clustering is the FINAL transformation, applied to the MIN-CROSSINGS result. Fold the
1247
+ # spine runs ONE AT A TIME, keeping each fold only if the full-layout (overlaps, crossings)
1248
+ # is no worse than before. Incremental (not all-at-once) so that a run whose fold would add
1249
+ # a crossing is skipped instead of poisoning the runs that fold cleanly.
1250
+ def _max_edge(pos):
1251
+ return max((abs(pos[s]["x"] - pos[d]["x"]) + abs(pos[s]["y"] - pos[d]["y"])
1252
+ for s, d in all_pairs if s in pos and d in pos), default=0.0)
1253
+
1254
+ cur_top = dict(top_mc)
1255
+ best_score = _final_score(positions)
1256
+ # edge limit anchored to the BASE (pre-fold) max edge -- NOT ratcheting per kept fold,
1257
+ # or each fold inflates the limit for the next one and a late fold can tear geometry.
1258
+ # Slack covers legit growth: a serpentine's wrap connector lengthens one edge
1259
+ # (Callback 180->340), a fold beside a tall region reroutes one edge (Start ~1260).
1260
+ edge_lim = _max_edge(positions) * 2.0 + 400
1261
+ for run in _spine_runs(cur_top, top, top_edges, o):
1262
+ for mode in ("ufold", "shelves"): # U-pocket for mid-spine runs, shelves for tails
1263
+ trial_top = _fold_run(cur_top, run, top, top_edges, o, mode=mode)
1264
+ if trial_top is None:
1265
+ continue
1266
+ tpos, tars = _place_children(trial_top)
1267
+ tpos = _deoverlap(tpos)
1268
+ if _final_score(tpos) <= best_score and _max_edge(tpos) <= edge_lim:
1269
+ cur_top, positions, areas = trial_top, tpos, tars
1270
+ best_score = _final_score(tpos)
1271
+ break
1272
+
1273
+ if positions:
1274
+ minx = min(p["x"] for p in positions.values())
1275
+ miny = min(p["y"] for p in positions.values())
1276
+ dx, dy = o["MARGIN"] - minx, o["MARGIN"] - miny
1277
+ for p in positions.values():
1278
+ p["x"] += dx
1279
+ p["y"] += dy
1280
+ for a in areas.values():
1281
+ a["x"] += dx
1282
+ a["y"] += dy
1283
+ xs = [p["x"] for p in positions.values()] or [0]
1284
+ ys = [p["y"] for p in positions.values()] or [0]
1285
+ return {"positions": positions, "areas": areas,
1286
+ "bbox": {"minX": min(xs), "minY": min(ys), "maxX": max(xs), "maxY": max(ys)}}
1287
+
1288
+
1289
+
1290
+ def designer_writes(positions: dict, areas: dict, parent_of: dict) -> dict:
1291
+ """Map engine output to designer node values honoring the PROCESIO container
1292
+ convention: an node's position is its frame TOP-LEFT (plus areaSize); a
1293
+ container child's position is RELATIVE to its parent area frame; everything else is
1294
+ absolute. maps child_id -> parent area id. Returns
1295
+ {id: {"pos": {x, y}, "areaSize": {...} | None}}."""
1296
+ out = {}
1297
+ for nid in set(positions) | set(areas):
1298
+ if nid in areas:
1299
+ fr = areas[nid]
1300
+ out[nid] = {"pos": {"x": round(fr["x"], 2), "y": round(fr["y"], 2)},
1301
+ "areaSize": {"width": round(fr["width"], 2),
1302
+ "height": round(fr["height"], 2), "x": 0, "y": 0}}
1303
+ elif parent_of.get(nid) in areas and nid in positions:
1304
+ fr = areas[parent_of[nid]]
1305
+ p = positions[nid]
1306
+ out[nid] = {"pos": {"x": round(p["x"] - fr["x"], 2),
1307
+ "y": round(p["y"] - fr["y"], 2)}, "areaSize": None}
1308
+ elif nid in positions:
1309
+ p = positions[nid]
1310
+ out[nid] = {"pos": {"x": round(p["x"], 2), "y": round(p["y"], 2)}, "areaSize": None}
1311
+ return out