abstra 3.24.3__py3-none-any.whl → 3.24.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. abstra/ai.py +2 -0
  2. {abstra-3.24.3.dist-info → abstra-3.24.5.dist-info}/METADATA +2 -1
  3. {abstra-3.24.3.dist-info → abstra-3.24.5.dist-info}/RECORD +195 -192
  4. abstra_internals/contracts_generated.py +3737 -2560
  5. abstra_internals/controllers/workflows.py +42 -0
  6. abstra_internals/interface/sdk/ai.py +69 -0
  7. abstra_internals/repositories/execution.py +3 -6
  8. abstra_internals/repositories/git/native.py +90 -3
  9. abstra_internals/repositories/git/types.py +10 -0
  10. abstra_internals/repositories/linter/rules/env_in_bundle.py +2 -0
  11. abstra_internals/repositories/project/json_migrations/__init__.py +2 -0
  12. abstra_internals/repositories/project/json_migrations/migration_016.py +17 -0
  13. abstra_internals/repositories/project/json_migrations/migration_016_test.py +141 -0
  14. abstra_internals/repositories/project/project.py +62 -17
  15. abstra_internals/repositories/project/project_test.py +279 -0
  16. abstra_internals/repositories/tasks.py +2 -2
  17. abstra_internals/services/fs.py +311 -32
  18. abstra_internals/services/fs_test.py +28 -5
  19. abstra_internals/services/sql_storage.py +236 -0
  20. abstra_internals/services/sql_storage_test.py +112 -0
  21. abstra_internals/utils/file.py +7 -3
  22. abstra_internals/utils/fs_cache.py +173 -0
  23. abstra_statics/dist/assets/{AbstraButton.vue_vue_type_script_setup_true_lang.13670ae7.js → AbstraButton.vue_vue_type_script_setup_true_lang.6c541630.js} +2 -2
  24. abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.e1cfa824.js +2 -0
  25. abstra_statics/dist/assets/{ApiKeys.9b0b18b5.js → ApiKeys.96f96fe2.js} +2 -2
  26. abstra_statics/dist/assets/App.6b1b6a94.js +2 -0
  27. abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.08210ecb.js +2 -0
  28. abstra_statics/dist/assets/BaseLayout.2a82be24.js +2 -0
  29. abstra_statics/dist/assets/{Billing.f9062d88.js → Billing.24199f3a.js} +2 -2
  30. abstra_statics/dist/assets/{Breadcrumb.e54636d6.js → Breadcrumb.c8da3019.js} +2 -2
  31. abstra_statics/dist/assets/{Builds.c7363e1b.js → Builds.366d6ceb.js} +2 -2
  32. abstra_statics/dist/assets/{Card.4a8a30bb.js → Card.0d56c597.js} +2 -2
  33. abstra_statics/dist/assets/{CircularLoading.d81a4cac.js → CircularLoading.daf759d9.js} +2 -2
  34. abstra_statics/dist/assets/CloseCircleOutlined.2be0ee6c.js +2 -0
  35. abstra_statics/dist/assets/{ConnectorsView.d4b67e2e.js → ConnectorsView.904ff1c0.js} +2 -2
  36. abstra_statics/dist/assets/ConsoleOmniChat.vue_vue_type_script_setup_true_lang.17fc3e62.js +2 -0
  37. abstra_statics/dist/assets/ContentLayout.c133929b.js +2 -0
  38. abstra_statics/dist/assets/{CrudView.57e8b29a.js → CrudView.789b533f.js} +2 -2
  39. abstra_statics/dist/assets/{DocsButton.vue_vue_type_script_setup_true_lang.108b18e1.js → DocsButton.vue_vue_type_script_setup_true_lang.a2b3eeb7.js} +2 -2
  40. abstra_statics/dist/assets/{EditorLogin.2f00deb7.js → EditorLogin.cac0ed52.js} +2 -2
  41. abstra_statics/dist/assets/{EditorsView.eb87a2d8.js → EditorsView.4b74b13b.js} +2 -2
  42. abstra_statics/dist/assets/EnvVars.0dfba770.js +2 -0
  43. abstra_statics/dist/assets/{Error.98b8036c.js → Error.c7f25d1b.js} +2 -2
  44. abstra_statics/dist/assets/ExclamationCircleOutlined.ad66211a.js +2 -0
  45. abstra_statics/dist/assets/ExecutionContext.91c0e0db.js +2 -0
  46. abstra_statics/dist/assets/ExecutionStatusIcon.vue_vue_type_script_setup_true_lang.31b499a5.js +2 -0
  47. abstra_statics/dist/assets/{Files.9fc8199a.js → Files.4ec90b9a.js} +2 -2
  48. abstra_statics/dist/assets/Form.5a5cac5f.js +2 -0
  49. abstra_statics/dist/assets/Form.7d1b0423.css +1 -0
  50. abstra_statics/dist/assets/FormRunner.4a5270dc.js +2 -0
  51. abstra_statics/dist/assets/{Home.191a6dce.js → Home.1271fb51.js} +2 -2
  52. abstra_statics/dist/assets/Home.f82c1587.js +2 -0
  53. abstra_statics/dist/assets/LoadingContainer.4430af86.js +2 -0
  54. abstra_statics/dist/assets/{LoadingOutlined.4c40acc4.js → LoadingOutlined.2d29f0e1.js} +2 -2
  55. abstra_statics/dist/assets/{Login.edfbdaea.js → Login.180d7b1c.js} +2 -2
  56. abstra_statics/dist/assets/Login.8e13b15d.js +2 -0
  57. abstra_statics/dist/assets/{Login.vue_vue_type_script_setup_true_lang.02acef81.js → Login.vue_vue_type_script_setup_true_lang.ab4402e7.js} +2 -2
  58. abstra_statics/dist/assets/Logo.e86b6b23.js +2 -0
  59. abstra_statics/dist/assets/Logs.03931b09.js +2 -0
  60. abstra_statics/dist/assets/Main.3afc4ba4.js +2 -0
  61. abstra_statics/dist/assets/MockForm.025d99f9.css +1 -0
  62. abstra_statics/dist/assets/{MockForm.091aa4ce.js → MockForm.1fa371d2.js} +2 -2
  63. abstra_statics/dist/assets/{Navbar.24019fd6.js → Navbar.2d7490c1.js} +2 -2
  64. abstra_statics/dist/assets/{NewEditor.f2d1c0c3.css → NewEditor.d2ee5048.css} +1 -1
  65. abstra_statics/dist/assets/NewEditor.dc0e868f.js +8 -0
  66. abstra_statics/dist/assets/OidcLoginCallback.8216e341.js +2 -0
  67. abstra_statics/dist/assets/OidcLogoutCallback.f471ecec.js +2 -0
  68. abstra_statics/dist/assets/{OmniChat.c78c1e51.js → OmniChat.be2d3d92.js} +2 -2
  69. abstra_statics/dist/assets/{OnboardingView.687780ed.js → OnboardingView.4957b5f8.js} +2 -2
  70. abstra_statics/dist/assets/{Organization.0ac1bf79.js → Organization.27e529a7.js} +2 -2
  71. abstra_statics/dist/assets/{Organizations.fc123489.js → Organizations.88897e55.js} +2 -2
  72. abstra_statics/dist/assets/{PhArrowCounterClockwise.vue.6ab1b899.js → PhArrowCounterClockwise.vue.9ad742bf.js} +2 -2
  73. abstra_statics/dist/assets/{PhArrowSquareOut.vue.1cebb708.js → PhArrowSquareOut.vue.2a07a9b3.js} +2 -2
  74. abstra_statics/dist/assets/{PhClockCounterClockwise.vue.dae2e135.js → PhClockCounterClockwise.vue.b4dc22ed.js} +2 -2
  75. abstra_statics/dist/assets/{PhCopy.vue.71703533.js → PhCopy.vue.17ec4184.js} +2 -2
  76. abstra_statics/dist/assets/PhCopySimple.vue.d9213ca9.js +2 -0
  77. abstra_statics/dist/assets/{PhCube.vue.f8549a9b.js → PhCube.vue.027246a6.js} +2 -2
  78. abstra_statics/dist/assets/PhDatabase.vue.a188015f.js +2 -0
  79. abstra_statics/dist/assets/{PhDotsThreeVertical.vue.9d76c4de.js → PhDotsThreeVertical.vue.e9c6f787.js} +2 -2
  80. abstra_statics/dist/assets/{PhDownloadSimple.vue.21156b6d.js → PhDownloadSimple.vue.9ba8ac6f.js} +2 -2
  81. abstra_statics/dist/assets/{PhFileArrowUp.vue.406b22e3.js → PhFileArrowUp.vue.cd3d3139.js} +2 -2
  82. abstra_statics/dist/assets/{PhFilePlus.vue.b180df90.js → PhFilePlus.vue.2464bbea.js} +2 -2
  83. abstra_statics/dist/assets/{PhFolderPlus.vue.b18fd061.js → PhFolderPlus.vue.696aab26.js} +2 -2
  84. abstra_statics/dist/assets/{PhGear.vue.bed38929.js → PhGear.vue.c247e86d.js} +2 -2
  85. abstra_statics/dist/assets/{PhKey.vue.6ef5fdd3.js → PhKey.vue.1a84e5d0.js} +2 -2
  86. abstra_statics/dist/assets/{PhPencil.vue.0fc0fcc0.js → PhPencil.vue.3586175f.js} +2 -2
  87. abstra_statics/dist/assets/{PhPencilSimple.vue.0707effd.js → PhPencilSimple.vue.e88b8fd0.js} +2 -2
  88. abstra_statics/dist/assets/{PhRocket.vue.761192f5.js → PhRocket.vue.8b8080c3.js} +2 -2
  89. abstra_statics/dist/assets/{PhSignOut.vue.8d8dfd96.js → PhSignOut.vue.9eb21b1c.js} +2 -2
  90. abstra_statics/dist/assets/{PhSparkle.vue.18ed0427.js → PhSparkle.vue.70bce97e.js} +2 -2
  91. abstra_statics/dist/assets/{PhTranslate.vue.00a17a08.js → PhTranslate.vue.e579c286.js} +2 -2
  92. abstra_statics/dist/assets/{PhUsersThree.vue.d69f0723.js → PhUsersThree.vue.65e9b349.js} +2 -2
  93. abstra_statics/dist/assets/{PhWarningCircle.vue.20bfeba7.js → PhWarningCircle.vue.3134c1fb.js} +2 -2
  94. abstra_statics/dist/assets/{PhWebhooksLogo.vue.58a98824.js → PhWebhooksLogo.vue.e7321653.js} +2 -2
  95. abstra_statics/dist/assets/{PlayerConfigProvider.ad360920.js → PlayerConfigProvider.f0eaf9f3.js} +2 -2
  96. abstra_statics/dist/assets/{PlayerNavbar.97e8dee9.js → PlayerNavbar.7c0453f2.js} +2 -2
  97. abstra_statics/dist/assets/Project.fad3c835.js +2 -0
  98. abstra_statics/dist/assets/{ProjectLogin.f92a038d.js → ProjectLogin.83ae9c4c.js} +2 -2
  99. abstra_statics/dist/assets/{ProjectSettings.582746dc.js → ProjectSettings.f4e91391.js} +2 -2
  100. abstra_statics/dist/assets/{ProjectsView.a6b3674b.js → ProjectsView.c4e3053a.js} +2 -2
  101. abstra_statics/dist/assets/{SaveButton.c3ad6e9b.js → SaveButton.986667ef.js} +2 -2
  102. abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.077a2088.js +2 -0
  103. abstra_statics/dist/assets/{Sidebar.69f9369e.js → Sidebar.b1e6ca23.js} +2 -2
  104. abstra_statics/dist/assets/{Sql.cdefe5b9.js → Sql.24116fa0.js} +4 -4
  105. abstra_statics/dist/assets/Steps.95771774.js +2 -0
  106. abstra_statics/dist/assets/TableCard.981d88c4.js +2 -0
  107. abstra_statics/dist/assets/{TableEditor.fcfa13de.js → TableEditor.a74fc9f4.js} +2 -2
  108. abstra_statics/dist/assets/{Tables.4ee84a7c.js → Tables.870957e7.js} +2 -2
  109. abstra_statics/dist/assets/{TablesDiagram.b1d1579e.js → TablesDiagram.6a217e62.js} +3 -3
  110. abstra_statics/dist/assets/TablesTabs.vue_vue_type_script_setup_true_lang.983777c7.js +2 -0
  111. abstra_statics/dist/assets/{Tasks.fd2605bd.js → Tasks.8fbc0cc0.js} +2 -2
  112. abstra_statics/dist/assets/{UploadOutlined.64837788.js → UploadOutlined.e2352877.js} +2 -2
  113. abstra_statics/dist/assets/{View.b144c5e3.js → View.fd9cb47e.js} +2 -2
  114. abstra_statics/dist/assets/{View.vue_vue_type_script_setup_true_lang.c79117ce.js → View.vue_vue_type_script_setup_true_lang.69e51c6f.js} +2 -2
  115. abstra_statics/dist/assets/{Watermark.c0756030.js → Watermark.b1fed4a7.js} +2 -2
  116. abstra_statics/dist/assets/{WebEditor.774989ad.js → WebEditor.75ce5bd6.js} +2 -2
  117. abstra_statics/dist/assets/{WidgetPreview.4fd6afc0.js → WidgetPreview.5b0abaab.js} +2 -2
  118. abstra_statics/dist/assets/WorkflowViewer.0a209003.css +1 -0
  119. abstra_statics/dist/assets/WorkflowViewer.6f38d23f.js +2 -0
  120. abstra_statics/dist/assets/ant-design.5bd7ec4d.js +2 -0
  121. abstra_statics/dist/assets/{apiKey.ee792d72.js → apiKey.7cf16e08.js} +2 -2
  122. abstra_statics/dist/assets/asyncComputed.febe2b11.js +2 -0
  123. abstra_statics/dist/assets/{build.6e7d77b3.js → build.db7d8668.js} +2 -2
  124. abstra_statics/dist/assets/colorHelpers.939427f5.js +2 -0
  125. abstra_statics/dist/assets/{console.38bda98e.js → console.f402574a.js} +3 -3
  126. abstra_statics/dist/assets/{constants.be8ad36c.js → constants.bbfdfb21.js} +2 -2
  127. abstra_statics/dist/assets/contracts.generated.3f22c968.js +2 -0
  128. abstra_statics/dist/assets/{cssMode.408206bf.js → cssMode.d0c8e26e.js} +2 -2
  129. abstra_statics/dist/assets/{datetime.a6d58ce1.js → datetime.d5fe62ba.js} +2 -2
  130. abstra_statics/dist/assets/{dayjs.703ebc20.js → dayjs.8c9480e7.js} +2 -2
  131. abstra_statics/dist/assets/editor.11a4f0cf.js +2 -0
  132. abstra_statics/dist/assets/editor.main.2f21f781.js +2 -0
  133. abstra_statics/dist/assets/fetch.e0dfa394.js +2 -0
  134. abstra_statics/dist/assets/{files.1c1692f5.js → files.59b464cc.js} +2 -2
  135. abstra_statics/dist/assets/{folder.1b74b12c.js → folder.5b0f3179.js} +2 -2
  136. abstra_statics/dist/assets/{freemarker2.e62e067c.js → freemarker2.03629ab6.js} +2 -2
  137. abstra_statics/dist/assets/{handlebars.604fc901.js → handlebars.1163d9ba.js} +2 -2
  138. abstra_statics/dist/assets/{html.c02f177e.js → html.ef74c7bd.js} +2 -2
  139. abstra_statics/dist/assets/{htmlMode.64078e03.js → htmlMode.8829acd3.js} +2 -2
  140. abstra_statics/dist/assets/{index.2ec95eae.js → index.07e9309d.js} +2 -2
  141. abstra_statics/dist/assets/{index.5197afb2.js → index.12e6cfe2.js} +2 -2
  142. abstra_statics/dist/assets/{index.a12eba98.js → index.2aa34d4f.js} +5 -5
  143. abstra_statics/dist/assets/{index.82590a75.js → index.5eeedb69.js} +2 -2
  144. abstra_statics/dist/assets/{index.b91afb03.js → index.63e70668.js} +2 -2
  145. abstra_statics/dist/assets/index.75a16b09.js +2 -0
  146. abstra_statics/dist/assets/{index.bec0ecd0.js → index.76cbd30f.js} +2 -2
  147. abstra_statics/dist/assets/{index.015caad7.js → index.8995a499.js} +2 -2
  148. abstra_statics/dist/assets/{index.82842143.js → index.d408b03e.js} +2 -2
  149. abstra_statics/dist/assets/{javascript.57026f87.js → javascript.0dfeb7bb.js} +3 -3
  150. abstra_statics/dist/assets/{jsonMode.9b45b375.js → jsonMode.179b6695.js} +2 -2
  151. abstra_statics/dist/assets/{jwt-decode.c5760184.css → jwt-decode.cfe2994b.css} +1 -1
  152. abstra_statics/dist/assets/{jwt-decode.esm.3348bca5.js → jwt-decode.esm.47f59010.js} +88 -54
  153. abstra_statics/dist/assets/linters.9f818fd6.js +2 -0
  154. abstra_statics/dist/assets/{liquid.233d5164.js → liquid.0627704b.js} +3 -3
  155. abstra_statics/dist/assets/{member.d878cf3f.js → member.689a99e8.js} +2 -2
  156. abstra_statics/dist/assets/{metadata.9f7495db.js → metadata.69e468d6.js} +2 -2
  157. abstra_statics/dist/assets/{omniChatStore.40ad0b1b.js → omniChatStore.07f62bd5.js} +2 -2
  158. abstra_statics/dist/assets/{organization.8f08e075.js → organization.298987a0.js} +2 -2
  159. abstra_statics/dist/assets/{os.8ffdbf05.js → os.faa277a9.js} +2 -2
  160. abstra_statics/dist/assets/player.ebf3133f.js +2 -0
  161. abstra_statics/dist/assets/{plotly.min.da87d61b.js → plotly.min.16914e67.js} +2 -2
  162. abstra_statics/dist/assets/polling.96dd15ee.js +2 -0
  163. abstra_statics/dist/assets/{project.2483de10.js → project.8a5a3632.js} +2 -2
  164. abstra_statics/dist/assets/{python.1bdbd404.js → python.3bf17d7f.js} +3 -3
  165. abstra_statics/dist/assets/{razor.be821b87.js → razor.ea162aec.js} +3 -3
  166. abstra_statics/dist/assets/{record.a108da5a.js → record.30ff6eef.js} +2 -2
  167. abstra_statics/dist/assets/{redirect.eedb2bf6.js → redirect.d0ca2136.js} +2 -2
  168. abstra_statics/dist/assets/{repository.48119e01.js → repository.5c0cd878.js} +2 -2
  169. abstra_statics/dist/assets/repository.b1c27c35.js +2 -0
  170. abstra_statics/dist/assets/router.ae5c14de.js +2 -0
  171. abstra_statics/dist/assets/{router.c6e27700.js → router.cfb03f89.js} +5 -5
  172. abstra_statics/dist/assets/{string.998fa621.js → string.39c8a903.js} +2 -2
  173. abstra_statics/dist/assets/{tables.9701f90c.js → tables.34208b7c.js} +2 -2
  174. abstra_statics/dist/assets/tasksController.04461f1a.js +4 -0
  175. abstra_statics/dist/assets/{toggleHighContrast.23d5a1ab.js → toggleHighContrast.fa77fdf8.js} +7 -7
  176. abstra_statics/dist/assets/{tsMode.4558d65a.js → tsMode.a5869619.js} +2 -2
  177. abstra_statics/dist/assets/{typescript.4445d2fa.js → typescript.f2aa2c4b.js} +3 -3
  178. abstra_statics/dist/assets/url.4ba49005.js +2 -0
  179. abstra_statics/dist/assets/{useCodebaseEvents.6ebbc5a2.js → useCodebaseEvents.3542d20f.js} +2 -2
  180. abstra_statics/dist/assets/useTables.3e387cf0.js +2 -0
  181. abstra_statics/dist/assets/userStore.e8304ebc.js +2 -0
  182. abstra_statics/dist/assets/uuid.2075c158.js +2 -0
  183. abstra_statics/dist/assets/{vue-flow-background.f1022925.js → vue-flow-background.32950d16.js} +2 -2
  184. abstra_statics/dist/assets/{vue-flow-core.0de753a6.js → vue-flow-core.d96b7b33.js} +2 -2
  185. abstra_statics/dist/assets/{vue-quill.esm-bundler.8f4ad2b3.js → vue-quill.esm-bundler.42450ff3.js} +2 -2
  186. abstra_statics/dist/assets/{workspaceStore.5d3f2aec.js → workspaceStore.9693f43b.js} +2 -2
  187. abstra_statics/dist/assets/{xml.8a25758b.js → xml.a1244cf9.js} +3 -3
  188. abstra_statics/dist/assets/{yaml.e466330b.js → yaml.759fa896.js} +3 -3
  189. abstra_statics/dist/console.html +15 -15
  190. abstra_statics/dist/editor.html +14 -14
  191. abstra_statics/dist/player.html +10 -10
  192. abstra_internals/services/fs_storage.py +0 -76
  193. abstra_internals/services/fs_storage_test.py +0 -71
  194. abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.1035457c.js +0 -2
  195. abstra_statics/dist/assets/App.9ab9cabb.js +0 -2
  196. abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.6713c9c9.js +0 -2
  197. abstra_statics/dist/assets/BaseLayout.28c01b5b.js +0 -2
  198. abstra_statics/dist/assets/CloseCircleOutlined.39b5ab06.js +0 -2
  199. abstra_statics/dist/assets/ConsoleOmniChat.vue_vue_type_script_setup_true_lang.5360224e.js +0 -2
  200. abstra_statics/dist/assets/ContentLayout.10f24838.js +0 -2
  201. abstra_statics/dist/assets/EnvVars.883a4a57.js +0 -2
  202. abstra_statics/dist/assets/ExclamationCircleOutlined.2441b96e.js +0 -2
  203. abstra_statics/dist/assets/Form.5d562f15.js +0 -2
  204. abstra_statics/dist/assets/Form.7493bc0a.css +0 -1
  205. abstra_statics/dist/assets/FormRunner.2b1b3c45.js +0 -2
  206. abstra_statics/dist/assets/Home.8502aa41.js +0 -2
  207. abstra_statics/dist/assets/LoadingContainer.ac03ea28.js +0 -2
  208. abstra_statics/dist/assets/Login.8bd6a07a.js +0 -2
  209. abstra_statics/dist/assets/Logo.fc8ace6c.js +0 -2
  210. abstra_statics/dist/assets/Logs.4c6c0b3a.js +0 -2
  211. abstra_statics/dist/assets/LogsController.a58ca42a.js +0 -2
  212. abstra_statics/dist/assets/Main.e6b2d2d5.js +0 -2
  213. abstra_statics/dist/assets/MockForm.e410c2c1.css +0 -1
  214. abstra_statics/dist/assets/NewEditor.2b6f4ed3.js +0 -8
  215. abstra_statics/dist/assets/OidcLoginCallback.987cebba.js +0 -2
  216. abstra_statics/dist/assets/OidcLogoutCallback.6c00d878.js +0 -2
  217. abstra_statics/dist/assets/PhCopySimple.vue.369eb629.js +0 -2
  218. abstra_statics/dist/assets/PhDatabase.vue.0d3246d7.js +0 -2
  219. abstra_statics/dist/assets/Project.6c4642b5.js +0 -2
  220. abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.62178939.js +0 -2
  221. abstra_statics/dist/assets/Steps.82252fc0.js +0 -2
  222. abstra_statics/dist/assets/TableCard.5462c89d.js +0 -2
  223. abstra_statics/dist/assets/TablesTabs.vue_vue_type_script_setup_true_lang.3e5206e0.js +0 -2
  224. abstra_statics/dist/assets/WorkflowViewer.2666936e.js +0 -2
  225. abstra_statics/dist/assets/WorkflowViewer.3b6aee8e.css +0 -1
  226. abstra_statics/dist/assets/ant-design.b3eefa58.js +0 -2
  227. abstra_statics/dist/assets/asyncComputed.c73d027a.js +0 -2
  228. abstra_statics/dist/assets/colorHelpers.5ee17d14.js +0 -2
  229. abstra_statics/dist/assets/contracts.generated.f01de5a3.js +0 -2
  230. abstra_statics/dist/assets/editor.a77b56bd.js +0 -2
  231. abstra_statics/dist/assets/editor.main.a1ebf0ab.js +0 -2
  232. abstra_statics/dist/assets/fetch.cd29ef4c.js +0 -2
  233. abstra_statics/dist/assets/index.b72cb2b3.js +0 -2
  234. abstra_statics/dist/assets/linters.903f3240.js +0 -2
  235. abstra_statics/dist/assets/player.7112583e.js +0 -2
  236. abstra_statics/dist/assets/polling.f547718c.js +0 -2
  237. abstra_statics/dist/assets/repository.353e892d.js +0 -2
  238. abstra_statics/dist/assets/repository.677ca13c.js +0 -2
  239. abstra_statics/dist/assets/router.c7abfb0c.js +0 -2
  240. abstra_statics/dist/assets/tasksController.5db769f7.js +0 -4
  241. abstra_statics/dist/assets/url.5d02a63f.js +0 -2
  242. abstra_statics/dist/assets/useTables.4d5edd80.js +0 -2
  243. abstra_statics/dist/assets/userStore.34b8f1eb.js +0 -2
  244. abstra_statics/dist/assets/uuid.6980e2bb.js +0 -2
  245. {abstra-3.24.3.dist-info → abstra-3.24.5.dist-info}/WHEEL +0 -0
  246. {abstra-3.24.3.dist-info → abstra-3.24.5.dist-info}/entry_points.txt +0 -0
  247. {abstra-3.24.3.dist-info → abstra-3.24.5.dist-info}/top_level.txt +0 -0
  248. /abstra_statics/dist/assets/{LogsController.61f8e22d.css → ExecutionContext.61f8e22d.css} +0 -0
@@ -368,6 +368,48 @@ class WorkflowController:
368
368
  - Changes are persisted immediately to the project configuration
369
369
  - Use with caution as it can completely restructure the workflow
370
370
  """
371
+ # Deduplicate transitions from frontend to prevent corrupted state
372
+ # This handles cases where the client may send duplicate transitions
373
+ # Rules:
374
+ # 1. No duplicate IDs (keep first occurrence)
375
+ # 2. No duplicate source→target pairs (keep last occurrence as an update)
376
+ # 3. Allow bidirectional: a→b and b→a are both valid
377
+ if "transitions" in workflow_state_dto:
378
+ original_count = len(workflow_state_dto["transitions"])
379
+
380
+ # Use dict to track transitions by (source, target) pair
381
+ # This ensures only one transition per direction
382
+ transitions_by_direction = {}
383
+
384
+ for transition in workflow_state_dto["transitions"]:
385
+ source_id = transition.get("sourceStageId")
386
+ target_id = transition.get("targetStageId")
387
+
388
+ # Skip invalid transitions
389
+ if not source_id or not target_id:
390
+ continue
391
+
392
+ # Key by (source, target) to enforce one transition per direction
393
+ # Note: (a, b) != (b, a), so bidirectional edges are allowed
394
+ direction_key = (source_id, target_id)
395
+
396
+ # Later occurrences replace earlier ones (update semantics)
397
+ transitions_by_direction[direction_key] = transition
398
+
399
+ workflow_state_dto["transitions"] = list(transitions_by_direction.values())
400
+
401
+ # Log if duplicates were found and removed
402
+ deduped_count = len(workflow_state_dto["transitions"])
403
+ if original_count > deduped_count:
404
+ from abstra_internals.logger import AbstraLogger
405
+
406
+ removed_count = original_count - deduped_count
407
+ AbstraLogger.warning(
408
+ f"Removed {removed_count} duplicate transitions from workflow update "
409
+ f"(had {original_count}, now {deduped_count}). "
410
+ f"Multiple transitions with same source→target were treated as updates."
411
+ )
412
+
371
413
  project = self.repos.project.load(include_disabled_stages=True)
372
414
 
373
415
  for stage_dto in workflow_state_dto["stages"]:
@@ -4,6 +4,7 @@ from typing import Dict, List, Optional, TypeVar, Union
4
4
  from abstra_internals.contracts_generated import (
5
5
  CloudApiCliModelsBankStatementResponse,
6
6
  CloudApiCliModelsBoletoResponse,
7
+ CloudApiCliModelsInvoiceResponse,
7
8
  CloudApiCliModelsNfeResponse,
8
9
  CloudApiCliModelsNfseResponse,
9
10
  CloudApiCliModelsUsDriverLicenseResponse,
@@ -174,6 +175,74 @@ def parse_nfe(document_path: Union["Path", str]) -> CloudApiCliModelsNfeResponse
174
175
  return CloudApiCliModelsNfeResponse.from_dict(data)
175
176
 
176
177
 
178
+ def parse_invoice(
179
+ document_path: Union["Path", str],
180
+ ) -> CloudApiCliModelsInvoiceResponse:
181
+ """
182
+ Parse an invoice document using AI-powered OCR to extract comprehensive invoice information including supplier and receiver details, financial data, shipping information, and line items.
183
+
184
+ The parser extracts 35+ fields including:
185
+
186
+ **Supplier Information:**
187
+ - supplier_name: Name of the supplier/vendor
188
+ - supplier_address: Supplier's full address
189
+ - supplier_email: Supplier's email address
190
+ - supplier_phone: Supplier's phone number
191
+ - supplier_tax_id: Supplier's tax identification number
192
+ - supplier_registration: Supplier's registration number
193
+ - supplier_iban: Supplier's IBAN for payments
194
+ - supplier_payment_ref: Payment reference number
195
+ - supplier_website: Supplier's website URL
196
+
197
+ **Receiver Information:**
198
+ - receiver_name: Name of the receiver/customer
199
+ - receiver_address: Receiver's full address
200
+ - receiver_email: Receiver's email address
201
+ - receiver_phone: Receiver's phone number
202
+ - receiver_tax_id: Receiver's tax identification number
203
+ - receiver_website: Receiver's website URL
204
+
205
+ **Financial Information:**
206
+ - invoice_id: Invoice number/identifier
207
+ - invoice_date: Date the invoice was issued
208
+ - due_date: Payment due date
209
+ - total_amount: Total invoice amount
210
+ - net_amount: Net amount (after tax/discount)
211
+ - total_tax_amount: Total tax amount
212
+ - freight_amount: Freight/shipping cost
213
+ - amount_paid_since_last_invoice: Amount paid since last invoice
214
+ - currency: Currency code (e.g., USD, EUR, BRL)
215
+ - currency_exchange_rate: Exchange rate if applicable
216
+ - payment_terms: Payment terms and conditions
217
+
218
+ **Shipping Information:**
219
+ - ship_from_name: Ship-from party name
220
+ - ship_from_address: Ship-from address
221
+ - ship_to_name: Ship-to party name
222
+ - ship_to_address: Ship-to destination address
223
+ - remit_to_name: Remit-to party name
224
+ - remit_to_address: Remit-to address for payment
225
+ - carrier: Shipping carrier name
226
+ - delivery_date: Expected or actual delivery date
227
+
228
+ **Other Information:**
229
+ - purchase_order: Purchase order number reference
230
+
231
+ Args:
232
+ document_path (Union[Path, str]): The path to the invoice document to be parsed.
233
+
234
+ Returns:
235
+ dict: The parsed invoice data.
236
+
237
+ Raises:
238
+ ValueError: If document path is invalid or parsing fails.
239
+ """
240
+ data = SDKContextStore.get_by_thread().ai_sdk.parse_document(
241
+ document_path, "invoice"
242
+ )
243
+ return CloudApiCliModelsInvoiceResponse.from_dict(data)
244
+
245
+
177
246
  def parse_boleto(document_path: Union["Path", str]) -> CloudApiCliModelsBoletoResponse:
178
247
  """
179
248
  Parse a Brazilian Boleto (bank slip) document using AI-powered OCR for automated payment processing.
@@ -6,12 +6,9 @@ from typing import List, Optional
6
6
  from abstra_internals.cloud_api.http_client import HTTPClient
7
7
  from abstra_internals.consts.filepaths import EXECUTIONS_DIR_PATH
8
8
  from abstra_internals.entities.execution import Execution, ExecutionStatus
9
- from abstra_internals.environment import (
10
- SERVER_UUID,
11
- WORKER_UUID,
12
- )
9
+ from abstra_internals.environment import SERVER_UUID, WORKER_UUID
13
10
  from abstra_internals.repositories.multiprocessing import MPContext
14
- from abstra_internals.services.fs_storage import FileSystemStorage
11
+ from abstra_internals.services.sql_storage import SqlStorage
15
12
 
16
13
 
17
14
  @dataclass
@@ -101,7 +98,7 @@ class ExecutionRepository(ABC):
101
98
 
102
99
  class LocalExecutionRepository(ExecutionRepository):
103
100
  def __init__(self, mp_context: MPContext):
104
- self.fs_storage = FileSystemStorage(
101
+ self.fs_storage = SqlStorage(
105
102
  mp_context, directory=EXECUTIONS_DIR_PATH, model=Execution
106
103
  )
107
104
 
@@ -1,7 +1,7 @@
1
1
  import shutil
2
2
  import subprocess
3
3
  from pathlib import Path
4
- from typing import List, Optional, Tuple
4
+ from typing import Dict, List, Optional, Tuple
5
5
 
6
6
  from abstra_internals.environment import REMOTE_NAME
7
7
 
@@ -19,7 +19,10 @@ class NativeGitRepository(GitRepositoryInterface):
19
19
  self._git_available = None
20
20
 
21
21
  def _run_git_command(
22
- self, command: List[str], cwd: Optional[Path] = None
22
+ self,
23
+ command: List[str],
24
+ cwd: Optional[Path] = None,
25
+ input: Optional[str] = None,
23
26
  ) -> Tuple[bool, str, str]:
24
27
  """Run a git command and return success, stdout, stderr"""
25
28
  try:
@@ -27,7 +30,12 @@ class NativeGitRepository(GitRepositoryInterface):
27
30
  cwd = self.working_directory
28
31
 
29
32
  result = subprocess.run(
30
- ["git"] + command, cwd=cwd, capture_output=True, text=True, timeout=30
33
+ ["git"] + command,
34
+ cwd=cwd,
35
+ capture_output=True,
36
+ text=True,
37
+ timeout=30,
38
+ input=input,
31
39
  )
32
40
  return result.returncode == 0, result.stdout.strip(), result.stderr.strip()
33
41
  except (
@@ -582,3 +590,82 @@ class NativeGitRepository(GitRepositoryInterface):
582
590
  return "<<<<<<< " in output or "=======" in output or ">>>>>>> " in output
583
591
 
584
592
  return False
593
+
594
+ def check_ignore(self, path: Path) -> bool:
595
+ """
596
+ Check if a path should be ignored according to .gitignore rules.
597
+ Uses git check-ignore for accurate gitignore handling.
598
+ """
599
+ if not self.is_git_repository():
600
+ return False
601
+
602
+ try:
603
+ # Get path relative to working directory
604
+ try:
605
+ relative_path = path.relative_to(self.working_directory)
606
+ except ValueError:
607
+ # Path is outside working directory, not ignored
608
+ return False
609
+
610
+ # Use git check-ignore -q (quiet mode, exit code only)
611
+ success, _, _ = self._run_git_command(
612
+ ["check-ignore", "-q", str(relative_path)]
613
+ )
614
+
615
+ # Exit code 0 means the path is ignored
616
+ return success
617
+
618
+ except Exception:
619
+ return False
620
+
621
+ def check_ignore_batch(self, paths: List[Path]) -> Dict[Path, bool]:
622
+ """
623
+ Check multiple paths at once using git check-ignore --stdin.
624
+ Much more efficient than checking one path at a time.
625
+ """
626
+ if not self.is_git_repository():
627
+ return {path: False for path in paths}
628
+
629
+ results = {}
630
+ relative_paths = []
631
+ path_map = {}
632
+
633
+ # Convert to relative paths
634
+ for path in paths:
635
+ try:
636
+ rel_path = path.relative_to(self.working_directory)
637
+ rel_path_str = str(rel_path)
638
+ relative_paths.append(rel_path_str)
639
+ path_map[rel_path_str] = path
640
+ except ValueError:
641
+ # Path outside working directory, not ignored
642
+ results[path] = False
643
+
644
+ if not relative_paths:
645
+ return results
646
+
647
+ try:
648
+ # Use git check-ignore with stdin for batch processing
649
+ input_data = "\n".join(relative_paths)
650
+ success, stdout, _ = self._run_git_command(
651
+ ["check-ignore", "--stdin"],
652
+ cwd=self.working_directory,
653
+ input=input_data,
654
+ )
655
+
656
+ # git check-ignore --stdin returns the paths that ARE ignored in stdout
657
+ ignored_paths_set = (
658
+ set(stdout.strip().split("\n")) if stdout.strip() else set()
659
+ )
660
+
661
+ # Map results back to original paths
662
+ for rel_path_str, path in path_map.items():
663
+ results[path] = rel_path_str in ignored_paths_set
664
+
665
+ except Exception:
666
+ # On error, mark all remaining as not ignored
667
+ for path in paths:
668
+ if path not in results:
669
+ results[path] = False
670
+
671
+ return results
@@ -271,3 +271,13 @@ class GitRepositoryInterface(ABC):
271
271
  ) -> Tuple[int, int]:
272
272
  """Calculate ahead/behind count between local and remote commits"""
273
273
  pass
274
+
275
+ @abstractmethod
276
+ def check_ignore(self, path: Path) -> bool:
277
+ """Check if a path should be ignored according to .gitignore rules"""
278
+ pass
279
+
280
+ @abstractmethod
281
+ def check_ignore_batch(self, paths: List[Path]) -> Dict[Path, bool]:
282
+ """Check multiple paths at once for better performance"""
283
+ pass
@@ -17,6 +17,8 @@ class AddEnvToGitIgnore(LinterFix):
17
17
  abstraignore_file = Settings.root_path / GITIGNORE_FILEPATH
18
18
  with abstraignore_file.open("a") as file:
19
19
  file.write("\n.env")
20
+ # Clear cache after modifying .gitignore
21
+ FileSystemService.clear_gitignore_cache()
20
22
 
21
23
 
22
24
  class EnvInBundleFound(LinterIssue):
@@ -23,6 +23,7 @@ from .migration_012 import Migration012
23
23
  from .migration_013 import Migration013
24
24
  from .migration_014 import Migration014
25
25
  from .migration_015 import Migration015
26
+ from .migration_016 import Migration016
26
27
 
27
28
  MIGRATIONS: List[Type[Migration]] = [
28
29
  Migration001,
@@ -40,6 +41,7 @@ MIGRATIONS: List[Type[Migration]] = [
40
41
  Migration013,
41
42
  Migration014,
42
43
  Migration015,
44
+ Migration016,
43
45
  ]
44
46
 
45
47
 
@@ -0,0 +1,17 @@
1
+ from .base_migration import Migration
2
+
3
+
4
+ class Migration016(Migration):
5
+ @staticmethod
6
+ def target_version() -> str:
7
+ return "16.0"
8
+
9
+ @staticmethod
10
+ def source_version() -> str:
11
+ return "15.0"
12
+
13
+ def _migrate(self) -> None:
14
+ for form_data in self.data.get("forms", []):
15
+ form_data.pop("allow_restart", None)
16
+ form_data.pop("restart_button_text", None)
17
+ form_data.pop("welcome_title", None)
@@ -0,0 +1,141 @@
1
+ from unittest import TestCase
2
+
3
+ from abstra_internals.repositories.project.json_migrations.migration_016 import (
4
+ Migration016,
5
+ )
6
+
7
+
8
+ class TestMigration016(TestCase):
9
+ def test_removes_deprecated_form_fields(self):
10
+ data = {
11
+ "forms": [
12
+ {
13
+ "id": "f1",
14
+ "title": "Form A",
15
+ "file": "form_a.py",
16
+ "path": "/form-a",
17
+ "is_initial": True,
18
+ "auto_start": False,
19
+ "allow_restart": True,
20
+ "restart_button_text": "Start Over",
21
+ "welcome_title": "Welcome!",
22
+ "end_message": None,
23
+ "start_message": None,
24
+ "error_message": None,
25
+ "timeout_message": None,
26
+ "start_button_text": "Begin",
27
+ "notification_trigger": {
28
+ "variable_name": "notify",
29
+ "enabled": False,
30
+ },
31
+ "access_control": {"is_public": True, "required_roles": []},
32
+ "other_field": "should remain",
33
+ }
34
+ ],
35
+ "scripts": [],
36
+ "jobs": [],
37
+ "hooks": [],
38
+ "version": "15.0",
39
+ }
40
+ m = Migration016(data)
41
+ m.apply()
42
+
43
+ self.assertEqual(m.data["version"], "16.0")
44
+
45
+ form = m.data["forms"][0]
46
+ self.assertNotIn("allow_restart", form)
47
+ self.assertNotIn("restart_button_text", form)
48
+ self.assertNotIn("welcome_title", form)
49
+
50
+ self.assertEqual(form["other_field"], "should remain")
51
+ self.assertEqual(form["title"], "Form A")
52
+ self.assertEqual(form["path"], "/form-a")
53
+ self.assertEqual(form["is_initial"], True)
54
+ self.assertEqual(form["auto_start"], False)
55
+
56
+ def test_handles_multiple_forms(self):
57
+ data = {
58
+ "forms": [
59
+ {
60
+ "id": "f1",
61
+ "title": "Form 1",
62
+ "file": "form_1.py",
63
+ "path": "/form-1",
64
+ "is_initial": True,
65
+ "auto_start": False,
66
+ "allow_restart": True,
67
+ "restart_button_text": "Restart 1",
68
+ "welcome_title": "Welcome 1",
69
+ "end_message": None,
70
+ "start_message": None,
71
+ "error_message": None,
72
+ "timeout_message": None,
73
+ "start_button_text": None,
74
+ "notification_trigger": {"variable_name": "n", "enabled": False},
75
+ "access_control": {"is_public": True, "required_roles": []},
76
+ },
77
+ {
78
+ "id": "f2",
79
+ "title": "Form 2",
80
+ "file": "form_2.py",
81
+ "path": "/form-2",
82
+ "is_initial": False,
83
+ "auto_start": True,
84
+ "allow_restart": False,
85
+ "restart_button_text": None,
86
+ "welcome_title": "Hello",
87
+ "end_message": None,
88
+ "start_message": None,
89
+ "error_message": None,
90
+ "timeout_message": None,
91
+ "start_button_text": None,
92
+ "notification_trigger": {"variable_name": "n", "enabled": False},
93
+ "access_control": {"is_public": False, "required_roles": ["admin"]},
94
+ },
95
+ ],
96
+ "scripts": [],
97
+ "jobs": [],
98
+ "hooks": [],
99
+ "version": "15.0",
100
+ }
101
+ m = Migration016(data)
102
+ m.apply()
103
+
104
+ for form in m.data["forms"]:
105
+ self.assertNotIn("allow_restart", form)
106
+ self.assertNotIn("restart_button_text", form)
107
+ self.assertNotIn("welcome_title", form)
108
+
109
+ self.assertIn("path", form)
110
+ self.assertIn("is_initial", form)
111
+ self.assertIn("auto_start", form)
112
+
113
+ def test_handles_forms_without_deprecated_fields(self):
114
+ data = {
115
+ "forms": [
116
+ {
117
+ "id": "f1",
118
+ "title": "Form Without Deprecated Fields",
119
+ "file": "form.py",
120
+ "path": "/form",
121
+ "is_initial": True,
122
+ "auto_start": False,
123
+ "end_message": None,
124
+ "start_message": None,
125
+ "error_message": None,
126
+ "timeout_message": None,
127
+ "start_button_text": None,
128
+ "notification_trigger": {"variable_name": "n", "enabled": False},
129
+ "access_control": {"is_public": True, "required_roles": []},
130
+ }
131
+ ],
132
+ "scripts": [],
133
+ "jobs": [],
134
+ "hooks": [],
135
+ "version": "15.0",
136
+ }
137
+ m = Migration016(data)
138
+ m.apply()
139
+
140
+ self.assertEqual(m.data["version"], "16.0")
141
+ self.assertEqual(len(m.data["forms"]), 1)
@@ -496,11 +496,8 @@ class FormStage(StageWithFile):
496
496
  auto_start: Optional[bool] = False
497
497
  start_message: Optional[str] = None
498
498
  error_message: Optional[str] = None
499
- welcome_title: Optional[str] = None
500
- allow_restart: Optional[bool] = False
501
499
  timeout_message: Optional[str] = None
502
500
  start_button_text: Optional[str] = None
503
- restart_button_text: Optional[str] = None
504
501
  access_control: AccessSettings = field(
505
502
  default_factory=lambda: AccessSettings(is_public=False, required_roles=[])
506
503
  )
@@ -541,11 +538,8 @@ class FormStage(StageWithFile):
541
538
  auto_start=data["auto_start"],
542
539
  start_message=data["start_message"],
543
540
  error_message=data["error_message"],
544
- welcome_title=data["welcome_title"],
545
- allow_restart=data["allow_restart"],
546
541
  timeout_message=data["timeout_message"],
547
542
  start_button_text=data["start_button_text"],
548
- restart_button_text=data["restart_button_text"],
549
543
  workflow_position=(x, y),
550
544
  is_initial=data["is_initial"],
551
545
  workflow_transitions=[
@@ -569,22 +563,17 @@ class FormStage(StageWithFile):
569
563
 
570
564
  @property
571
565
  def browser_runner_dto(self):
572
- allow_restart = self.allow_restart if self.is_initial else False
573
-
574
566
  return {
575
567
  "id": self.id,
576
568
  "path": self.path,
577
569
  "title": self.title,
578
570
  "is_initial": self.is_initial,
579
571
  "auto_start": self.auto_start,
580
- "allow_restart": allow_restart,
581
572
  "end_message": self.end_message,
582
573
  "start_message": self.start_message,
583
574
  "error_message": self.error_message,
584
- "welcome_title": self.welcome_title,
585
575
  "timeout_message": self.timeout_message,
586
576
  "start_button_text": self.start_button_text,
587
- "restart_button_text": self.restart_button_text,
588
577
  }
589
578
 
590
579
  @property
@@ -618,11 +607,8 @@ class FormStage(StageWithFile):
618
607
  "auto_start",
619
608
  "start_message",
620
609
  "error_message",
621
- "welcome_title",
622
- "allow_restart",
623
610
  "timeout_message",
624
611
  "start_button_text",
625
- "restart_button_text",
626
612
  ]:
627
613
  setattr(self, attr, value)
628
614
  elif attr == "file":
@@ -657,11 +643,8 @@ class FormStage(StageWithFile):
657
643
  auto_start=self.auto_start or False,
658
644
  start_message=self.start_message,
659
645
  error_message=self.error_message,
660
- welcome_title=self.welcome_title,
661
- allow_restart=self.allow_restart or False,
662
646
  timeout_message=self.timeout_message,
663
647
  start_button_text=self.start_button_text,
664
- restart_button_text=self.restart_button_text,
665
648
  workflow_position=[self.workflow_position[0], self.workflow_position[1]],
666
649
  transitions=[t.to_abstra_json_dto() for t in self.workflow_transitions],
667
650
  is_initial=self.is_initial,
@@ -1222,6 +1205,50 @@ class Project:
1222
1205
  self.jobs = [j for j in self.jobs if j.id != id]
1223
1206
  self.scripts = [s for s in self.scripts if s.id != id]
1224
1207
 
1208
+ @staticmethod
1209
+ def __deduplicate_transitions(transitions: List[dict]) -> List[dict]:
1210
+ """
1211
+ Remove duplicate transitions based on ID and target, keeping only the first occurrence.
1212
+
1213
+ This is necessary to handle corrupted project files that may have accumulated
1214
+ duplicate transitions over time, which can cause performance issues when
1215
+ loading large workflows.
1216
+
1217
+ Rules:
1218
+ 1. No duplicate IDs (keeps first occurrence)
1219
+ 2. No duplicate targets (keeps first occurrence - since source is implicit from stage)
1220
+ 3. Allows bidirectional: a→b and b→a are in different stages, so both valid
1221
+
1222
+ Args:
1223
+ transitions: List of transition dictionaries from the JSON data
1224
+ (all from the same source stage)
1225
+
1226
+ Returns:
1227
+ List of unique transitions, preserving order
1228
+ """
1229
+ seen_ids = set()
1230
+ seen_targets = set()
1231
+ unique_transitions = []
1232
+
1233
+ for transition in transitions:
1234
+ transition_id = transition.get("id")
1235
+ target_id = transition.get("target_id")
1236
+
1237
+ # Skip if duplicate ID
1238
+ if transition_id in seen_ids:
1239
+ continue
1240
+
1241
+ # Skip if duplicate target (same source→target)
1242
+ if target_id in seen_targets:
1243
+ continue
1244
+
1245
+ # Valid unique transition
1246
+ seen_ids.add(transition_id)
1247
+ seen_targets.add(target_id)
1248
+ unique_transitions.append(transition)
1249
+
1250
+ return unique_transitions
1251
+
1225
1252
  @staticmethod
1226
1253
  def __from_dict(data: dict):
1227
1254
  target_stages = set()
@@ -1230,6 +1257,24 @@ class Project:
1230
1257
 
1231
1258
  stage_keys = ["forms", "hooks", "scripts", "jobs"]
1232
1259
 
1260
+ # Deduplicate transitions in each stage before processing
1261
+ for key in stage_keys:
1262
+ for stage in data[key]:
1263
+ if "transitions" in stage:
1264
+ original_count = len(stage["transitions"])
1265
+ stage["transitions"] = Project.__deduplicate_transitions(
1266
+ stage["transitions"]
1267
+ )
1268
+ deduped_count = len(stage["transitions"])
1269
+
1270
+ # Log warning if duplicates were found
1271
+ if original_count > deduped_count:
1272
+ removed_count = original_count - deduped_count
1273
+ AbstraLogger.warning(
1274
+ f"Removed {removed_count} duplicate transitions from stage {stage.get('id', 'unknown')} "
1275
+ f"(had {original_count}, now {deduped_count})"
1276
+ )
1277
+
1233
1278
  for key in stage_keys:
1234
1279
  for stage in data[key]:
1235
1280
  nodes.append(Node(id=stage["id"]))