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.
- procesio_cli/__init__.py +18 -0
- procesio_cli/_cli.py +71 -0
- procesio_cli/agents/__init__.py +0 -0
- procesio_cli/agents/_lib/__init__.py +0 -0
- procesio_cli/agents/_lib/actiondef.py +24 -0
- procesio_cli/agents/_lib/errors.py +40 -0
- procesio_cli/agents/_lib/reversibility.py +236 -0
- procesio_cli/agents/_lib/toolrunner.py +141 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-PLAYBOOK.md +62 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-PROCESIO-INTEROP.md +76 -0
- procesio_cli/agents/connector-builder/CONNECTOR-BUILDER-TROUBLESHOOTING.md +60 -0
- procesio_cli/agents/connector-builder/README.md +51 -0
- procesio_cli/agents/connector-builder/agent.yaml +61 -0
- procesio_cli/agents/connector-builder/context.py +19 -0
- procesio_cli/agents/connector-builder/handlers/__init__.py +1 -0
- procesio_cli/agents/connector-builder/handlers/checklist.py +71 -0
- procesio_cli/agents/connector-builder/handlers/guidance.py +37 -0
- procesio_cli/agents/connector-builder/handlers/nextstep.py +221 -0
- procesio_cli/agents/connector-builder/handlers/status.py +35 -0
- procesio_cli/agents/connector-builder/knowledge.py +47 -0
- procesio_cli/agents/connector-builder/main.py +93 -0
- procesio_cli/agents/connector-builder/tests/conftest.py +95 -0
- procesio_cli/agents/connector-builder/tests/test_cba_agent.py +148 -0
- procesio_cli/agents/orchestrator/__init__.py +0 -0
- procesio_cli/agents/orchestrator/bootstraplib.py +166 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/00-INDEX.md +60 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/01-PROCESSES.md +230 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/02-FORMS.md +287 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/03-INTEGRATIONS.md +384 -0
- procesio_cli/agents/procesio/IMPLEMENTATION-PATTERNS/04-DATABASE.md +293 -0
- procesio_cli/agents/procesio/PROCESIO-API-RELIABILITY-DOCTRINE.md +113 -0
- procesio_cli/agents/procesio/PROCESIO-BEST-PRACTICES.md +316 -0
- procesio_cli/agents/procesio/PROCESIO-BUILD-AND-TEST-PLAYBOOK.md +217 -0
- procesio_cli/agents/procesio/PROCESIO-DATASTORE.md +72 -0
- procesio_cli/agents/procesio/PROCESIO-ENVIRONMENTS.md +62 -0
- procesio_cli/agents/procesio/PROCESIO-SCHEDULING.md +47 -0
- procesio_cli/agents/procesio/PROCESIO-VISUAL-ORGANIZATION.md +175 -0
- procesio_cli/agents/procesio/README.md +97 -0
- procesio_cli/agents/procesio/__init__.py +0 -0
- procesio_cli/agents/procesio/agent.yaml +73 -0
- procesio_cli/agents/procesio/audit.py +226 -0
- procesio_cli/agents/procesio/checklist.py +135 -0
- procesio_cli/agents/procesio/context.py +19 -0
- procesio_cli/agents/procesio/handlers/__init__.py +0 -0
- procesio_cli/agents/procesio/handlers/audit.py +39 -0
- procesio_cli/agents/procesio/handlers/checklist.py +34 -0
- procesio_cli/agents/procesio/handlers/guidance.py +36 -0
- procesio_cli/agents/procesio/handlers/status.py +33 -0
- procesio_cli/agents/procesio/handlers/verify.py +39 -0
- procesio_cli/agents/procesio/knowledge.py +115 -0
- procesio_cli/agents/procesio/main.py +89 -0
- procesio_cli/agents/procesio/tests/__init__.py +0 -0
- procesio_cli/agents/procesio/tests/conftest.py +62 -0
- procesio_cli/agents/procesio/tests/test_audit.py +91 -0
- procesio_cli/agents/procesio/tests/test_checklist.py +28 -0
- procesio_cli/agents/procesio/tests/test_guidance.py +78 -0
- procesio_cli/agents/procesio/tests/test_manifest_sync.py +48 -0
- procesio_cli/agents/procesio/tests/test_verify.py +134 -0
- procesio_cli/agents/procesio/tests/test_workspace_scope.py +97 -0
- procesio_cli/agents/procesio/verifylib.py +222 -0
- procesio_cli/dashboard/README.md +117 -0
- procesio_cli/dashboard/__init__.py +18 -0
- procesio_cli/dashboard/serve.py +98 -0
- procesio_cli/dashboard/server/__init__.py +1 -0
- procesio_cli/dashboard/server/config_schemas.py +92 -0
- procesio_cli/dashboard/server/google.py +103 -0
- procesio_cli/dashboard/server/handler.py +160 -0
- procesio_cli/dashboard/server/inventory.py +213 -0
- procesio_cli/dashboard/server/jobs.py +118 -0
- procesio_cli/dashboard/server/llmtest.py +181 -0
- procesio_cli/dashboard/server/procesio_env.py +94 -0
- procesio_cli/dashboard/server/routes.py +74 -0
- procesio_cli/dashboard/server/runner.py +150 -0
- procesio_cli/dashboard/server/security.py +29 -0
- procesio_cli/dashboard/server/setup.py +211 -0
- procesio_cli/dashboard/server/validate.py +152 -0
- procesio_cli/dashboard/tests/conftest.py +16 -0
- procesio_cli/dashboard/tests/test_dashboard.py +225 -0
- procesio_cli/dashboard/tests/test_procesio_env.py +159 -0
- procesio_cli/dashboard/web/actions.js +274 -0
- procesio_cli/dashboard/web/api.js +79 -0
- procesio_cli/dashboard/web/app.js +385 -0
- procesio_cli/dashboard/web/copilot.js +72 -0
- procesio_cli/dashboard/web/google.js +121 -0
- procesio_cli/dashboard/web/index.html +67 -0
- procesio_cli/dashboard/web/procesio_env.js +187 -0
- procesio_cli/dashboard/web/styles.css +207 -0
- procesio_cli/registry.py +370 -0
- procesio_cli/scripts/build-router.py +249 -0
- procesio_cli/scripts/build-tool-skill.py +202 -0
- procesio_cli/scripts/check-requirements.py +131 -0
- procesio_cli/scripts/check-skill-governance.py +174 -0
- procesio_cli/scripts/evaluate-skill-routing.py +217 -0
- procesio_cli/scripts/evaluate-skills.py +316 -0
- procesio_cli/scripts/get-skill.py +122 -0
- procesio_cli/scripts/list-skills.py +68 -0
- procesio_cli/scripts/list-tools.py +73 -0
- procesio_cli/scripts/run-agent.py +59 -0
- procesio_cli/scripts/run-tool.py +123 -0
- procesio_cli/scripts/secret_scan.py +584 -0
- procesio_cli/scripts/set-credential.py +126 -0
- procesio_cli/scripts/validate-skills.py +530 -0
- procesio_cli/skills/PORTFOLIO.md +134 -0
- procesio_cli/skills/__init__.py +6 -0
- procesio_cli/skills/agent-skill-engineer/SKILL.md +226 -0
- procesio_cli/skills/agent-skill-engineer/evals/evals.json +422 -0
- procesio_cli/skills/agent-skill-engineer/references/authoring-standard.md +178 -0
- procesio_cli/skills/agent-skill-engineer/references/decision-framework.md +107 -0
- procesio_cli/skills/agent-skill-engineer/references/domain-evidence-protocol.md +248 -0
- procesio_cli/skills/agent-skill-engineer/references/evaluation-standard.md +191 -0
- procesio_cli/skills/agent-skill-engineer/references/expert-lenses.md +167 -0
- procesio_cli/skills/agent-skill-engineer/references/field-gate-standard.md +231 -0
- procesio_cli/skills/agent-skill-engineer/references/optimization-protocol.md +233 -0
- procesio_cli/skills/agent-skill-engineer/references/optimizer-contract.md +268 -0
- procesio_cli/skills/agent-skill-engineer/references/portability-standard.md +99 -0
- procesio_cli/skills/agent-skill-engineer/references/research-basis.md +128 -0
- procesio_cli/skills/agent-skill-engineer/references/review-standard.md +143 -0
- procesio_cli/skills/agent-skill-engineer/scripts/audit_skill.py +959 -0
- procesio_cli/skills/agent-skill-engineer/scripts/optimize_skill.py +891 -0
- procesio_cli/skills/agent-skill-engineer/scripts/scaffold_skill.py +397 -0
- procesio_cli/skills/evals/baseline-catalog.json +14 -0
- procesio_cli/skills/evals/baseline-routing-v2.json +38 -0
- procesio_cli/skills/evals/baseline.json +37 -0
- procesio_cli/skills/evals/behavioral.json +367 -0
- procesio_cli/skills/evals/field-boundary-development-v1.json +126 -0
- procesio_cli/skills/evals/field-closeout-correction-development-v1.json +23 -0
- procesio_cli/skills/evals/field-closeout-development-v1.json +89 -0
- procesio_cli/skills/evals/portfolio-pressure-training-v1.json +83 -0
- procesio_cli/skills/evals/routing.json +363 -0
- procesio_cli/skills/evals/validation-baseline.json +4 -0
- procesio_cli/skills/procesio-cli/SKILL.md +134 -0
- procesio_cli/skills/procesio-cli/evals/evals.json +111 -0
- procesio_cli/skills/procesio-cli/evals/workflows.json +16 -0
- procesio_cli/skills/procesio-cli/references/connector-lifecycle.md +47 -0
- procesio_cli/skills/procesio-cli/references/credentials-admin.md +113 -0
- procesio_cli/skills/procesio-cli/references/data-verification.md +71 -0
- procesio_cli/skills/procesio-cli/references/documents-files.md +44 -0
- procesio_cli/skills/procesio-cli/references/form-e2e.md +57 -0
- procesio_cli/skills/procesio-cli/references/operation-contract.md +138 -0
- procesio_cli/skills/procesio-cli/references/process-debugging.md +65 -0
- procesio_cli/skills/procesio-cli/references/process-lifecycle.md +88 -0
- procesio_cli/skills/procesio-cli/references/schedules-webhooks.md +58 -0
- procesio_cli/skills/procesio-cli/references/transport-environments.md +53 -0
- procesio_cli/skills/procesio-cli-maintainer/SKILL.md +84 -0
- procesio_cli/skills/procesio-cli-maintainer/evals/evals.json +116 -0
- procesio_cli/skills/procesio-cli-maintainer/references/change-contract.md +34 -0
- procesio_cli/skills/procesio-cli-maintainer/references/mcp-compatibility.md +13 -0
- procesio_cli/skills/procesio-cli-maintainer/references/skill-authoring.md +51 -0
- procesio_cli/skills/procesio-platform-advisor/SKILL.md +82 -0
- procesio_cli/skills/procesio-platform-advisor/evals/evals.json +116 -0
- procesio_cli/skills/procesio-platform-advisor/references/capability-map.md +22 -0
- procesio_cli/skills/procesio-platform-advisor/references/fit-framework.md +35 -0
- procesio_cli/skills/procesio-platform-advisor/references/freshness-policy.md +25 -0
- procesio_cli/skills/procesio-platform-advisor/references/sizing-method.md +70 -0
- procesio_cli/skills/procesio-platform-advisor/scripts/estimate_capacity.py +99 -0
- procesio_cli/skills/procesio-setup/SKILL.md +180 -0
- procesio_cli/skills/sql-server-optimizer/SKILL.md +86 -0
- procesio_cli/skills/sql-server-optimizer/evals/evals.json +116 -0
- procesio_cli/skills/sql-server-optimizer/references/db-context.md +44 -0
- procesio_cli/skills/sql-server-optimizer/references/generic-mode.md +32 -0
- procesio_cli/skills/sql-server-optimizer/references/procesio-mode.md +40 -0
- procesio_cli/skills/sql-server-optimizer/references/sql-rules.md +43 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-indexes.sql +178 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-procs-and-functions.sql +118 -0
- procesio_cli/skills/sql-server-optimizer/scripts/export-tables.sql +367 -0
- procesio_cli/skills/tests/__init__.py +0 -0
- procesio_cli/skills/tests/test_agent_skill_engineer.py +240 -0
- procesio_cli/skills/tests/test_auth_failure_stop.py +25 -0
- procesio_cli/skills/tests/test_capacity_estimator.py +85 -0
- procesio_cli/skills/tests/test_field_boundary_learning.py +100 -0
- procesio_cli/skills/tests/test_field_closeout_learning.py +123 -0
- procesio_cli/skills/tests/test_field_gate_standard.py +76 -0
- procesio_cli/skills/tests/test_operational_playbooks.py +51 -0
- procesio_cli/skills/tests/test_portfolio_pressure_training.py +35 -0
- procesio_cli/skills/tests/test_process_lifecycle_single_run.py +25 -0
- procesio_cli/skills/tests/test_skill_evaluator.py +67 -0
- procesio_cli/skills/tests/test_skill_optimizer.py +473 -0
- procesio_cli/skills/tests/test_skill_portfolio_review.py +70 -0
- procesio_cli/skills/tests/test_skill_release_binding.py +204 -0
- procesio_cli/skills/tests/test_skill_routing.py +74 -0
- procesio_cli/skills/tests/test_skill_scaffold_v2.py +116 -0
- procesio_cli/skills/tests/test_validate_skills.py +115 -0
- procesio_cli/tools/__init__.py +0 -0
- procesio_cli/tools/_lib/README.md +119 -0
- procesio_cli/tools/_lib/__init__.py +0 -0
- procesio_cli/tools/_lib/accounting.py +120 -0
- procesio_cli/tools/_lib/contacts.py +544 -0
- procesio_cli/tools/_lib/creds.py +83 -0
- procesio_cli/tools/_lib/creds_backends.py +578 -0
- procesio_cli/tools/_lib/dbprofiles.py +147 -0
- procesio_cli/tools/_lib/dbquery.py +286 -0
- procesio_cli/tools/_lib/dbschema.py +225 -0
- procesio_cli/tools/_lib/io.py +54 -0
- procesio_cli/tools/_lib/manifest.py +297 -0
- procesio_cli/tools/_lib/skill_release.py +127 -0
- procesio_cli/tools/_lib/skill_resources.py +118 -0
- procesio_cli/tools/_lib/userdata.py +228 -0
- procesio_cli/tools/_lib/webserialize/__init__.py +16 -0
- procesio_cli/tools/_lib/webserialize/admin.py +61 -0
- procesio_cli/tools/_lib/webserialize/client.py +264 -0
- procesio_cli/tools/_lib/webserialize/config.py +96 -0
- procesio_cli/tools/_lib/webserialize/errors.py +27 -0
- procesio_cli/tools/_lib/webserialize/framing.py +45 -0
- procesio_cli/tools/_lib/webserialize/registry.py +47 -0
- procesio_cli/tools/_lib/webserialize/runtime.py +61 -0
- procesio_cli/tools/_lib/webserialize/server.py +510 -0
- procesio_cli/tools/_lib/websession.py +194 -0
- procesio_cli/tools/connector-builder/CONNECTOR-BUILDER-API-NOTES.md +91 -0
- procesio_cli/tools/connector-builder/README.md +101 -0
- procesio_cli/tools/connector-builder/SKILL.md +248 -0
- procesio_cli/tools/connector-builder/__init__.py +7 -0
- procesio_cli/tools/connector-builder/actiondef.py +24 -0
- procesio_cli/tools/connector-builder/client.py +251 -0
- procesio_cli/tools/connector-builder/errors.py +44 -0
- procesio_cli/tools/connector-builder/gen_manifest.py +142 -0
- procesio_cli/tools/connector-builder/handlers/__init__.py +1 -0
- procesio_cli/tools/connector-builder/handlers/admin.py +182 -0
- procesio_cli/tools/connector-builder/handlers/auth.py +74 -0
- procesio_cli/tools/connector-builder/handlers/builds.py +396 -0
- procesio_cli/tools/connector-builder/handlers/common.py +104 -0
- procesio_cli/tools/connector-builder/handlers/files.py +111 -0
- procesio_cli/tools/connector-builder/handlers/raw.py +38 -0
- procesio_cli/tools/connector-builder/handlers/telemetry.py +39 -0
- procesio_cli/tools/connector-builder/main.py +94 -0
- procesio_cli/tools/connector-builder/tests/conftest.py +146 -0
- procesio_cli/tools/connector-builder/tests/test_cb_client.py +86 -0
- procesio_cli/tools/connector-builder/tests/test_cb_dispatch.py +105 -0
- procesio_cli/tools/connector-builder/tests/test_cb_manifest_sync.py +76 -0
- procesio_cli/tools/connector-builder/tests/test_cb_wait.py +123 -0
- procesio_cli/tools/connector-builder/tool.yaml +671 -0
- procesio_cli/tools/framework-map/.gitattributes +2 -0
- procesio_cli/tools/framework-map/README.md +98 -0
- procesio_cli/tools/framework-map/SKILL.md +30 -0
- procesio_cli/tools/framework-map/app.js +188 -0
- procesio_cli/tools/framework-map/fm_builder.py +476 -0
- procesio_cli/tools/framework-map/fm_catalog.py +19 -0
- procesio_cli/tools/framework-map/fm_ro_catalog.py +94 -0
- procesio_cli/tools/framework-map/fm_strings.py +101 -0
- procesio_cli/tools/framework-map/main.py +95 -0
- procesio_cli/tools/framework-map/styles.css +214 -0
- procesio_cli/tools/framework-map/tests/conftest.py +33 -0
- procesio_cli/tools/framework-map/tests/test_fm_build.py +126 -0
- procesio_cli/tools/framework-map/tests/test_fm_manifest_sync.py +63 -0
- procesio_cli/tools/framework-map/tests/test_fm_store_tree.py +75 -0
- procesio_cli/tools/framework-map/tool.yaml +61 -0
- procesio_cli/tools/mysql/MOVED-TO-CENTRAL.md +29 -0
- procesio_cli/tools/mysql/README.md +196 -0
- procesio_cli/tools/mysql/SKILL.md +37 -0
- procesio_cli/tools/mysql/__init__.py +0 -0
- procesio_cli/tools/mysql/connection.py +191 -0
- procesio_cli/tools/mysql/main.py +487 -0
- procesio_cli/tools/mysql/profiles.template.json +1 -0
- procesio_cli/tools/mysql/schema.py +434 -0
- procesio_cli/tools/mysql/tests/__init__.py +0 -0
- procesio_cli/tools/mysql/tests/conftest.py +21 -0
- procesio_cli/tools/mysql/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/mysql/tests/test_mysql.py +201 -0
- procesio_cli/tools/mysql/tests/test_mysql_schema.py +337 -0
- procesio_cli/tools/mysql/tool.yaml +112 -0
- procesio_cli/tools/procesio/DTO-SUBTOOLS-NOTE.md +360 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/00-INDEX.md +67 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/01-ANATOMY.md +209 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/02-CODE-INJECTION.md +309 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/03-DOM-CONTRACT.md +370 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/04-INTERACTION-RECIPES.md +600 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/05-STEPPER-AND-MOTION.md +426 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/06-PROCESS-INTEGRATION.md +347 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/07-DEPLOY-WORKFLOW.md +294 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/08-PITFALLS.md +569 -0
- procesio_cli/tools/procesio/FORM-DEV-GUIDE/09-NAVIGATION-AND-HOSTING.md +308 -0
- procesio_cli/tools/procesio/PHASE4-E2E-NOTES.md +594 -0
- procesio_cli/tools/procesio/PROCESIO-API-CORRECTIONS.md +295 -0
- procesio_cli/tools/procesio/PROCESIO-API-NOTES.md +6976 -0
- procesio_cli/tools/procesio/PROCESIO-AUTH-NOTES.md +226 -0
- procesio_cli/tools/procesio/PROCESIO-CARD-BUILD-NOTES.md +183 -0
- procesio_cli/tools/procesio/PROCESIO-CONNECTORS-NOTES.md +141 -0
- procesio_cli/tools/procesio/PROCESIO-CUSTOM-ACTION-NOTES.md +179 -0
- procesio_cli/tools/procesio/PROCESIO-DOCS-FIX-REPORT.md +123 -0
- procesio_cli/tools/procesio/PROCESIO-ENVIRONMENTS-NOTES.md +94 -0
- procesio_cli/tools/procesio/PROCESIO-FE-VALIDATION-NOTES.md +107 -0
- procesio_cli/tools/procesio/PROCESIO-FOREACH-NOTES.md +199 -0
- procesio_cli/tools/procesio/PROCESIO-FORM-API-HANG-NOTE.md +101 -0
- procesio_cli/tools/procesio/PROCESIO-FORM-SUBMISSION-NOTES.md +172 -0
- procesio_cli/tools/procesio/PROCESIO-HTML-TO-DOCX-NOTES.md +110 -0
- procesio_cli/tools/procesio/PROCESIO-INSTANCE-HISTORY-NOTES.md +242 -0
- procesio_cli/tools/procesio/PROCESIO-METERING-NOTES.md +201 -0
- procesio_cli/tools/procesio/PROCESIO-NODE-CODE-NOTES.md +279 -0
- procesio_cli/tools/procesio/PROCESIO-NODE-MODULE-WHITELIST.md +113 -0
- procesio_cli/tools/procesio/PROCESIO-PYTHON-ACTION-NOTES.md +165 -0
- procesio_cli/tools/procesio/PROCESIO-QUERY-STORE-NOTES.md +826 -0
- procesio_cli/tools/procesio/PROCESIO-RECONCILIATION-PATTERNS.md +104 -0
- procesio_cli/tools/procesio/PROCESIO-RESOURCE-MODEL-NOTES.md +1100 -0
- procesio_cli/tools/procesio/PROCESIO-SEND-EMAIL-CRED-OPTIONALITY.md +45 -0
- procesio_cli/tools/procesio/PROCESIO-SEND-EMAIL-NOTES.md +252 -0
- procesio_cli/tools/procesio/PROCESIO-SQL-ACTIONS-NOTES.md +284 -0
- procesio_cli/tools/procesio/PROCESIO-USAGE-GUIDE.md +219 -0
- procesio_cli/tools/procesio/README.md +341 -0
- procesio_cli/tools/procesio/SCHEDULE-INPUT-SECURITY-NOTES.md +63 -0
- procesio_cli/tools/procesio/SKILL.md +694 -0
- procesio_cli/tools/procesio/__init__.py +1 -0
- procesio_cli/tools/procesio/actiondef.py +27 -0
- procesio_cli/tools/procesio/assets/html2docx/html2docx.js +2155 -0
- procesio_cli/tools/procesio/auth.py +354 -0
- procesio_cli/tools/procesio/autogen.py +121 -0
- procesio_cli/tools/procesio/client.py +355 -0
- procesio_cli/tools/procesio/config.py +96 -0
- procesio_cli/tools/procesio/data/endpoints.json +2901 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/01-authentication.md +180 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/02-conventions.md +116 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/ANONYMOUS-SURFACE.md +51 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/README.md +104 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/SYMBOL-INDEX.md +444 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/03-auth-users-workspaces.md +731 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/04-processes.md +913 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/05-actions.md +605 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/06-files.md +237 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/07-forms-documents.md +985 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/08-credentials.md +532 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/09-datastore-datatypes.md +830 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/10-custom-urls.md +319 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/11-subscriptions-resources-analytics.md +455 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/12-webhooks-notifications-misc.md +519 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/13-realtime-signalr.md +153 -0
- procesio_cli/tools/procesio/docs_info/API-DOCUMENTATION/endpoints/99-internal-and-other-services.md +52 -0
- procesio_cli/tools/procesio/docs_info/FormBuilder mock.ts +5725 -0
- procesio_cli/tools/procesio/docs_info/Procesio Platform Actions.json +30219 -0
- procesio_cli/tools/procesio/docs_info/form-config-enums.ts +92 -0
- procesio_cli/tools/procesio/docs_info/form-theme.ts +1603 -0
- procesio_cli/tools/procesio/docs_info/process-validation-reference-1.md +2739 -0
- procesio_cli/tools/procesio/dto/__init__.py +13 -0
- procesio_cli/tools/procesio/dto/credential/builder.py +179 -0
- procesio_cli/tools/procesio/dto/credential/config.schema.json +19 -0
- procesio_cli/tools/procesio/dto/credential/description.md +193 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.config.json +11 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.dto.json +28 -0
- procesio_cli/tools/procesio/dto/credential/fixtures/rest.template.json +44 -0
- procesio_cli/tools/procesio/dto/credential/template.dto.json +16 -0
- procesio_cli/tools/procesio/dto/data/_refresh.py +147 -0
- procesio_cli/tools/procesio/dto/data/action_catalog.json +30825 -0
- procesio_cli/tools/procesio/dto/data/action_families.json +55 -0
- procesio_cli/tools/procesio/dto/data/platform_types.json +182 -0
- procesio_cli/tools/procesio/dto/datatype/builder.py +223 -0
- procesio_cli/tools/procesio/dto/datatype/config.schema.json +47 -0
- procesio_cli/tools/procesio/dto/datatype/description.md +85 -0
- procesio_cli/tools/procesio/dto/datatype/fixtures/customer.config.json +41 -0
- procesio_cli/tools/procesio/dto/datatype/fixtures/customer.dto.json +164 -0
- procesio_cli/tools/procesio/dto/datatype/template.dto.json +37 -0
- procesio_cli/tools/procesio/dto/document/builder.py +193 -0
- procesio_cli/tools/procesio/dto/document/config.schema.json +33 -0
- procesio_cli/tools/procesio/dto/document/description.md +124 -0
- procesio_cli/tools/procesio/dto/document/fixtures/invoice.config.json +16 -0
- procesio_cli/tools/procesio/dto/document/fixtures/invoice.dto.json +32 -0
- procesio_cli/tools/procesio/dto/document/template.dto.json +14 -0
- procesio_cli/tools/procesio/dto/form/FORM-STYLING-NOTES.md +142 -0
- procesio_cli/tools/procesio/dto/form/GOLDEN-DRIFT-NOTES.md +72 -0
- procesio_cli/tools/procesio/dto/form/RUN-PROCESS-RESULT-NOTES.md +80 -0
- procesio_cli/tools/procesio/dto/form/addelement.py +277 -0
- procesio_cli/tools/procesio/dto/form/builder.py +1009 -0
- procesio_cli/tools/procesio/dto/form/capture_goldens.py +106 -0
- procesio_cli/tools/procesio/dto/form/code_cipher.py +67 -0
- procesio_cli/tools/procesio/dto/form/config.schema.json +76 -0
- procesio_cli/tools/procesio/dto/form/data_shell.json +1 -0
- procesio_cli/tools/procesio/dto/form/description.md +346 -0
- procesio_cli/tools/procesio/dto/form/elements/approval.json +259 -0
- procesio_cli/tools/procesio/dto/form/elements/assignee.json +137 -0
- procesio_cli/tools/procesio/dto/form/elements/button.json +125 -0
- procesio_cli/tools/procesio/dto/form/elements/chart.json +341 -0
- procesio_cli/tools/procesio/dto/form/elements/chat.json +199 -0
- procesio_cli/tools/procesio/dto/form/elements/checkbox.json +153 -0
- procesio_cli/tools/procesio/dto/form/elements/column.json +77 -0
- procesio_cli/tools/procesio/dto/form/elements/columns.json +78 -0
- procesio_cli/tools/procesio/dto/form/elements/datetime-input.json +304 -0
- procesio_cli/tools/procesio/dto/form/elements/divider.json +54 -0
- procesio_cli/tools/procesio/dto/form/elements/dropdown.json +130 -0
- procesio_cli/tools/procesio/dto/form/elements/dynamic-table-row.json +253 -0
- procesio_cli/tools/procesio/dto/form/elements/file-upload.json +196 -0
- procesio_cli/tools/procesio/dto/form/elements/file-viewer.json +169 -0
- procesio_cli/tools/procesio/dto/form/elements/heading.json +79 -0
- procesio_cli/tools/procesio/dto/form/elements/icon.json +107 -0
- procesio_cli/tools/procesio/dto/form/elements/image.json +174 -0
- procesio_cli/tools/procesio/dto/form/elements/input.json +206 -0
- procesio_cli/tools/procesio/dto/form/elements/list.json +202 -0
- procesio_cli/tools/procesio/dto/form/elements/number-input.json +202 -0
- procesio_cli/tools/procesio/dto/form/elements/paragraph.json +79 -0
- procesio_cli/tools/procesio/dto/form/elements/radiobox.json +174 -0
- procesio_cli/tools/procesio/dto/form/elements/section.json +114 -0
- procesio_cli/tools/procesio/dto/form/elements/select.json +217 -0
- procesio_cli/tools/procesio/dto/form/elements/side-panel.json +146 -0
- procesio_cli/tools/procesio/dto/form/elements/signature-pad.json +141 -0
- procesio_cli/tools/procesio/dto/form/elements/static-table-row.json +84 -0
- procesio_cli/tools/procesio/dto/form/elements/step.json +105 -0
- procesio_cli/tools/procesio/dto/form/elements/stepper.json +126 -0
- procesio_cli/tools/procesio/dto/form/elements/tab.json +77 -0
- procesio_cli/tools/procesio/dto/form/elements/table.json +279 -0
- procesio_cli/tools/procesio/dto/form/elements/tabs.json +114 -0
- procesio_cli/tools/procesio/dto/form/elements/textarea.json +213 -0
- procesio_cli/tools/procesio/dto/form/enum_map.json +68 -0
- procesio_cli/tools/procesio/dto/form/fieldpath.py +284 -0
- procesio_cli/tools/procesio/dto/form/fixtures/reg.config.json +32 -0
- procesio_cli/tools/procesio/dto/form/fixtures/reg.elements.json +638 -0
- procesio_cli/tools/procesio/dto/form/sync_from_mock.py +386 -0
- procesio_cli/tools/procesio/dto/framework.py +125 -0
- procesio_cli/tools/procesio/dto/process/builder.py +1991 -0
- procesio_cli/tools/procesio/dto/process/config.schema.json +159 -0
- procesio_cli/tools/procesio/dto/process/description.md +218 -0
- procesio_cli/tools/procesio/dto/process/fixtures/greet.config.json +32 -0
- procesio_cli/tools/procesio/dto/process/fixtures/greet.dto.json +317 -0
- procesio_cli/tools/procesio/dto/process/naming.py +305 -0
- procesio_cli/tools/procesio/dto/process/normalize.py +74 -0
- procesio_cli/tools/procesio/dto/process/template.dto.json +32 -0
- procesio_cli/tools/procesio/dto/refdata.py +87 -0
- procesio_cli/tools/procesio/dto/registry.py +27 -0
- procesio_cli/tools/procesio/dto/webhook/builder.py +142 -0
- procesio_cli/tools/procesio/dto/webhook/config.schema.json +25 -0
- procesio_cli/tools/procesio/dto/webhook/description.md +92 -0
- procesio_cli/tools/procesio/dto/webhook/fixtures/order.config.json +10 -0
- procesio_cli/tools/procesio/dto/webhook/fixtures/order.dto.json +25 -0
- procesio_cli/tools/procesio/dto/webhook/template.dto.json +14 -0
- procesio_cli/tools/procesio/environments.py +264 -0
- procesio_cli/tools/procesio/errors.py +166 -0
- procesio_cli/tools/procesio/flowmodel/README.md +37 -0
- procesio_cli/tools/procesio/flowmodel/__init__.py +14 -0
- procesio_cli/tools/procesio/flowmodel/digest.py +223 -0
- procesio_cli/tools/procesio/flowmodel/families.py +34 -0
- procesio_cli/tools/procesio/flowmodel/fevalidation.py +1098 -0
- procesio_cli/tools/procesio/flowmodel/formdigest.py +232 -0
- procesio_cli/tools/procesio/flowmodel/inspect.py +78 -0
- procesio_cli/tools/procesio/flowmodel/model.py +82 -0
- procesio_cli/tools/procesio/flowmodel/nodeparam.py +871 -0
- procesio_cli/tools/procesio/flowmodel/reader.py +191 -0
- procesio_cli/tools/procesio/flowmodel/sqlparam.py +410 -0
- procesio_cli/tools/procesio/form_code_keys.py +50 -0
- procesio_cli/tools/procesio/formlint.py +292 -0
- procesio_cli/tools/procesio/gen_manifest.py +118 -0
- procesio_cli/tools/procesio/handlers/__init__.py +1 -0
- procesio_cli/tools/procesio/handlers/auth_actions.py +103 -0
- procesio_cli/tools/procesio/handlers/catalog.py +147 -0
- procesio_cli/tools/procesio/handlers/common.py +90 -0
- procesio_cli/tools/procesio/handlers/customaction_ops.py +146 -0
- procesio_cli/tools/procesio/handlers/datastore_ops.py +482 -0
- procesio_cli/tools/procesio/handlers/datatype_ops.py +275 -0
- procesio_cli/tools/procesio/handlers/dto_actions.py +258 -0
- procesio_cli/tools/procesio/handlers/environment_admin.py +146 -0
- procesio_cli/tools/procesio/handlers/fevalidate.py +236 -0
- procesio_cli/tools/procesio/handlers/files.py +141 -0
- procesio_cli/tools/procesio/handlers/flowgraph.py +229 -0
- procesio_cli/tools/procesio/handlers/flowlint.py +269 -0
- procesio_cli/tools/procesio/handlers/form_chains.py +336 -0
- procesio_cli/tools/procesio/handlers/form_code.py +277 -0
- procesio_cli/tools/procesio/handlers/form_element.py +199 -0
- procesio_cli/tools/procesio/handlers/form_events.py +441 -0
- procesio_cli/tools/procesio/handlers/form_update.py +270 -0
- procesio_cli/tools/procesio/handlers/formprocess.py +211 -0
- procesio_cli/tools/procesio/handlers/generic.py +135 -0
- procesio_cli/tools/procesio/handlers/layout.py +137 -0
- procesio_cli/tools/procesio/handlers/nodeparams.py +1018 -0
- procesio_cli/tools/procesio/handlers/process_layout.py +189 -0
- procesio_cli/tools/procesio/handlers/process_naming.py +213 -0
- procesio_cli/tools/procesio/handlers/processes.py +416 -0
- procesio_cli/tools/procesio/handlers/profile_admin.py +184 -0
- procesio_cli/tools/procesio/handlers/resource_ops.py +447 -0
- procesio_cli/tools/procesio/handlers/schedules.py +320 -0
- procesio_cli/tools/procesio/handlers/sqlactions.py +232 -0
- procesio_cli/tools/procesio/handlers/transport.py +463 -0
- procesio_cli/tools/procesio/handlers/usageguide.py +222 -0
- procesio_cli/tools/procesio/layout/README.md +196 -0
- procesio_cli/tools/procesio/layout/__init__.py +11 -0
- procesio_cli/tools/procesio/layout/adapter.py +736 -0
- procesio_cli/tools/procesio/layout/cycles.py +186 -0
- procesio_cli/tools/procesio/layout/dispatch.py +39 -0
- procesio_cli/tools/procesio/layout/elk/elk_runner.mjs +50 -0
- procesio_cli/tools/procesio/layout/elk/package-lock.json +21 -0
- procesio_cli/tools/procesio/layout/elk/package.json +11 -0
- procesio_cli/tools/procesio/layout/elk_engine.py +568 -0
- procesio_cli/tools/procesio/layout/engine.py +1311 -0
- procesio_cli/tools/procesio/layout/report.py +187 -0
- procesio_cli/tools/procesio/main.py +179 -0
- procesio_cli/tools/procesio/profiles.py +193 -0
- procesio_cli/tools/procesio/reliability.py +193 -0
- procesio_cli/tools/procesio/sqlformat.py +167 -0
- procesio_cli/tools/procesio/swagger.py +51 -0
- procesio_cli/tools/procesio/tests/__init__.py +0 -0
- procesio_cli/tools/procesio/tests/conftest.py +129 -0
- procesio_cli/tools/procesio/tests/test_auth.py +200 -0
- procesio_cli/tools/procesio/tests/test_autogen.py +78 -0
- procesio_cli/tools/procesio/tests/test_builder_attrpath_sqlbind.py +180 -0
- procesio_cli/tools/procesio/tests/test_builder_custom_action_catalog.py +72 -0
- procesio_cli/tools/procesio/tests/test_client.py +114 -0
- procesio_cli/tools/procesio/tests/test_client_reliability.py +139 -0
- procesio_cli/tools/procesio/tests/test_customaction.py +103 -0
- procesio_cli/tools/procesio/tests/test_customaction_test.py +80 -0
- procesio_cli/tools/procesio/tests/test_cycles.py +69 -0
- procesio_cli/tools/procesio/tests/test_datastore.py +378 -0
- procesio_cli/tools/procesio/tests/test_datastore_node.py +94 -0
- procesio_cli/tools/procesio/tests/test_dispatch.py +170 -0
- procesio_cli/tools/procesio/tests/test_document_page_size.py +19 -0
- procesio_cli/tools/procesio/tests/test_dto.py +734 -0
- procesio_cli/tools/procesio/tests/test_edit_dryrun_preview.py +96 -0
- procesio_cli/tools/procesio/tests/test_elk_layout.py +221 -0
- procesio_cli/tools/procesio/tests/test_environments.py +214 -0
- procesio_cli/tools/procesio/tests/test_fevalidate_gate.py +104 -0
- procesio_cli/tools/procesio/tests/test_fevalidation.py +317 -0
- procesio_cli/tools/procesio/tests/test_fieldpath_variables.py +87 -0
- procesio_cli/tools/procesio/tests/test_files.py +72 -0
- procesio_cli/tools/procesio/tests/test_flow_digest.py +202 -0
- procesio_cli/tools/procesio/tests/test_flowlint.py +114 -0
- procesio_cli/tools/procesio/tests/test_flowmodel.py +158 -0
- procesio_cli/tools/procesio/tests/test_form_add_element.py +256 -0
- procesio_cli/tools/procesio/tests/test_form_approval_gaps.py +319 -0
- procesio_cli/tools/procesio/tests/test_form_auto_names.py +60 -0
- procesio_cli/tools/procesio/tests/test_form_chains.py +217 -0
- procesio_cli/tools/procesio/tests/test_form_code_actions.py +188 -0
- procesio_cli/tools/procesio/tests/test_form_code_keys.py +92 -0
- procesio_cli/tools/procesio/tests/test_form_code_style.py +178 -0
- procesio_cli/tools/procesio/tests/test_form_delete_element.py +99 -0
- procesio_cli/tools/procesio/tests/test_form_element_config.py +141 -0
- procesio_cli/tools/procesio/tests/test_form_event_maps.py +135 -0
- procesio_cli/tools/procesio/tests/test_form_event_ops.py +376 -0
- procesio_cli/tools/procesio/tests/test_form_event_replace.py +86 -0
- procesio_cli/tools/procesio/tests/test_form_events.py +92 -0
- procesio_cli/tools/procesio/tests/test_form_parity.py +171 -0
- procesio_cli/tools/procesio/tests/test_form_select_source.py +74 -0
- procesio_cli/tools/procesio/tests/test_form_sync.py +35 -0
- procesio_cli/tools/procesio/tests/test_form_update.py +133 -0
- procesio_cli/tools/procesio/tests/test_formlint.py +155 -0
- procesio_cli/tools/procesio/tests/test_formlint_css_scope.py +38 -0
- procesio_cli/tools/procesio/tests/test_formlint_field_names.py +83 -0
- procesio_cli/tools/procesio/tests/test_guards.py +110 -0
- procesio_cli/tools/procesio/tests/test_hidden_setting_types.py +39 -0
- procesio_cli/tools/procesio/tests/test_html2docx_asset.py +132 -0
- procesio_cli/tools/procesio/tests/test_key_handle_terminology.py +45 -0
- procesio_cli/tools/procesio/tests/test_layout.py +574 -0
- procesio_cli/tools/procesio/tests/test_layout_report.py +88 -0
- procesio_cli/tools/procesio/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/procesio/tests/test_nodedelete.py +151 -0
- procesio_cli/tools/procesio/tests/test_nodeparam.py +826 -0
- procesio_cli/tools/procesio/tests/test_normalize.py +70 -0
- procesio_cli/tools/procesio/tests/test_payload_docs.py +86 -0
- procesio_cli/tools/procesio/tests/test_process_action_naming.py +170 -0
- procesio_cli/tools/procesio/tests/test_process_call_api_seed.py +46 -0
- procesio_cli/tools/procesio/tests/test_process_ext.py +373 -0
- procesio_cli/tools/procesio/tests/test_process_invariants.py +183 -0
- procesio_cli/tools/procesio/tests/test_process_layout.py +117 -0
- procesio_cli/tools/procesio/tests/test_process_naming.py +188 -0
- procesio_cli/tools/procesio/tests/test_process_query_store.py +172 -0
- procesio_cli/tools/procesio/tests/test_processinfo_variable.py +56 -0
- procesio_cli/tools/procesio/tests/test_profiles.py +127 -0
- procesio_cli/tools/procesio/tests/test_put_projects.py +41 -0
- procesio_cli/tools/procesio/tests/test_reliability.py +107 -0
- procesio_cli/tools/procesio/tests/test_removal_gate.py +95 -0
- procesio_cli/tools/procesio/tests/test_resource_ops.py +173 -0
- procesio_cli/tools/procesio/tests/test_save_validity.py +207 -0
- procesio_cli/tools/procesio/tests/test_schedule_cron_shape.py +111 -0
- procesio_cli/tools/procesio/tests/test_schedules.py +309 -0
- procesio_cli/tools/procesio/tests/test_sqlconvert.py +234 -0
- procesio_cli/tools/procesio/tests/test_sqlformat.py +83 -0
- procesio_cli/tools/procesio/tests/test_sqlparam.py +187 -0
- procesio_cli/tools/procesio/tests/test_superseded_actions.py +54 -0
- procesio_cli/tools/procesio/tests/test_surgical_concurrency.py +156 -0
- procesio_cli/tools/procesio/tests/test_tier4.py +123 -0
- procesio_cli/tools/procesio/tests/test_transport.py +147 -0
- procesio_cli/tools/procesio/tests/test_transport_data_stores.py +168 -0
- procesio_cli/tools/procesio/tests/test_transport_mapper_repair.py +150 -0
- procesio_cli/tools/procesio/tests/test_transport_webhook_strip.py +253 -0
- procesio_cli/tools/procesio/tests/test_usage_guide.py +96 -0
- procesio_cli/tools/procesio/tool.yaml +12486 -0
- procesio_cli/tools/sqlserver/MOVED-TO-CENTRAL.md +29 -0
- procesio_cli/tools/sqlserver/README.md +192 -0
- procesio_cli/tools/sqlserver/SKILL.md +37 -0
- procesio_cli/tools/sqlserver/SQLSERVER-SCHEMA-NOTES.md +284 -0
- procesio_cli/tools/sqlserver/__init__.py +0 -0
- procesio_cli/tools/sqlserver/connection.py +242 -0
- procesio_cli/tools/sqlserver/main.py +521 -0
- procesio_cli/tools/sqlserver/profiles.template.json +1 -0
- procesio_cli/tools/sqlserver/schema.py +600 -0
- procesio_cli/tools/sqlserver/tests/__init__.py +0 -0
- procesio_cli/tools/sqlserver/tests/conftest.py +21 -0
- procesio_cli/tools/sqlserver/tests/test_connection.py +75 -0
- procesio_cli/tools/sqlserver/tests/test_dbprofiles.py +92 -0
- procesio_cli/tools/sqlserver/tests/test_dbquery.py +220 -0
- procesio_cli/tools/sqlserver/tests/test_manifest_sync.py +68 -0
- procesio_cli/tools/sqlserver/tests/test_sqlserver.py +326 -0
- procesio_cli/tools/sqlserver/tests/test_sqlserver_schema.py +488 -0
- procesio_cli/tools/sqlserver/tool.yaml +123 -0
- procesio_cli/tools/web/README.md +381 -0
- procesio_cli/tools/web/SKILL.md +37 -0
- procesio_cli/tools/web/WEB-AUTOMATION-NOTES.md +277 -0
- procesio_cli/tools/web/__init__.py +21 -0
- procesio_cli/tools/web/actiondef.py +24 -0
- procesio_cli/tools/web/api.py +444 -0
- procesio_cli/tools/web/brokered.py +107 -0
- procesio_cli/tools/web/driver.py +839 -0
- procesio_cli/tools/web/drivercontract.py +44 -0
- procesio_cli/tools/web/errors.py +54 -0
- procesio_cli/tools/web/handlers/__init__.py +1 -0
- procesio_cli/tools/web/handlers/archive.py +367 -0
- procesio_cli/tools/web/handlers/browse.py +182 -0
- procesio_cli/tools/web/handlers/contacts.py +74 -0
- procesio_cli/tools/web/handlers/sessmgmt.py +120 -0
- procesio_cli/tools/web/main.py +128 -0
- procesio_cli/tools/web/sessions.py +253 -0
- procesio_cli/tools/web/steps.py +162 -0
- procesio_cli/tools/web/tests/__init__.py +0 -0
- procesio_cli/tools/web/tests/conftest.py +197 -0
- procesio_cli/tools/web/tests/test_api.py +330 -0
- procesio_cli/tools/web/tests/test_archive.py +190 -0
- procesio_cli/tools/web/tests/test_brokered.py +138 -0
- procesio_cli/tools/web/tests/test_dispatch.py +132 -0
- procesio_cli/tools/web/tests/test_errors.py +60 -0
- procesio_cli/tools/web/tests/test_manifest_sync.py +76 -0
- procesio_cli/tools/web/tests/test_page_contacts.py +68 -0
- procesio_cli/tools/web/tests/test_sessions.py +145 -0
- procesio_cli/tools/web/tests/test_steps.py +210 -0
- procesio_cli/tools/web/tool.yaml +184 -0
- procesio_cli/tools/xlsx/README.md +34 -0
- procesio_cli/tools/xlsx/SKILL.md +28 -0
- procesio_cli/tools/xlsx/__init__.py +0 -0
- procesio_cli/tools/xlsx/main.py +129 -0
- procesio_cli/tools/xlsx/reader.py +91 -0
- procesio_cli/tools/xlsx/tests/__init__.py +0 -0
- procesio_cli/tools/xlsx/tests/conftest.py +39 -0
- procesio_cli/tools/xlsx/tests/test_xlsx.py +103 -0
- procesio_cli/tools/xlsx/tool.yaml +30 -0
- procesio_cli/webplatform/aat_mcp/AAT-MCP-NOTES.md +244 -0
- procesio_cli/webplatform/aat_mcp/README.md +78 -0
- procesio_cli/webplatform/aat_mcp/bridge.py +705 -0
- procesio_cli/webplatform/aat_mcp/chat_server.py +194 -0
- procesio_cli/webplatform/aat_mcp/chat_surface.snapshot.json +668 -0
- procesio_cli/webplatform/aat_mcp/chat_surface.yaml +372 -0
- procesio_cli/webplatform/aat_mcp/gate.py +68 -0
- procesio_cli/webplatform/aat_mcp/http_server.py +139 -0
- procesio_cli/webplatform/aat_mcp/protocol.py +175 -0
- procesio_cli/webplatform/aat_mcp/server.py +306 -0
- procesio_cli/webplatform/aat_mcp/tests/conftest.py +8 -0
- procesio_cli/webplatform/aat_mcp/tests/test_aat_mcp.py +255 -0
- procesio_cli/webplatform/aat_mcp/tests/test_chat_surface.py +247 -0
- procesio_cli/webplatform/aat_mcp/tests/test_discovery.py +489 -0
- procesio_cli/webplatform/aat_mcp/tests/test_multiuser.py +86 -0
- procesio_cli/webplatform/aat_mcp/tests/test_progressive_discovery.py +89 -0
- procesio_cli/webplatform/aat_mcp/tests/test_protocol.py +180 -0
- procesio_cli/webplatform/aat_mcp/tests/test_skill_discovery.py +23 -0
- procesio_cli-0.1.1.dist-info/METADATA +467 -0
- procesio_cli-0.1.1.dist-info/RECORD +647 -0
- procesio_cli-0.1.1.dist-info/WHEEL +4 -0
- procesio_cli-0.1.1.dist-info/entry_points.txt +7 -0
- procesio_cli-0.1.1.dist-info/licenses/LICENSE +201 -0
- 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
|