abstra 3.24.3__py3-none-any.whl → 3.24.4__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 (240) hide show
  1. abstra/ai.py +2 -0
  2. {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/METADATA +1 -1
  3. {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/RECORD +190 -187
  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/git/native.py +90 -3
  8. abstra_internals/repositories/git/types.py +10 -0
  9. abstra_internals/repositories/linter/rules/env_in_bundle.py +2 -0
  10. abstra_internals/repositories/project/json_migrations/__init__.py +2 -0
  11. abstra_internals/repositories/project/json_migrations/migration_016.py +17 -0
  12. abstra_internals/repositories/project/json_migrations/migration_016_test.py +141 -0
  13. abstra_internals/repositories/project/project.py +62 -17
  14. abstra_internals/repositories/project/project_test.py +279 -0
  15. abstra_internals/services/fs.py +311 -32
  16. abstra_internals/services/fs_test.py +28 -5
  17. abstra_internals/utils/file.py +7 -3
  18. abstra_internals/utils/fs_cache.py +173 -0
  19. abstra_statics/dist/assets/{AbstraButton.vue_vue_type_script_setup_true_lang.13670ae7.js → AbstraButton.vue_vue_type_script_setup_true_lang.779f608b.js} +2 -2
  20. abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.9c4020b5.js +2 -0
  21. abstra_statics/dist/assets/{ApiKeys.9b0b18b5.js → ApiKeys.fbd3ff63.js} +2 -2
  22. abstra_statics/dist/assets/App.cdc99dd8.js +2 -0
  23. abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.c5b57972.js +2 -0
  24. abstra_statics/dist/assets/BaseLayout.c2cb2f91.js +2 -0
  25. abstra_statics/dist/assets/Billing.f53186a3.js +2 -0
  26. abstra_statics/dist/assets/{Breadcrumb.e54636d6.js → Breadcrumb.1a138bb1.js} +2 -2
  27. abstra_statics/dist/assets/{Builds.c7363e1b.js → Builds.f3c7442c.js} +2 -2
  28. abstra_statics/dist/assets/{Card.4a8a30bb.js → Card.0899e9af.js} +2 -2
  29. abstra_statics/dist/assets/{CircularLoading.d81a4cac.js → CircularLoading.9f9e733d.js} +2 -2
  30. abstra_statics/dist/assets/{CloseCircleOutlined.39b5ab06.js → CloseCircleOutlined.36d9f25b.js} +2 -2
  31. abstra_statics/dist/assets/{ConnectorsView.d4b67e2e.js → ConnectorsView.9d99a93b.js} +2 -2
  32. abstra_statics/dist/assets/{ConsoleOmniChat.vue_vue_type_script_setup_true_lang.5360224e.js → ConsoleOmniChat.vue_vue_type_script_setup_true_lang.7061772f.js} +2 -2
  33. abstra_statics/dist/assets/{ContentLayout.10f24838.js → ContentLayout.da1fff81.js} +2 -2
  34. abstra_statics/dist/assets/CrudView.5b250a71.js +2 -0
  35. abstra_statics/dist/assets/{DocsButton.vue_vue_type_script_setup_true_lang.108b18e1.js → DocsButton.vue_vue_type_script_setup_true_lang.88300c0e.js} +2 -2
  36. abstra_statics/dist/assets/{EditorLogin.2f00deb7.js → EditorLogin.440d3dc5.js} +2 -2
  37. abstra_statics/dist/assets/{EditorsView.eb87a2d8.js → EditorsView.c81d5c0a.js} +2 -2
  38. abstra_statics/dist/assets/EnvVars.688f662d.js +2 -0
  39. abstra_statics/dist/assets/{Error.98b8036c.js → Error.8fd45945.js} +2 -2
  40. abstra_statics/dist/assets/ExclamationCircleOutlined.ea15dcd1.js +2 -0
  41. abstra_statics/dist/assets/{Files.9fc8199a.js → Files.2ef1fd75.js} +2 -2
  42. abstra_statics/dist/assets/Form.4047a0fe.js +2 -0
  43. abstra_statics/dist/assets/Form.7d1b0423.css +1 -0
  44. abstra_statics/dist/assets/FormRunner.8d0c448a.js +2 -0
  45. abstra_statics/dist/assets/Home.586b0b6c.js +2 -0
  46. abstra_statics/dist/assets/{Home.191a6dce.js → Home.c4610516.js} +2 -2
  47. abstra_statics/dist/assets/LoadingContainer.12120ff7.js +2 -0
  48. abstra_statics/dist/assets/LoadingOutlined.3c83d190.js +2 -0
  49. abstra_statics/dist/assets/{Login.edfbdaea.js → Login.2d19f80c.js} +2 -2
  50. abstra_statics/dist/assets/Login.de9c56a5.js +2 -0
  51. abstra_statics/dist/assets/{Login.vue_vue_type_script_setup_true_lang.02acef81.js → Login.vue_vue_type_script_setup_true_lang.8d4054f1.js} +2 -2
  52. abstra_statics/dist/assets/Logo.3f68eae2.js +2 -0
  53. abstra_statics/dist/assets/{Logs.4c6c0b3a.js → Logs.1f1770c9.js} +2 -2
  54. abstra_statics/dist/assets/{LogsController.a58ca42a.js → LogsController.e88bddfb.js} +2 -2
  55. abstra_statics/dist/assets/Main.a79ded11.js +2 -0
  56. abstra_statics/dist/assets/MockForm.025d99f9.css +1 -0
  57. abstra_statics/dist/assets/{MockForm.091aa4ce.js → MockForm.aa5ad3bb.js} +2 -2
  58. abstra_statics/dist/assets/Navbar.2529c5ae.js +2 -0
  59. abstra_statics/dist/assets/NewEditor.2603174c.js +8 -0
  60. abstra_statics/dist/assets/{NewEditor.f2d1c0c3.css → NewEditor.5ebf7c09.css} +1 -1
  61. abstra_statics/dist/assets/OidcLoginCallback.7ed0c484.js +2 -0
  62. abstra_statics/dist/assets/OidcLogoutCallback.7303c2ab.js +2 -0
  63. abstra_statics/dist/assets/{OmniChat.c78c1e51.js → OmniChat.3d03d97a.js} +2 -2
  64. abstra_statics/dist/assets/{OnboardingView.687780ed.js → OnboardingView.4b747af0.js} +2 -2
  65. abstra_statics/dist/assets/Organization.aab680aa.js +2 -0
  66. abstra_statics/dist/assets/{Organizations.fc123489.js → Organizations.2340795a.js} +2 -2
  67. abstra_statics/dist/assets/{PhArrowCounterClockwise.vue.6ab1b899.js → PhArrowCounterClockwise.vue.d9dd0137.js} +2 -2
  68. abstra_statics/dist/assets/{PhArrowSquareOut.vue.1cebb708.js → PhArrowSquareOut.vue.93df49c1.js} +2 -2
  69. abstra_statics/dist/assets/{PhClockCounterClockwise.vue.dae2e135.js → PhClockCounterClockwise.vue.f1f419a1.js} +2 -2
  70. abstra_statics/dist/assets/{PhCopy.vue.71703533.js → PhCopy.vue.e59eaa52.js} +2 -2
  71. abstra_statics/dist/assets/{PhCopySimple.vue.369eb629.js → PhCopySimple.vue.11467aaf.js} +2 -2
  72. abstra_statics/dist/assets/{PhCube.vue.f8549a9b.js → PhCube.vue.e29a6bae.js} +2 -2
  73. abstra_statics/dist/assets/PhDatabase.vue.e22926ed.js +2 -0
  74. abstra_statics/dist/assets/{PhDotsThreeVertical.vue.9d76c4de.js → PhDotsThreeVertical.vue.fff5caa8.js} +2 -2
  75. abstra_statics/dist/assets/PhDownloadSimple.vue.e11671c2.js +2 -0
  76. abstra_statics/dist/assets/{PhFileArrowUp.vue.406b22e3.js → PhFileArrowUp.vue.9f743e50.js} +2 -2
  77. abstra_statics/dist/assets/{PhFilePlus.vue.b180df90.js → PhFilePlus.vue.b2e51e09.js} +2 -2
  78. abstra_statics/dist/assets/{PhFolderPlus.vue.b18fd061.js → PhFolderPlus.vue.8742ea4d.js} +2 -2
  79. abstra_statics/dist/assets/{PhGear.vue.bed38929.js → PhGear.vue.1c3eb148.js} +2 -2
  80. abstra_statics/dist/assets/{PhKey.vue.6ef5fdd3.js → PhKey.vue.8702106e.js} +2 -2
  81. abstra_statics/dist/assets/{PhPencil.vue.0fc0fcc0.js → PhPencil.vue.74eafe52.js} +2 -2
  82. abstra_statics/dist/assets/{PhPencilSimple.vue.0707effd.js → PhPencilSimple.vue.87355169.js} +2 -2
  83. abstra_statics/dist/assets/{PhRocket.vue.761192f5.js → PhRocket.vue.d4a6ad6a.js} +2 -2
  84. abstra_statics/dist/assets/{PhSignOut.vue.8d8dfd96.js → PhSignOut.vue.83e5f761.js} +2 -2
  85. abstra_statics/dist/assets/{PhSparkle.vue.18ed0427.js → PhSparkle.vue.d2009d46.js} +2 -2
  86. abstra_statics/dist/assets/{PhTranslate.vue.00a17a08.js → PhTranslate.vue.bec980b1.js} +2 -2
  87. abstra_statics/dist/assets/{PhUsersThree.vue.d69f0723.js → PhUsersThree.vue.dd23f9fb.js} +2 -2
  88. abstra_statics/dist/assets/{PhWarningCircle.vue.20bfeba7.js → PhWarningCircle.vue.27414f28.js} +2 -2
  89. abstra_statics/dist/assets/{PhWebhooksLogo.vue.58a98824.js → PhWebhooksLogo.vue.ff084558.js} +2 -2
  90. abstra_statics/dist/assets/{PlayerConfigProvider.ad360920.js → PlayerConfigProvider.ca40f824.js} +2 -2
  91. abstra_statics/dist/assets/{PlayerNavbar.97e8dee9.js → PlayerNavbar.393e1a48.js} +2 -2
  92. abstra_statics/dist/assets/{Project.6c4642b5.js → Project.72b53439.js} +2 -2
  93. abstra_statics/dist/assets/{ProjectLogin.f92a038d.js → ProjectLogin.26c92806.js} +2 -2
  94. abstra_statics/dist/assets/{ProjectSettings.582746dc.js → ProjectSettings.70e7668b.js} +2 -2
  95. abstra_statics/dist/assets/{ProjectsView.a6b3674b.js → ProjectsView.83667357.js} +2 -2
  96. abstra_statics/dist/assets/{SaveButton.c3ad6e9b.js → SaveButton.56d96f71.js} +2 -2
  97. abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.a29d9bc5.js +2 -0
  98. abstra_statics/dist/assets/{Sidebar.69f9369e.js → Sidebar.fd8a9f17.js} +2 -2
  99. abstra_statics/dist/assets/{Sql.cdefe5b9.js → Sql.92e57cd8.js} +4 -4
  100. abstra_statics/dist/assets/Steps.8d0493a8.js +2 -0
  101. abstra_statics/dist/assets/{TableCard.5462c89d.js → TableCard.75d256c8.js} +2 -2
  102. abstra_statics/dist/assets/{TableEditor.fcfa13de.js → TableEditor.de62b5ae.js} +2 -2
  103. abstra_statics/dist/assets/{Tables.4ee84a7c.js → Tables.f33c00ab.js} +2 -2
  104. abstra_statics/dist/assets/{TablesDiagram.b1d1579e.js → TablesDiagram.621aac9c.js} +3 -3
  105. abstra_statics/dist/assets/{TablesTabs.vue_vue_type_script_setup_true_lang.3e5206e0.js → TablesTabs.vue_vue_type_script_setup_true_lang.db87820d.js} +2 -2
  106. abstra_statics/dist/assets/{Tasks.fd2605bd.js → Tasks.e7e8affd.js} +2 -2
  107. abstra_statics/dist/assets/{UploadOutlined.64837788.js → UploadOutlined.76665096.js} +2 -2
  108. abstra_statics/dist/assets/{View.b144c5e3.js → View.2d181255.js} +2 -2
  109. abstra_statics/dist/assets/{View.vue_vue_type_script_setup_true_lang.c79117ce.js → View.vue_vue_type_script_setup_true_lang.c02bf815.js} +2 -2
  110. abstra_statics/dist/assets/{Watermark.c0756030.js → Watermark.34db0ee5.js} +2 -2
  111. abstra_statics/dist/assets/{WebEditor.774989ad.js → WebEditor.615c5ed3.js} +2 -2
  112. abstra_statics/dist/assets/WidgetPreview.d7362a8d.js +2 -0
  113. abstra_statics/dist/assets/WorkflowViewer.0a209003.css +1 -0
  114. abstra_statics/dist/assets/WorkflowViewer.e89c0824.js +2 -0
  115. abstra_statics/dist/assets/ant-design.24becb3a.js +2 -0
  116. abstra_statics/dist/assets/{apiKey.ee792d72.js → apiKey.846016a7.js} +2 -2
  117. abstra_statics/dist/assets/asyncComputed.78dd1715.js +2 -0
  118. abstra_statics/dist/assets/{build.6e7d77b3.js → build.8774fc90.js} +2 -2
  119. abstra_statics/dist/assets/colorHelpers.d8c19ea3.js +2 -0
  120. abstra_statics/dist/assets/{console.38bda98e.js → console.984bbe98.js} +2 -2
  121. abstra_statics/dist/assets/constants.3b7395d7.js +2 -0
  122. abstra_statics/dist/assets/contracts.generated.31740563.js +2 -0
  123. abstra_statics/dist/assets/{cssMode.408206bf.js → cssMode.f4a00eca.js} +2 -2
  124. abstra_statics/dist/assets/datetime.01b86df2.js +2 -0
  125. abstra_statics/dist/assets/dayjs.26942e0e.js +2 -0
  126. abstra_statics/dist/assets/editor.a6369f16.js +2 -0
  127. abstra_statics/dist/assets/editor.main.a0763b31.js +2 -0
  128. abstra_statics/dist/assets/fetch.33e85d9b.js +2 -0
  129. abstra_statics/dist/assets/{files.1c1692f5.js → files.c547743b.js} +2 -2
  130. abstra_statics/dist/assets/{folder.1b74b12c.js → folder.57131245.js} +2 -2
  131. abstra_statics/dist/assets/{freemarker2.e62e067c.js → freemarker2.7a4cfae0.js} +2 -2
  132. abstra_statics/dist/assets/{handlebars.604fc901.js → handlebars.db4a27de.js} +2 -2
  133. abstra_statics/dist/assets/{html.c02f177e.js → html.f4b3970c.js} +2 -2
  134. abstra_statics/dist/assets/{htmlMode.64078e03.js → htmlMode.631923d5.js} +2 -2
  135. abstra_statics/dist/assets/index.0a1e5d8b.js +2 -0
  136. abstra_statics/dist/assets/{index.015caad7.js → index.12c03275.js} +2 -2
  137. abstra_statics/dist/assets/{index.b91afb03.js → index.2141f0e8.js} +2 -2
  138. abstra_statics/dist/assets/{index.82590a75.js → index.2f74579e.js} +2 -2
  139. abstra_statics/dist/assets/{index.bec0ecd0.js → index.30fbc3f5.js} +2 -2
  140. abstra_statics/dist/assets/{index.a12eba98.js → index.6f45b384.js} +5 -5
  141. abstra_statics/dist/assets/{index.2ec95eae.js → index.7f04c017.js} +2 -2
  142. abstra_statics/dist/assets/{index.82842143.js → index.8e10d0e4.js} +2 -2
  143. abstra_statics/dist/assets/{index.b72cb2b3.js → index.fb17f22c.js} +2 -2
  144. abstra_statics/dist/assets/{javascript.57026f87.js → javascript.b2197abc.js} +3 -3
  145. abstra_statics/dist/assets/{jsonMode.9b45b375.js → jsonMode.8f2810a6.js} +2 -2
  146. abstra_statics/dist/assets/{jwt-decode.c5760184.css → jwt-decode.cfe2994b.css} +1 -1
  147. abstra_statics/dist/assets/{jwt-decode.esm.3348bca5.js → jwt-decode.esm.5ee65524.js} +88 -54
  148. abstra_statics/dist/assets/{linters.903f3240.js → linters.7d520e27.js} +2 -2
  149. abstra_statics/dist/assets/{liquid.233d5164.js → liquid.d3e68b2e.js} +3 -3
  150. abstra_statics/dist/assets/{member.d878cf3f.js → member.0ebe904c.js} +2 -2
  151. abstra_statics/dist/assets/{metadata.9f7495db.js → metadata.db332d21.js} +2 -2
  152. abstra_statics/dist/assets/{omniChatStore.40ad0b1b.js → omniChatStore.cf2158f0.js} +2 -2
  153. abstra_statics/dist/assets/{organization.8f08e075.js → organization.23b0aa74.js} +2 -2
  154. abstra_statics/dist/assets/{os.8ffdbf05.js → os.e0510e90.js} +2 -2
  155. abstra_statics/dist/assets/player.78bcc85c.js +2 -0
  156. abstra_statics/dist/assets/{plotly.min.da87d61b.js → plotly.min.f771780a.js} +2 -2
  157. abstra_statics/dist/assets/polling.5339a00f.js +2 -0
  158. abstra_statics/dist/assets/{project.2483de10.js → project.afe4bf99.js} +2 -2
  159. abstra_statics/dist/assets/{python.1bdbd404.js → python.d8c220ed.js} +3 -3
  160. abstra_statics/dist/assets/{razor.be821b87.js → razor.97fa5198.js} +3 -3
  161. abstra_statics/dist/assets/{record.a108da5a.js → record.dd367e66.js} +2 -2
  162. abstra_statics/dist/assets/redirect.970a0b6b.js +2 -0
  163. abstra_statics/dist/assets/{repository.48119e01.js → repository.214607cb.js} +2 -2
  164. abstra_statics/dist/assets/{repository.353e892d.js → repository.c874615c.js} +2 -2
  165. abstra_statics/dist/assets/{repository.677ca13c.js → repository.d889eafa.js} +2 -2
  166. abstra_statics/dist/assets/{router.c6e27700.js → router.9781de48.js} +5 -5
  167. abstra_statics/dist/assets/router.e3b4de3c.js +2 -0
  168. abstra_statics/dist/assets/{string.998fa621.js → string.0d721ad6.js} +2 -2
  169. abstra_statics/dist/assets/{tables.9701f90c.js → tables.45712b3f.js} +2 -2
  170. abstra_statics/dist/assets/tasksController.538cacf5.js +4 -0
  171. abstra_statics/dist/assets/{toggleHighContrast.23d5a1ab.js → toggleHighContrast.daf44fef.js} +7 -7
  172. abstra_statics/dist/assets/{tsMode.4558d65a.js → tsMode.b785363f.js} +2 -2
  173. abstra_statics/dist/assets/{typescript.4445d2fa.js → typescript.8bb42736.js} +3 -3
  174. abstra_statics/dist/assets/url.804625c6.js +2 -0
  175. abstra_statics/dist/assets/{useCodebaseEvents.6ebbc5a2.js → useCodebaseEvents.e9e5d343.js} +2 -2
  176. abstra_statics/dist/assets/useTables.2441f2b4.js +2 -0
  177. abstra_statics/dist/assets/userStore.9eb65729.js +2 -0
  178. abstra_statics/dist/assets/uuid.bc394306.js +2 -0
  179. abstra_statics/dist/assets/{vue-flow-background.f1022925.js → vue-flow-background.a4e5e1cd.js} +2 -2
  180. abstra_statics/dist/assets/{vue-flow-core.0de753a6.js → vue-flow-core.bc9175da.js} +2 -2
  181. abstra_statics/dist/assets/{vue-quill.esm-bundler.8f4ad2b3.js → vue-quill.esm-bundler.12c58800.js} +2 -2
  182. abstra_statics/dist/assets/{workspaceStore.5d3f2aec.js → workspaceStore.18d1ed9a.js} +2 -2
  183. abstra_statics/dist/assets/{xml.8a25758b.js → xml.1dacd023.js} +3 -3
  184. abstra_statics/dist/assets/{yaml.e466330b.js → yaml.e841ac1c.js} +3 -3
  185. abstra_statics/dist/console.html +15 -15
  186. abstra_statics/dist/editor.html +14 -14
  187. abstra_statics/dist/player.html +10 -10
  188. abstra_statics/dist/assets/AbstraLogo.vue_vue_type_script_setup_true_lang.1035457c.js +0 -2
  189. abstra_statics/dist/assets/App.9ab9cabb.js +0 -2
  190. abstra_statics/dist/assets/App.vue_vue_type_style_index_0_lang.6713c9c9.js +0 -2
  191. abstra_statics/dist/assets/BaseLayout.28c01b5b.js +0 -2
  192. abstra_statics/dist/assets/Billing.f9062d88.js +0 -2
  193. abstra_statics/dist/assets/CrudView.57e8b29a.js +0 -2
  194. abstra_statics/dist/assets/EnvVars.883a4a57.js +0 -2
  195. abstra_statics/dist/assets/ExclamationCircleOutlined.2441b96e.js +0 -2
  196. abstra_statics/dist/assets/Form.5d562f15.js +0 -2
  197. abstra_statics/dist/assets/Form.7493bc0a.css +0 -1
  198. abstra_statics/dist/assets/FormRunner.2b1b3c45.js +0 -2
  199. abstra_statics/dist/assets/Home.8502aa41.js +0 -2
  200. abstra_statics/dist/assets/LoadingContainer.ac03ea28.js +0 -2
  201. abstra_statics/dist/assets/LoadingOutlined.4c40acc4.js +0 -2
  202. abstra_statics/dist/assets/Login.8bd6a07a.js +0 -2
  203. abstra_statics/dist/assets/Logo.fc8ace6c.js +0 -2
  204. abstra_statics/dist/assets/Main.e6b2d2d5.js +0 -2
  205. abstra_statics/dist/assets/MockForm.e410c2c1.css +0 -1
  206. abstra_statics/dist/assets/Navbar.24019fd6.js +0 -2
  207. abstra_statics/dist/assets/NewEditor.2b6f4ed3.js +0 -8
  208. abstra_statics/dist/assets/OidcLoginCallback.987cebba.js +0 -2
  209. abstra_statics/dist/assets/OidcLogoutCallback.6c00d878.js +0 -2
  210. abstra_statics/dist/assets/Organization.0ac1bf79.js +0 -2
  211. abstra_statics/dist/assets/PhDatabase.vue.0d3246d7.js +0 -2
  212. abstra_statics/dist/assets/PhDownloadSimple.vue.21156b6d.js +0 -2
  213. abstra_statics/dist/assets/ScrollArea.vue_vue_type_script_setup_true_lang.62178939.js +0 -2
  214. abstra_statics/dist/assets/Steps.82252fc0.js +0 -2
  215. abstra_statics/dist/assets/WidgetPreview.4fd6afc0.js +0 -2
  216. abstra_statics/dist/assets/WorkflowViewer.2666936e.js +0 -2
  217. abstra_statics/dist/assets/WorkflowViewer.3b6aee8e.css +0 -1
  218. abstra_statics/dist/assets/ant-design.b3eefa58.js +0 -2
  219. abstra_statics/dist/assets/asyncComputed.c73d027a.js +0 -2
  220. abstra_statics/dist/assets/colorHelpers.5ee17d14.js +0 -2
  221. abstra_statics/dist/assets/constants.be8ad36c.js +0 -2
  222. abstra_statics/dist/assets/contracts.generated.f01de5a3.js +0 -2
  223. abstra_statics/dist/assets/datetime.a6d58ce1.js +0 -2
  224. abstra_statics/dist/assets/dayjs.703ebc20.js +0 -2
  225. abstra_statics/dist/assets/editor.a77b56bd.js +0 -2
  226. abstra_statics/dist/assets/editor.main.a1ebf0ab.js +0 -2
  227. abstra_statics/dist/assets/fetch.cd29ef4c.js +0 -2
  228. abstra_statics/dist/assets/index.5197afb2.js +0 -2
  229. abstra_statics/dist/assets/player.7112583e.js +0 -2
  230. abstra_statics/dist/assets/polling.f547718c.js +0 -2
  231. abstra_statics/dist/assets/redirect.eedb2bf6.js +0 -2
  232. abstra_statics/dist/assets/router.c7abfb0c.js +0 -2
  233. abstra_statics/dist/assets/tasksController.5db769f7.js +0 -4
  234. abstra_statics/dist/assets/url.5d02a63f.js +0 -2
  235. abstra_statics/dist/assets/useTables.4d5edd80.js +0 -2
  236. abstra_statics/dist/assets/userStore.34b8f1eb.js +0 -2
  237. abstra_statics/dist/assets/uuid.6980e2bb.js +0 -2
  238. {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/WHEEL +0 -0
  239. {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/entry_points.txt +0 -0
  240. {abstra-3.24.3.dist-info → abstra-3.24.4.dist-info}/top_level.txt +0 -0
@@ -205,3 +205,282 @@ class ProjectTests(TestCase):
205
205
  self.assertEqual(p.get_next_stages_ids(form2.id), [])
206
206
 
207
207
  self.assertEqual(p.get_previous_stages_ids(form2.id), [form1.id])
208
+
209
+ def test_deduplicate_transitions_on_load(self):
210
+ """Test that duplicate transitions are removed when loading a project from JSON"""
211
+ # Create a project with a script that has duplicate transitions
212
+ script1 = ScriptStage(
213
+ file="script1.py",
214
+ id="script1",
215
+ is_initial=True,
216
+ title="Script 1",
217
+ workflow_position=(0, 0),
218
+ workflow_transitions=[],
219
+ )
220
+
221
+ script2 = ScriptStage(
222
+ file="script2.py",
223
+ id="script2",
224
+ is_initial=False,
225
+ title="Script 2",
226
+ workflow_position=(100, 100),
227
+ workflow_transitions=[],
228
+ )
229
+
230
+ # Add the same transition multiple times (simulating corrupted data)
231
+ duplicate_transition = WorkflowTransition(
232
+ id="transition1",
233
+ target_id=script2.id,
234
+ target_type=script2.type_name,
235
+ type="task",
236
+ task_type="test_task",
237
+ )
238
+
239
+ script1.workflow_transitions.append(duplicate_transition)
240
+ script1.workflow_transitions.append(duplicate_transition)
241
+ script1.workflow_transitions.append(duplicate_transition)
242
+
243
+ project = self.project_repository.load()
244
+ project.add_stage(script1)
245
+ project.add_stage(script2)
246
+ self.project_repository.save(project)
247
+
248
+ # Manually add duplicates to the JSON file to simulate corrupted data
249
+ import json
250
+
251
+ json_path = self.project_repository.get_file_path()
252
+ with open(json_path, "r", encoding="utf-8") as f:
253
+ data = json.load(f)
254
+
255
+ # Verify duplicates were saved (as expected from the in-memory structure)
256
+ self.assertEqual(len(data["scripts"][0]["transitions"]), 3)
257
+
258
+ # Load the project - this should deduplicate the transitions
259
+ loaded_project = self.project_repository.load()
260
+
261
+ loaded_script1 = loaded_project.get_script(script1.id)
262
+ self.assertIsNotNone(loaded_script1)
263
+ assert loaded_script1 is not None # Type narrowing for pyright
264
+
265
+ # Verify that only one transition remains after deduplication
266
+ self.assertEqual(len(loaded_script1.workflow_transitions), 1)
267
+ self.assertEqual(loaded_script1.workflow_transitions[0].id, "transition1")
268
+ self.assertEqual(loaded_script1.workflow_transitions[0].target_id, script2.id)
269
+
270
+ def test_deduplicate_multiple_unique_transitions(self):
271
+ """Test that unique transitions are preserved while duplicates are removed"""
272
+ script1 = ScriptStage(
273
+ file="script1.py",
274
+ id="script1",
275
+ is_initial=True,
276
+ title="Script 1",
277
+ workflow_position=(0, 0),
278
+ workflow_transitions=[],
279
+ )
280
+
281
+ script2 = ScriptStage(
282
+ file="script2.py",
283
+ id="script2",
284
+ is_initial=False,
285
+ title="Script 2",
286
+ workflow_position=(100, 100),
287
+ workflow_transitions=[],
288
+ )
289
+
290
+ script3 = ScriptStage(
291
+ file="script3.py",
292
+ id="script3",
293
+ is_initial=False,
294
+ title="Script 3",
295
+ workflow_position=(200, 200),
296
+ workflow_transitions=[],
297
+ )
298
+
299
+ # Create unique and duplicate transitions
300
+ transition1 = WorkflowTransition(
301
+ id="transition1",
302
+ target_id=script2.id,
303
+ target_type=script2.type_name,
304
+ type="task",
305
+ )
306
+
307
+ transition2 = WorkflowTransition(
308
+ id="transition2",
309
+ target_id=script3.id,
310
+ target_type=script3.type_name,
311
+ type="task",
312
+ )
313
+
314
+ # Add transitions: 2 unique, but transition1 is duplicated
315
+ script1.workflow_transitions.append(transition1)
316
+ script1.workflow_transitions.append(transition2)
317
+ script1.workflow_transitions.append(transition1) # duplicate
318
+
319
+ project = self.project_repository.load()
320
+ project.add_stage(script1)
321
+ project.add_stage(script2)
322
+ project.add_stage(script3)
323
+ self.project_repository.save(project)
324
+
325
+ # Load and verify deduplication
326
+ loaded_project = self.project_repository.load()
327
+ loaded_script1 = loaded_project.get_script(script1.id)
328
+
329
+ self.assertIsNotNone(loaded_script1)
330
+ assert loaded_script1 is not None # Type narrowing for pyright
331
+ self.assertEqual(len(loaded_script1.workflow_transitions), 2)
332
+
333
+ # Verify both unique transitions are present
334
+ transition_ids = [t.id for t in loaded_script1.workflow_transitions]
335
+ self.assertIn("transition1", transition_ids)
336
+ self.assertIn("transition2", transition_ids)
337
+
338
+ def test_deduplicate_transitions_with_same_source_target(self):
339
+ """Test that multiple transitions with same source→target (but different IDs) are deduplicated"""
340
+ # Create stages
341
+ script1 = ScriptStage(
342
+ file="script1.py",
343
+ id="script1",
344
+ is_initial=True,
345
+ title="Script 1",
346
+ workflow_position=(0, 0),
347
+ workflow_transitions=[],
348
+ )
349
+
350
+ script2 = ScriptStage(
351
+ file="script2.py",
352
+ id="script2",
353
+ is_initial=False,
354
+ title="Script 2",
355
+ workflow_position=(100, 100),
356
+ workflow_transitions=[],
357
+ )
358
+
359
+ # Add DIFFERENT transitions with SAME source→target
360
+ # This simulates corrupted data where multiple transitions point same direction
361
+ transition_a = WorkflowTransition(
362
+ id="trans-a",
363
+ target_id=script2.id,
364
+ target_type=script2.type_name,
365
+ type="task",
366
+ task_type="type1",
367
+ )
368
+
369
+ transition_b = WorkflowTransition(
370
+ id="trans-b",
371
+ target_id=script2.id, # SAME target as trans-a
372
+ target_type=script2.type_name,
373
+ type="task",
374
+ task_type="type2",
375
+ )
376
+
377
+ transition_c = WorkflowTransition(
378
+ id="trans-c",
379
+ target_id=script2.id, # SAME target as trans-a
380
+ target_type=script2.type_name,
381
+ type="task",
382
+ task_type="type3",
383
+ )
384
+
385
+ script1.workflow_transitions.append(transition_a)
386
+ script1.workflow_transitions.append(transition_b)
387
+ script1.workflow_transitions.append(transition_c)
388
+
389
+ project = self.project_repository.load()
390
+ project.add_stage(script1)
391
+ project.add_stage(script2)
392
+ self.project_repository.save(project)
393
+
394
+ # Manually verify JSON has all 3 transitions
395
+ import json
396
+
397
+ json_path = self.project_repository.get_file_path()
398
+ with open(json_path, "r", encoding="utf-8") as f:
399
+ data = json.load(f)
400
+
401
+ # All 3 transitions are saved
402
+ self.assertEqual(len(data["scripts"][0]["transitions"]), 3)
403
+
404
+ # Load the project - should deduplicate by source→target
405
+ loaded_project = self.project_repository.load()
406
+ loaded_script1 = loaded_project.get_script(script1.id)
407
+
408
+ self.assertIsNotNone(loaded_script1)
409
+ assert loaded_script1 is not None # Type narrowing for pyright
410
+
411
+ # Should keep only ONE transition (first occurrence)
412
+ self.assertEqual(
413
+ len(loaded_script1.workflow_transitions),
414
+ 1,
415
+ f"Expected 1 transition after deduplication, got {len(loaded_script1.workflow_transitions)}",
416
+ )
417
+
418
+ # Should be the first one (trans-a)
419
+ self.assertEqual(loaded_script1.workflow_transitions[0].id, "trans-a")
420
+ self.assertEqual(loaded_script1.workflow_transitions[0].target_id, script2.id)
421
+
422
+ def test_deduplicate_allows_bidirectional_transitions(self):
423
+ """Test that bidirectional transitions (a→b and b→a) are both preserved"""
424
+ # Create stages
425
+ script1 = ScriptStage(
426
+ file="script1.py",
427
+ id="script1",
428
+ is_initial=True,
429
+ title="Script 1",
430
+ workflow_position=(0, 0),
431
+ workflow_transitions=[],
432
+ )
433
+
434
+ script2 = ScriptStage(
435
+ file="script2.py",
436
+ id="script2",
437
+ is_initial=False,
438
+ title="Script 2",
439
+ workflow_position=(100, 100),
440
+ workflow_transitions=[],
441
+ )
442
+
443
+ # Add bidirectional transitions: script1→script2 and script2→script1
444
+ forward = WorkflowTransition(
445
+ id="forward",
446
+ target_id=script2.id,
447
+ target_type=script2.type_name,
448
+ type="task",
449
+ task_type="forward_type",
450
+ )
451
+
452
+ backward = WorkflowTransition(
453
+ id="backward",
454
+ target_id=script1.id,
455
+ target_type=script1.type_name,
456
+ type="task",
457
+ task_type="backward_type",
458
+ )
459
+
460
+ script1.workflow_transitions.append(forward)
461
+ script2.workflow_transitions.append(backward)
462
+
463
+ project = self.project_repository.load()
464
+ project.add_stage(script1)
465
+ project.add_stage(script2)
466
+ self.project_repository.save(project)
467
+
468
+ # Load and verify both directions are preserved
469
+ loaded_project = self.project_repository.load()
470
+ loaded_script1 = loaded_project.get_script(script1.id)
471
+ loaded_script2 = loaded_project.get_script(script2.id)
472
+
473
+ self.assertIsNotNone(loaded_script1)
474
+ self.assertIsNotNone(loaded_script2)
475
+ assert loaded_script1 is not None
476
+ assert loaded_script2 is not None
477
+
478
+ # script1 should have forward transition
479
+ self.assertEqual(len(loaded_script1.workflow_transitions), 1)
480
+ self.assertEqual(loaded_script1.workflow_transitions[0].id, "forward")
481
+ self.assertEqual(loaded_script1.workflow_transitions[0].target_id, script2.id)
482
+
483
+ # script2 should have backward transition
484
+ self.assertEqual(len(loaded_script2.workflow_transitions), 1)
485
+ self.assertEqual(loaded_script2.workflow_transitions[0].id, "backward")
486
+ self.assertEqual(loaded_script2.workflow_transitions[0].target_id, script1.id)
@@ -1,13 +1,131 @@
1
1
  import fnmatch
2
2
  import os
3
+ import threading
3
4
  from pathlib import Path
4
- from typing import List, Optional, Tuple
5
+ from typing import Dict, List, Optional, Tuple
5
6
 
6
7
  from abstra_internals.consts.filepaths import GITIGNORE_FILEPATH
8
+ from abstra_internals.repositories.git import create_git_repository
7
9
  from abstra_internals.settings import Settings
10
+ from abstra_internals.utils.fs_cache import get_path_cache
8
11
 
9
12
 
10
13
  class FileSystemService:
14
+ # Cache for git check-ignore results
15
+ _git_ignore_cache: Dict[Path, bool] = {}
16
+ _git_ignore_cache_lock = threading.Lock()
17
+ _git_repository = None
18
+
19
+ # Fallback cache for Python .gitignore parsing (when git unavailable)
20
+ _gitignore_cache: Dict[Path, List[str]] = {}
21
+ _gitignore_cache_lock = threading.Lock()
22
+
23
+ @staticmethod
24
+ def _get_git_repository():
25
+ """Get or create the git repository instance."""
26
+ if FileSystemService._git_repository is None:
27
+ FileSystemService._git_repository = create_git_repository(
28
+ Settings.root_path
29
+ )
30
+ return FileSystemService._git_repository
31
+
32
+ @staticmethod
33
+ def _is_git_available() -> bool:
34
+ """Check if git is available and we're in a git repository."""
35
+ git_repo = FileSystemService._get_git_repository()
36
+ return git_repo.is_git_repository()
37
+
38
+ @staticmethod
39
+ def _check_git_ignore_batch(paths: List[Path]) -> Dict[Path, bool]:
40
+ """
41
+ Check multiple paths at once using git check-ignore --stdin.
42
+ Much more efficient than checking one at a time.
43
+ """
44
+ if not FileSystemService._is_git_available():
45
+ return {path: False for path in paths}
46
+
47
+ results = {}
48
+ uncached_paths = []
49
+
50
+ # Check cache first
51
+ with FileSystemService._git_ignore_cache_lock:
52
+ for path in paths:
53
+ if path in FileSystemService._git_ignore_cache:
54
+ results[path] = FileSystemService._git_ignore_cache[path]
55
+ else:
56
+ uncached_paths.append(path)
57
+
58
+ if not uncached_paths:
59
+ return results
60
+
61
+ # Use git repository to check in batch
62
+ git_repo = FileSystemService._get_git_repository()
63
+ batch_results = git_repo.check_ignore_batch(uncached_paths)
64
+
65
+ # Update cache
66
+ with FileSystemService._git_ignore_cache_lock:
67
+ for path, is_ignored in batch_results.items():
68
+ FileSystemService._git_ignore_cache[path] = is_ignored
69
+ results[path] = is_ignored
70
+
71
+ return results
72
+
73
+ @staticmethod
74
+ def _check_git_ignore(path: Path) -> bool:
75
+ """
76
+ Use git check-ignore to determine if a path should be ignored.
77
+ Much faster than parsing .gitignore files manually.
78
+ """
79
+ # Quick cache check
80
+ if path in FileSystemService._git_ignore_cache:
81
+ return FileSystemService._git_ignore_cache[path]
82
+
83
+ # Use batch method for single path (still efficient due to caching)
84
+ result = FileSystemService._check_git_ignore_batch([path])
85
+ return result.get(path, False)
86
+
87
+ @staticmethod
88
+ def _get_gitignore_patterns(directory: Path) -> Optional[List[str]]:
89
+ """
90
+ Get cached .gitignore patterns for a directory.
91
+ Returns None if no .gitignore file exists.
92
+ """
93
+ # Quick check without lock
94
+ if directory in FileSystemService._gitignore_cache:
95
+ return FileSystemService._gitignore_cache[directory]
96
+
97
+ with FileSystemService._gitignore_cache_lock:
98
+ # Double-check after acquiring lock
99
+ if directory in FileSystemService._gitignore_cache:
100
+ return FileSystemService._gitignore_cache[directory]
101
+
102
+ ignore_file = directory / GITIGNORE_FILEPATH
103
+ patterns = None
104
+
105
+ # Check if .gitignore exists and read it
106
+ try:
107
+ if ignore_file.exists():
108
+ patterns = ignore_file.read_text().splitlines()
109
+ except (IOError, UnicodeDecodeError):
110
+ pass
111
+
112
+ # Cache the result (None if file doesn't exist)
113
+ FileSystemService._gitignore_cache[directory] = patterns if patterns else []
114
+ return FileSystemService._gitignore_cache[directory]
115
+
116
+ @staticmethod
117
+ def clear_gitignore_cache():
118
+ """Clear all gitignore-related caches. Use when .gitignore files are modified."""
119
+ with FileSystemService._git_ignore_cache_lock:
120
+ FileSystemService._git_ignore_cache.clear()
121
+
122
+ # Also clear the old pattern cache if it exists (for fallback mode)
123
+ if hasattr(FileSystemService, "_gitignore_cache"):
124
+ FileSystemService._gitignore_cache.clear()
125
+
126
+ # Clear git repository cache to avoid using stale repository instances
127
+ FileSystemService._git_repository = None
128
+
11
129
  @staticmethod
12
130
  def venv_path() -> Optional[Path]:
13
131
  str_path = os.getenv("VIRTUAL_ENV") or os.getenv("CONDA_PREFIX")
@@ -66,6 +184,99 @@ class FileSystemService:
66
184
  if not dirpath.is_dir():
67
185
  raise ValueError(f"Provided path {dirpath} is not a directory.")
68
186
 
187
+ # If using git ignore, collect all paths first and check in one batch
188
+ if use_ignore and FileSystemService._is_git_available():
189
+ return FileSystemService._list_paths_with_git(
190
+ dirpath,
191
+ include_dirs=include_dirs,
192
+ allowed_suffixes=allowed_suffixes,
193
+ recursive=recursive,
194
+ )
195
+
196
+ # Fallback to old method if git not available
197
+ return FileSystemService._list_paths_recursive(
198
+ dirpath,
199
+ include_dirs=include_dirs,
200
+ use_ignore=use_ignore,
201
+ allowed_suffixes=allowed_suffixes,
202
+ recursive=recursive,
203
+ )
204
+
205
+ @staticmethod
206
+ def _list_paths_with_git(
207
+ dirpath: Path,
208
+ *,
209
+ include_dirs: bool = True,
210
+ allowed_suffixes: Optional[List[str]] = None,
211
+ recursive: bool = True,
212
+ ) -> List[Path]:
213
+ """
214
+ Optimized version that collects all paths first, then checks git ignore in one batch.
215
+ """
216
+ all_paths = []
217
+ path_info = {} # Map path to (is_directory, should_check_suffix)
218
+
219
+ # Collect all paths recursively without checking ignore
220
+ def collect_paths(current_dir: Path):
221
+ try:
222
+ with os.scandir(current_dir) as entries:
223
+ for entry in entries:
224
+ try:
225
+ is_directory = entry.is_dir(follow_symlinks=False)
226
+ except OSError:
227
+ continue
228
+
229
+ child = Path(entry.path)
230
+ all_paths.append(child)
231
+ path_info[child] = (
232
+ is_directory,
233
+ not is_directory or include_dirs,
234
+ )
235
+
236
+ if is_directory and recursive:
237
+ collect_paths(child)
238
+ except PermissionError:
239
+ pass
240
+
241
+ collect_paths(dirpath)
242
+
243
+ # Check all paths at once with git
244
+ ignore_results = FileSystemService._check_git_ignore_batch(all_paths)
245
+
246
+ # Filter results
247
+ matches = []
248
+ if include_dirs and not dirpath.is_symlink():
249
+ matches.append(dirpath)
250
+
251
+ for path in all_paths:
252
+ # Skip ignored
253
+ if ignore_results.get(path, False):
254
+ continue
255
+
256
+ is_directory, should_include = path_info[path]
257
+
258
+ # Check suffix filter
259
+ if not is_directory and not FileSystemService._suffix_allowed(
260
+ path, allowed_suffixes
261
+ ):
262
+ continue
263
+
264
+ # Include based on type
265
+ if should_include:
266
+ matches.append(path)
267
+
268
+ return sorted(matches, key=lambda p: p.as_posix())
269
+
270
+ @staticmethod
271
+ def _list_paths_recursive(
272
+ dirpath: Path,
273
+ *,
274
+ include_dirs: bool = True,
275
+ use_ignore: bool = True,
276
+ allowed_suffixes: Optional[List[str]] = None,
277
+ recursive: bool = True,
278
+ ) -> List[Path]:
279
+ """Fallback recursive implementation when git is not available."""
69
280
  if use_ignore and (FileSystemService.is_ignored(dirpath)):
70
281
  return []
71
282
 
@@ -73,23 +284,42 @@ class FileSystemService:
73
284
  if include_dirs and not dirpath.is_symlink():
74
285
  matches.append(dirpath)
75
286
 
76
- for child in dirpath.iterdir():
77
- if use_ignore and (FileSystemService.is_ignored(child)):
78
- continue
79
- if child.is_dir() or FileSystemService._suffix_allowed(
80
- child, allowed_suffixes
81
- ):
82
- matches.append(child)
83
- if child.is_dir() and recursive:
84
- matches.extend(
85
- FileSystemService.list_paths(
86
- child,
87
- include_dirs=include_dirs,
88
- use_ignore=use_ignore,
89
- allowed_suffixes=allowed_suffixes,
90
- recursive=True,
91
- )
92
- )
287
+ try:
288
+ with os.scandir(dirpath) as entries:
289
+ for entry in entries:
290
+ try:
291
+ is_directory = entry.is_dir(follow_symlinks=False)
292
+ except OSError:
293
+ continue
294
+
295
+ child = Path(entry.path)
296
+
297
+ # Check if ignored
298
+ if use_ignore and FileSystemService._is_ignored_with_stat(
299
+ child, is_directory
300
+ ):
301
+ continue
302
+
303
+ # Check if we should include this entry
304
+ if is_directory or FileSystemService._suffix_allowed(
305
+ child, allowed_suffixes
306
+ ):
307
+ matches.append(child)
308
+
309
+ # Recurse into directories
310
+ if is_directory and recursive:
311
+ matches.extend(
312
+ FileSystemService._list_paths_recursive(
313
+ child,
314
+ include_dirs=include_dirs,
315
+ use_ignore=use_ignore,
316
+ allowed_suffixes=allowed_suffixes,
317
+ recursive=True,
318
+ )
319
+ )
320
+ except PermissionError:
321
+ pass
322
+
93
323
  return sorted(matches, key=lambda p: p.as_posix())
94
324
 
95
325
  @staticmethod
@@ -168,10 +398,60 @@ class FileSystemService:
168
398
  return True
169
399
  return any(path.suffix.lower() == suffix.lower() for suffix in allowed_suffixes)
170
400
 
401
+ @staticmethod
402
+ def _is_ignored_with_stat(path: Path, is_directory: bool):
403
+ """
404
+ Optimized version of is_ignored that accepts pre-computed is_directory flag
405
+ to avoid redundant stat calls when using os.scandir().
406
+ Uses git check-ignore when available for best performance.
407
+ """
408
+ # Try using git check-ignore first (much faster)
409
+ # Don't resolve path yet - let git handle it as-is
410
+ if FileSystemService._is_git_available():
411
+ return FileSystemService._check_git_ignore(path)
412
+
413
+ # Fallback: resolve path for Python implementation
414
+ path = get_path_cache().get_resolved_path(path)
415
+
416
+ # Fallback to Python implementation
417
+ # Use the pre-computed is_directory flag instead of calling path.is_file()
418
+ current_dir = path if is_directory else path.parent
419
+
420
+ project_root = Settings.root_path
421
+ while (
422
+ current_dir != current_dir.parent and current_dir != project_root.parent
423
+ ): # Stop at project root
424
+ # Get cached patterns for this directory
425
+ patterns = FileSystemService._get_gitignore_patterns(current_dir)
426
+
427
+ if patterns:
428
+ for pattern in patterns:
429
+ pattern = pattern.strip()
430
+ if pattern and not pattern.startswith("#"):
431
+ if FileSystemService._matches_gitignore_pattern(
432
+ path, pattern, current_dir
433
+ ):
434
+ return True
435
+
436
+ current_dir = current_dir.parent
437
+
438
+ return False
439
+
171
440
  @staticmethod
172
441
  def is_ignored(path: Path):
173
- path = path.resolve()
442
+ """
443
+ Check if a path should be ignored according to .gitignore rules.
444
+ Uses git check-ignore for performance when available, falls back to Python implementation.
445
+ """
446
+ # Try using git check-ignore first (much faster)
447
+ # Don't resolve path yet - let git handle it as-is
448
+ if FileSystemService._is_git_available():
449
+ return FileSystemService._check_git_ignore(path)
450
+
451
+ # Fallback: resolve path for Python implementation
452
+ path = get_path_cache().get_resolved_path(path)
174
453
 
454
+ # Fallback to Python implementation if git is not available
175
455
  # Look for ignore file starting from the path's directory up to the root
176
456
  # For files that don't exist, we need to check if it would be a file based on the path structure
177
457
  # If the path has a suffix or ends with a filename-like component, treat it as a file
@@ -187,19 +467,18 @@ class FileSystemService:
187
467
  while (
188
468
  current_dir != current_dir.parent and current_dir != project_root.parent
189
469
  ): # Stop at project root
190
- ignore_file = current_dir / GITIGNORE_FILEPATH
191
- if ignore_file.exists():
192
- try:
193
- patterns = ignore_file.read_text().splitlines()
194
- for pattern in patterns:
195
- pattern = pattern.strip()
196
- if pattern and not pattern.startswith("#"):
197
- if FileSystemService._matches_gitignore_pattern(
198
- path, pattern, current_dir
199
- ):
200
- return True
201
- except (IOError, UnicodeDecodeError):
202
- continue
470
+ # Get cached patterns for this directory
471
+ patterns = FileSystemService._get_gitignore_patterns(current_dir)
472
+
473
+ if patterns:
474
+ for pattern in patterns:
475
+ pattern = pattern.strip()
476
+ if pattern and not pattern.startswith("#"):
477
+ if FileSystemService._matches_gitignore_pattern(
478
+ path, pattern, current_dir
479
+ ):
480
+ return True
481
+
203
482
  current_dir = current_dir.parent
204
483
 
205
484
  return False