classifyre-cli 0.4.60__tar.gz → 0.4.62__tar.gz

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 (255) hide show
  1. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/.turbo/turbo-build.log +1 -1
  2. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/package.json +1 -1
  4. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_llm.py +100 -12
  6. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/models/generated_input.py +136 -0
  7. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/detector_pipeline.py +9 -0
  8. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_llm_runner.py +58 -1
  9. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/pipeline/test_detector_pipeline.py +38 -0
  10. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/uv.lock +103 -103
  11. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/.gitignore +0 -0
  12. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/.python-version +0 -0
  13. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/README.md +0 -0
  14. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/main.py +0 -0
  15. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/scripts/generate_models.py +0 -0
  16. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/__init__.py +0 -0
  17. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/config.py +0 -0
  18. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/__init__.py +0 -0
  19. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/base.py +0 -0
  20. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/broken_links/__init__.py +0 -0
  21. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/broken_links/detector.py +0 -0
  22. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/config.py +0 -0
  23. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/content/__init__.py +0 -0
  24. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/__init__.py +0 -0
  25. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/detector.py +0 -0
  26. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/extractor.py +0 -0
  27. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/__init__.py +0 -0
  28. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_base.py +0 -0
  29. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_factory.py +0 -0
  30. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_gliner2.py +0 -0
  31. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_image_classification.py +0 -0
  32. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_object_detection.py +0 -0
  33. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_regex.py +0 -0
  34. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/runners/_text_classification.py +0 -0
  35. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/custom/trainer.py +0 -0
  36. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/dependencies.py +0 -0
  37. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/pii/__init__.py +0 -0
  38. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/pii/detector.py +0 -0
  39. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/secrets/__init__.py +0 -0
  40. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/secrets/detector.py +0 -0
  41. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/threat/__init__.py +0 -0
  42. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/threat/code_security_detector.py +0 -0
  43. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/detectors/threat/yara_detector.py +0 -0
  44. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/file_evaluation/__init__.py +0 -0
  45. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/file_evaluation/runner.py +0 -0
  46. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/main.py +0 -0
  47. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/models/generated_detectors.py +0 -0
  48. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/models/generated_single_asset_scan_results.py +0 -0
  49. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/__init__.py +0 -0
  50. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/base.py +0 -0
  51. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/console.py +0 -0
  52. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/factory.py +0 -0
  53. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/file.py +0 -0
  54. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/outputs/rest.py +0 -0
  55. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/__init__.py +0 -0
  56. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/content_provider.py +0 -0
  57. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/parsed_content_provider.py +0 -0
  58. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/text_artifact.py +0 -0
  59. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/pipeline/worker_pool.py +0 -0
  60. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/__init__.py +0 -0
  61. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/asset_metadata.py +0 -0
  62. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/atlassian_common.py +0 -0
  63. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/azure_blob_storage/__init__.py +0 -0
  64. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/azure_blob_storage/source.py +0 -0
  65. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/base.py +0 -0
  66. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/confluence/__init__.py +0 -0
  67. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/confluence/source.py +0 -0
  68. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/databricks/__init__.py +0 -0
  69. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/databricks/source.py +0 -0
  70. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/delta_lake/__init__.py +0 -0
  71. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/delta_lake/source.py +0 -0
  72. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/dependencies.py +0 -0
  73. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/elasticsearch/__init__.py +0 -0
  74. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/elasticsearch/source.py +0 -0
  75. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/email/__init__.py +0 -0
  76. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/email/source.py +0 -0
  77. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/google_cloud_storage/__init__.py +0 -0
  78. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/google_cloud_storage/source.py +0 -0
  79. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/google_workspace/__init__.py +0 -0
  80. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/google_workspace/source.py +0 -0
  81. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/hive/__init__.py +0 -0
  82. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/hive/source.py +0 -0
  83. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/iceberg/__init__.py +0 -0
  84. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/iceberg/source.py +0 -0
  85. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/jira/__init__.py +0 -0
  86. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/jira/source.py +0 -0
  87. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/kafka/__init__.py +0 -0
  88. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/kafka/source.py +0 -0
  89. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/lakehouse_base.py +0 -0
  90. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/local_folder/__init__.py +0 -0
  91. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/local_folder/source.py +0 -0
  92. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/meilisearch/__init__.py +0 -0
  93. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/meilisearch/source.py +0 -0
  94. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/microsoft_365/__init__.py +0 -0
  95. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/microsoft_365/source.py +0 -0
  96. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mongodb/__init__.py +0 -0
  97. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mongodb/source.py +0 -0
  98. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mssql/__init__.py +0 -0
  99. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mssql/source.py +0 -0
  100. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mysql/__init__.py +0 -0
  101. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/mysql/source.py +0 -0
  102. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/neo4j/__init__.py +0 -0
  103. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/neo4j/source.py +0 -0
  104. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/notion/__init__.py +0 -0
  105. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/notion/client.py +0 -0
  106. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/notion/source.py +0 -0
  107. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/object_storage/base.py +0 -0
  108. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/opensearch/__init__.py +0 -0
  109. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/opensearch/source.py +0 -0
  110. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/oracle/__init__.py +0 -0
  111. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/oracle/source.py +0 -0
  112. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/postgresql/__init__.py +0 -0
  113. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/postgresql/source.py +0 -0
  114. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/powerbi/__init__.py +0 -0
  115. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/powerbi/source.py +0 -0
  116. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/recipe_normalizer.py +0 -0
  117. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/s3_client.py +0 -0
  118. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/s3_compatible_storage/README.md +0 -0
  119. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/s3_compatible_storage/__init__.py +0 -0
  120. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/s3_compatible_storage/source.py +0 -0
  121. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/sandbox/__init__.py +0 -0
  122. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/sandbox/source.py +0 -0
  123. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/search_engine_base.py +0 -0
  124. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/servicedesk/__init__.py +0 -0
  125. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/servicedesk/source.py +0 -0
  126. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/slack/__init__.py +0 -0
  127. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/slack/source.py +0 -0
  128. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/snowflake/__init__.py +0 -0
  129. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/snowflake/source.py +0 -0
  130. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/sqlite/__init__.py +0 -0
  131. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/sqlite/source.py +0 -0
  132. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/tableau/__init__.py +0 -0
  133. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/tableau/source.py +0 -0
  134. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/tabular_base.py +0 -0
  135. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/tabular_utils.py +0 -0
  136. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/wordpress/__init__.py +0 -0
  137. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/wordpress/source.py +0 -0
  138. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/youtube/__init__.py +0 -0
  139. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/sources/youtube/source.py +0 -0
  140. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/telemetry.py +0 -0
  141. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/__init__.py +0 -0
  142. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/archive_extraction.py +0 -0
  143. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/content_extraction.py +0 -0
  144. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/dependency_groups.py +0 -0
  145. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/embedded_images.py +0 -0
  146. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/file_metadata.py +0 -0
  147. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/file_parser.py +0 -0
  148. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/file_to_images.py +0 -0
  149. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/hashing.py +0 -0
  150. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/legacy_office.py +0 -0
  151. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/resources.py +0 -0
  152. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/transcription.py +0 -0
  153. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/uv_sync.py +0 -0
  154. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/validation.py +0 -0
  155. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/src/utils/video_processing.py +0 -0
  156. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/__init__.py +0 -0
  157. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/_lakehouse_fakes.py +0 -0
  158. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/conftest.py +0 -0
  159. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/__init__.py +0 -0
  160. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  161. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/conftest.py +0 -0
  162. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/content/__init__.py +0 -0
  163. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/__init__.py +0 -0
  164. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/conftest.py +0 -0
  165. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_gliner2_classification.py +0 -0
  166. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_invoice_extraction.py +0 -0
  167. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_pipeline_integration.py +0 -0
  168. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_regex_runner.py +0 -0
  169. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_runner_hardening.py +0 -0
  170. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/custom/test_transformer_runners.py +0 -0
  171. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/__init__.py +0 -0
  172. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/conftest.py +0 -0
  173. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/sample_invoice.pdf +0 -0
  174. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/test_pii_chunking.py +0 -0
  175. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/test_pii_detector.py +0 -0
  176. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  177. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/pii/test_pii_severity.py +0 -0
  178. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/secrets/__init__.py +0 -0
  179. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  180. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  181. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_base_detector.py +0 -0
  182. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_custom_detector_examples_runtime.py +0 -0
  183. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  184. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_detector_pipeline_types.py +0 -0
  185. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_detector_schema_examples.py +0 -0
  186. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_detector_types.py +0 -0
  187. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_phase2_detectors.py +0 -0
  188. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/test_registry.py +0 -0
  189. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/threat/__init__.py +0 -0
  190. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/threat/test_code_security_detector.py +0 -0
  191. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/detectors/threat/test_yara_detector.py +0 -0
  192. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  193. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/integration/test_wordpress_links_assets.py +0 -0
  194. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/pipeline/test_detector_outcomes.py +0 -0
  195. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/pipeline/test_worker_pool.py +0 -0
  196. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_assets_metadata_catalog.py +0 -0
  197. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_azure_blob_storage_source.py +0 -0
  198. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_base_source_attachment.py +0 -0
  199. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_base_source_sampling.py +0 -0
  200. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_config.py +0 -0
  201. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_confluence_source.py +0 -0
  202. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_custom_extractor.py +0 -0
  203. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_databricks_source.py +0 -0
  204. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_delta_lake_source.py +0 -0
  205. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_dependency_groups.py +0 -0
  206. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_elasticsearch_source.py +0 -0
  207. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_email_source.py +0 -0
  208. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_file_evaluation_runner.py +0 -0
  209. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_google_cloud_storage_source.py +0 -0
  210. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_google_workspace_source.py +0 -0
  211. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_hashing.py +0 -0
  212. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_hive_source.py +0 -0
  213. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_iceberg_source.py +0 -0
  214. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_jira_source.py +0 -0
  215. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_kafka_source.py +0 -0
  216. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_local_folder_source.py +0 -0
  217. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_main.py +0 -0
  218. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_meilisearch_source.py +0 -0
  219. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_microsoft_365_source.py +0 -0
  220. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_mongodb_source.py +0 -0
  221. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_mssql_source.py +0 -0
  222. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_mysql_source.py +0 -0
  223. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_neo4j_source.py +0 -0
  224. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_notion_source.py +0 -0
  225. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_opensearch_source.py +0 -0
  226. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_oracle_source.py +0 -0
  227. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_outputs.py +0 -0
  228. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_postgresql_source.py +0 -0
  229. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_powerbi_source.py +0 -0
  230. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_recipe_normalizer.py +0 -0
  231. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_s3_compatible_storage_source.py +0 -0
  232. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_sampling_automatic.py +0 -0
  233. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_sandbox_source.py +0 -0
  234. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_servicedesk_source.py +0 -0
  235. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_slack_source.py +0 -0
  236. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_snowflake_source.py +0 -0
  237. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_source_dependency_groups.py +0 -0
  238. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_source_strategy_conformance.py +0 -0
  239. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_sqlite_source.py +0 -0
  240. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_tableau_source.py +0 -0
  241. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_tabular_automatic_sampling.py +0 -0
  242. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_tabular_utils.py +0 -0
  243. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_uv_sync.py +0 -0
  244. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_wordpress_source.py +0 -0
  245. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_youtube_source.py +0 -0
  246. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/test_youtube_source_integration.py +0 -0
  247. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_archive_extraction.py +0 -0
  248. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_content_extraction.py +0 -0
  249. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_embedded_images.py +0 -0
  250. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_file_metadata.py +0 -0
  251. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_file_parser.py +0 -0
  252. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_file_to_images.py +0 -0
  253. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_legacy_office.py +0 -0
  254. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_transcription.py +0 -0
  255. {classifyre_cli-0.4.60 → classifyre_cli-0.4.62}/tests/utils/test_video_processing.py +0 -0
@@ -1,3 +1,3 @@
1
1
  $ uv sync
2
- Resolved 300 packages in 341ms
2
+ Resolved 300 packages in 177ms
3
3
  Checked 51 packages in 1ms
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: classifyre-cli
3
- Version: 0.4.60
3
+ Version: 0.4.62
4
4
  Summary: Classifyre CLI — scan and classify unstructured data sources
5
5
  License: MIT
6
6
  Keywords: data,ingestion,metadata,pii,secrets,unstructured
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@classifyre/cli",
3
- "version": "0.4.60",
3
+ "version": "0.4.62",
4
4
  "private": true,
5
5
  "scripts": {
6
6
  "build": "uv sync",
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "classifyre-cli"
3
- version = "0.4.60"
3
+ version = "0.4.62"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -6,6 +6,8 @@ import base64
6
6
  import json
7
7
  import logging
8
8
  import os
9
+ import random
10
+ import time
9
11
  from datetime import UTC, datetime
10
12
  from typing import Any
11
13
 
@@ -43,6 +45,23 @@ _MAX_VISION_IMAGES = 20
43
45
  # worker for litellm's ~10-minute default. Overridable per deployment.
44
46
  _COMPLETION_TIMEOUT_SECONDS = float(os.environ.get("CLASSIFYRE_LLM_TIMEOUT_SECONDS", "90"))
45
47
 
48
+ # Retry budget for transient provider failures (429 rate limits, 5xx, timeouts).
49
+ # Exponential backoff with jitter, honouring Retry-After when the provider
50
+ # sends one, so a saturated endpoint is backed off from instead of hammered.
51
+ _MAX_COMPLETION_ATTEMPTS = max(1, int(os.environ.get("CLASSIFYRE_LLM_MAX_ATTEMPTS", "4")))
52
+ _RETRY_BACKOFF_BASE_SECONDS = float(os.environ.get("CLASSIFYRE_LLM_RETRY_BASE_SECONDS", "2"))
53
+ _RETRY_BACKOFF_MAX_SECONDS = 60.0
54
+ _RETRYABLE_STATUS_CODES = {408, 429, 500, 502, 503, 504}
55
+
56
+
57
+ class LLMCompletionError(RuntimeError):
58
+ """Provider call failed after retries.
59
+
60
+ Carries only a string message so it survives pickling across the detector
61
+ worker-pool process boundary; the pipeline records the detector's outcome
62
+ as ERROR instead of a silent empty result.
63
+ """
64
+
46
65
 
47
66
  class LLMRunner(BaseRunner):
48
67
  """AI detector — sends content to a configured LLM provider for classification + extraction."""
@@ -130,18 +149,8 @@ class LLMRunner(BaseRunner):
130
149
  *,
131
150
  vision_pages: int | None = None,
132
151
  ) -> list[DetectionResult]:
133
- schema = self._schema
134
152
  try:
135
- response = self._litellm.completion(
136
- model=self._model_string(),
137
- api_key=self._runtime.api_key,
138
- api_base=self._runtime.base_url or None,
139
- temperature=schema.temperature if schema.temperature is not None else 0.0,
140
- max_tokens=self._max_tokens(),
141
- messages=messages,
142
- response_format={"type": "json_object"},
143
- timeout=_COMPLETION_TIMEOUT_SECONDS,
144
- )
153
+ response = self._complete_with_backoff(messages)
145
154
  raw = response.choices[0].message.content or "{}"
146
155
  parsed = self._parse_json(raw)
147
156
  except Exception as exc:
@@ -152,10 +161,70 @@ class LLMRunner(BaseRunner):
152
161
  exc,
153
162
  exc_info=True,
154
163
  )
155
- return []
164
+ # Propagate instead of returning [] — an empty result here is
165
+ # indistinguishable from "genuinely found nothing" and would let
166
+ # the pipeline record this detector OK on an asset it never saw.
167
+ raise LLMCompletionError(
168
+ f"LLM provider call failed for detector '{self._detector_key}' "
169
+ f"(model={self._runtime.model}): {exc}"
170
+ ) from None
156
171
 
157
172
  return self._results_from_payload(snippet, parsed, vision_pages=vision_pages)
158
173
 
174
+ def _complete_with_backoff(self, messages: list[dict[str, Any]]) -> Any:
175
+ schema = self._schema
176
+ for attempt in range(1, _MAX_COMPLETION_ATTEMPTS + 1):
177
+ try:
178
+ return self._litellm.completion(
179
+ model=self._model_string(),
180
+ api_key=self._runtime.api_key,
181
+ api_base=self._runtime.base_url or None,
182
+ temperature=schema.temperature if schema.temperature is not None else 0.0,
183
+ max_tokens=self._max_tokens(),
184
+ messages=messages,
185
+ response_format={"type": "json_object"},
186
+ timeout=_COMPLETION_TIMEOUT_SECONDS,
187
+ )
188
+ except Exception as exc:
189
+ if attempt >= _MAX_COMPLETION_ATTEMPTS or not self._is_retryable(exc):
190
+ raise
191
+ delay = min(
192
+ _RETRY_BACKOFF_MAX_SECONDS,
193
+ _RETRY_BACKOFF_BASE_SECONDS * (2 ** (attempt - 1)),
194
+ )
195
+ retry_after = _retry_after_seconds(exc)
196
+ if retry_after is not None:
197
+ delay = min(_RETRY_BACKOFF_MAX_SECONDS, max(delay, retry_after))
198
+ delay *= 0.75 + random.random() * 0.5 # ±25% jitter
199
+ logger.warning(
200
+ "llm provider transient failure (detector=%s, model=%s, "
201
+ "attempt %d/%d), retrying in %.1fs: %s",
202
+ self._detector_key,
203
+ self._runtime.model,
204
+ attempt,
205
+ _MAX_COMPLETION_ATTEMPTS,
206
+ delay,
207
+ exc,
208
+ )
209
+ time.sleep(delay)
210
+ raise LLMCompletionError("unreachable") # pragma: no cover
211
+
212
+ def _is_retryable(self, exc: Exception) -> bool:
213
+ retryable_types = tuple(
214
+ t
215
+ for t in (
216
+ getattr(self._litellm, "RateLimitError", None),
217
+ getattr(self._litellm, "Timeout", None),
218
+ getattr(self._litellm, "ServiceUnavailableError", None),
219
+ getattr(self._litellm, "InternalServerError", None),
220
+ getattr(self._litellm, "APIConnectionError", None),
221
+ )
222
+ if isinstance(t, type)
223
+ )
224
+ if retryable_types and isinstance(exc, retryable_types):
225
+ return True
226
+ return getattr(exc, "status_code", None) in _RETRYABLE_STATUS_CODES
227
+
159
228
  def _vision_enabled(self) -> bool:
160
229
  return bool(getattr(self._runtime, "supports_vision", False))
161
230
 
@@ -298,3 +367,22 @@ class LLMRunner(BaseRunner):
298
367
  @staticmethod
299
368
  def _coerce_fields(raw: Any) -> dict[str, Any]:
300
369
  return {str(k): v for k, v in raw.items()} if isinstance(raw, dict) else {}
370
+
371
+
372
+ def _retry_after_seconds(exc: Exception) -> float | None:
373
+ """Best-effort extraction of a Retry-After hint from a provider exception."""
374
+ response = getattr(exc, "response", None)
375
+ headers = getattr(response, "headers", None)
376
+ if headers is None:
377
+ return None
378
+ try:
379
+ raw = headers.get("retry-after")
380
+ except Exception:
381
+ return None
382
+ if raw is None:
383
+ return None
384
+ try:
385
+ value = float(raw)
386
+ except (TypeError, ValueError):
387
+ return None
388
+ return value if value > 0 else None
@@ -2122,6 +2122,10 @@ class CoreInput(BaseModel):
2122
2122
  )
2123
2123
  sampling: SamplingConfig
2124
2124
  resources: ResourceOverrides | None = None
2125
+ cleanup_removed_detector_findings: bool | None = Field(
2126
+ True,
2127
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2128
+ )
2125
2129
 
2126
2130
 
2127
2131
  class SlackInput(CoreInput):
@@ -2142,6 +2146,10 @@ class SlackInput(CoreInput):
2142
2146
  )
2143
2147
  sampling: SamplingConfig
2144
2148
  resources: ResourceOverrides | None = None
2149
+ cleanup_removed_detector_findings: bool | None = Field(
2150
+ True,
2151
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2152
+ )
2145
2153
 
2146
2154
 
2147
2155
  class EmailInput(CoreInput):
@@ -2160,6 +2168,10 @@ class EmailInput(CoreInput):
2160
2168
  )
2161
2169
  sampling: SamplingConfig
2162
2170
  resources: ResourceOverrides | None = None
2171
+ cleanup_removed_detector_findings: bool | None = Field(
2172
+ True,
2173
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2174
+ )
2163
2175
 
2164
2176
 
2165
2177
  class S3CompatibleStorageInput(CoreInput):
@@ -2178,6 +2190,10 @@ class S3CompatibleStorageInput(CoreInput):
2178
2190
  )
2179
2191
  sampling: SamplingConfig
2180
2192
  resources: ResourceOverrides | None = None
2193
+ cleanup_removed_detector_findings: bool | None = Field(
2194
+ True,
2195
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2196
+ )
2181
2197
 
2182
2198
 
2183
2199
  class LocalFolderInput(CoreInput):
@@ -2196,6 +2212,10 @@ class LocalFolderInput(CoreInput):
2196
2212
  )
2197
2213
  sampling: SamplingConfig
2198
2214
  resources: ResourceOverrides | None = None
2215
+ cleanup_removed_detector_findings: bool | None = Field(
2216
+ True,
2217
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2218
+ )
2199
2219
 
2200
2220
 
2201
2221
  class SandboxInput(CoreInput):
@@ -2214,6 +2234,10 @@ class SandboxInput(CoreInput):
2214
2234
  )
2215
2235
  sampling: SamplingConfig
2216
2236
  resources: ResourceOverrides | None = None
2237
+ cleanup_removed_detector_findings: bool | None = Field(
2238
+ True,
2239
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2240
+ )
2217
2241
 
2218
2242
 
2219
2243
  class AzureBlobStorageInput(CoreInput):
@@ -2232,6 +2256,10 @@ class AzureBlobStorageInput(CoreInput):
2232
2256
  )
2233
2257
  sampling: SamplingConfig
2234
2258
  resources: ResourceOverrides | None = None
2259
+ cleanup_removed_detector_findings: bool | None = Field(
2260
+ True,
2261
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2262
+ )
2235
2263
 
2236
2264
 
2237
2265
  class GoogleCloudStorageInput(CoreInput):
@@ -2250,6 +2278,10 @@ class GoogleCloudStorageInput(CoreInput):
2250
2278
  )
2251
2279
  sampling: SamplingConfig
2252
2280
  resources: ResourceOverrides | None = None
2281
+ cleanup_removed_detector_findings: bool | None = Field(
2282
+ True,
2283
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2284
+ )
2253
2285
 
2254
2286
 
2255
2287
  class WordPressInput(CoreInput):
@@ -2268,6 +2300,10 @@ class WordPressInput(CoreInput):
2268
2300
  )
2269
2301
  sampling: SamplingConfig
2270
2302
  resources: ResourceOverrides | None = None
2303
+ cleanup_removed_detector_findings: bool | None = Field(
2304
+ True,
2305
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2306
+ )
2271
2307
 
2272
2308
 
2273
2309
  class PostgreSQLInput(CoreInput):
@@ -2286,6 +2322,10 @@ class PostgreSQLInput(CoreInput):
2286
2322
  )
2287
2323
  sampling: SamplingConfig
2288
2324
  resources: ResourceOverrides | None = None
2325
+ cleanup_removed_detector_findings: bool | None = Field(
2326
+ True,
2327
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2328
+ )
2289
2329
 
2290
2330
 
2291
2331
  class MySQLInput(CoreInput):
@@ -2304,6 +2344,10 @@ class MySQLInput(CoreInput):
2304
2344
  )
2305
2345
  sampling: SamplingConfig
2306
2346
  resources: ResourceOverrides | None = None
2347
+ cleanup_removed_detector_findings: bool | None = Field(
2348
+ True,
2349
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2350
+ )
2307
2351
 
2308
2352
 
2309
2353
  class MSSQLInput(CoreInput):
@@ -2322,6 +2366,10 @@ class MSSQLInput(CoreInput):
2322
2366
  )
2323
2367
  sampling: SamplingConfig
2324
2368
  resources: ResourceOverrides | None = None
2369
+ cleanup_removed_detector_findings: bool | None = Field(
2370
+ True,
2371
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2372
+ )
2325
2373
 
2326
2374
 
2327
2375
  class OracleInput(CoreInput):
@@ -2340,6 +2388,10 @@ class OracleInput(CoreInput):
2340
2388
  )
2341
2389
  sampling: SamplingConfig
2342
2390
  resources: ResourceOverrides | None = None
2391
+ cleanup_removed_detector_findings: bool | None = Field(
2392
+ True,
2393
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2394
+ )
2343
2395
 
2344
2396
 
2345
2397
  class HiveInput(CoreInput):
@@ -2358,6 +2410,10 @@ class HiveInput(CoreInput):
2358
2410
  )
2359
2411
  sampling: SamplingConfig
2360
2412
  resources: ResourceOverrides | None = None
2413
+ cleanup_removed_detector_findings: bool | None = Field(
2414
+ True,
2415
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2416
+ )
2361
2417
 
2362
2418
 
2363
2419
  class DatabricksInput(CoreInput):
@@ -2380,6 +2436,10 @@ class DatabricksInput(CoreInput):
2380
2436
  )
2381
2437
  sampling: SamplingConfig
2382
2438
  resources: ResourceOverrides | None = None
2439
+ cleanup_removed_detector_findings: bool | None = Field(
2440
+ True,
2441
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2442
+ )
2383
2443
 
2384
2444
 
2385
2445
  class SnowflakeInput(CoreInput):
@@ -2408,6 +2468,10 @@ class SnowflakeInput(CoreInput):
2408
2468
  )
2409
2469
  sampling: SamplingConfig
2410
2470
  resources: ResourceOverrides | None = None
2471
+ cleanup_removed_detector_findings: bool | None = Field(
2472
+ True,
2473
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2474
+ )
2411
2475
 
2412
2476
 
2413
2477
  class MongoDBInput(CoreInput):
@@ -2430,6 +2494,10 @@ class MongoDBInput(CoreInput):
2430
2494
  )
2431
2495
  sampling: SamplingConfig
2432
2496
  resources: ResourceOverrides | None = None
2497
+ cleanup_removed_detector_findings: bool | None = Field(
2498
+ True,
2499
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2500
+ )
2433
2501
 
2434
2502
 
2435
2503
  class Neo4jRequired(BaseModel):
@@ -2556,6 +2624,10 @@ class Neo4jInput(CoreInput):
2556
2624
  )
2557
2625
  sampling: SamplingConfig
2558
2626
  resources: ResourceOverrides | None = None
2627
+ cleanup_removed_detector_findings: bool | None = Field(
2628
+ True,
2629
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2630
+ )
2559
2631
 
2560
2632
 
2561
2633
  class PowerBIInput(CoreInput):
@@ -2578,6 +2650,10 @@ class PowerBIInput(CoreInput):
2578
2650
  )
2579
2651
  sampling: SamplingConfig
2580
2652
  resources: ResourceOverrides | None = None
2653
+ cleanup_removed_detector_findings: bool | None = Field(
2654
+ True,
2655
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2656
+ )
2581
2657
 
2582
2658
 
2583
2659
  class TableauInput(CoreInput):
@@ -2600,6 +2676,10 @@ class TableauInput(CoreInput):
2600
2676
  )
2601
2677
  sampling: SamplingConfig
2602
2678
  resources: ResourceOverrides | None = None
2679
+ cleanup_removed_detector_findings: bool | None = Field(
2680
+ True,
2681
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
2682
+ )
2603
2683
 
2604
2684
 
2605
2685
  class ConfluenceRequired(BaseModel):
@@ -3006,6 +3086,10 @@ class ConfluenceInput(CoreInput):
3006
3086
  )
3007
3087
  sampling: SamplingConfig
3008
3088
  resources: ResourceOverrides | None = None
3089
+ cleanup_removed_detector_findings: bool | None = Field(
3090
+ True,
3091
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3092
+ )
3009
3093
 
3010
3094
 
3011
3095
  class JiraInput(CoreInput):
@@ -3024,6 +3108,10 @@ class JiraInput(CoreInput):
3024
3108
  )
3025
3109
  sampling: SamplingConfig
3026
3110
  resources: ResourceOverrides | None = None
3111
+ cleanup_removed_detector_findings: bool | None = Field(
3112
+ True,
3113
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3114
+ )
3027
3115
 
3028
3116
 
3029
3117
  class ServiceDeskInput(CoreInput):
@@ -3042,6 +3130,10 @@ class ServiceDeskInput(CoreInput):
3042
3130
  )
3043
3131
  sampling: SamplingConfig
3044
3132
  resources: ResourceOverrides | None = None
3133
+ cleanup_removed_detector_findings: bool | None = Field(
3134
+ True,
3135
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3136
+ )
3045
3137
 
3046
3138
 
3047
3139
  class SQLiteRequired(BaseModel):
@@ -3098,6 +3190,10 @@ class SQLiteInput(CoreInput):
3098
3190
  )
3099
3191
  sampling: SamplingConfig
3100
3192
  resources: ResourceOverrides | None = None
3193
+ cleanup_removed_detector_findings: bool | None = Field(
3194
+ True,
3195
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3196
+ )
3101
3197
 
3102
3198
 
3103
3199
  class NotionRequired(BaseModel):
@@ -3229,6 +3325,10 @@ class NotionInput(CoreInput):
3229
3325
  )
3230
3326
  sampling: SamplingConfig
3231
3327
  resources: ResourceOverrides | None = None
3328
+ cleanup_removed_detector_findings: bool | None = Field(
3329
+ True,
3330
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3331
+ )
3232
3332
 
3233
3333
 
3234
3334
  class KafkaSecurityProtocol(StrEnum):
@@ -3424,6 +3524,10 @@ class KafkaInput(CoreInput):
3424
3524
  )
3425
3525
  sampling: SamplingConfig
3426
3526
  resources: ResourceOverrides | None = None
3527
+ cleanup_removed_detector_findings: bool | None = Field(
3528
+ True,
3529
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3530
+ )
3427
3531
 
3428
3532
 
3429
3533
  class NoAuthentication2(BaseModel):
@@ -3554,6 +3658,10 @@ class ElasticsearchInput(CoreInput):
3554
3658
  )
3555
3659
  sampling: SamplingConfig
3556
3660
  resources: ResourceOverrides | None = None
3661
+ cleanup_removed_detector_findings: bool | None = Field(
3662
+ True,
3663
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3664
+ )
3557
3665
 
3558
3666
 
3559
3667
  class OpenSearchOptional(BaseModel):
@@ -3584,6 +3692,10 @@ class OpenSearchInput(CoreInput):
3584
3692
  )
3585
3693
  sampling: SamplingConfig
3586
3694
  resources: ResourceOverrides | None = None
3695
+ cleanup_removed_detector_findings: bool | None = Field(
3696
+ True,
3697
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3698
+ )
3587
3699
 
3588
3700
 
3589
3701
  class NoAuthentication4(BaseModel):
@@ -3672,6 +3784,10 @@ class MeilisearchInput(CoreInput):
3672
3784
  )
3673
3785
  sampling: SamplingConfig
3674
3786
  resources: ResourceOverrides | None = None
3787
+ cleanup_removed_detector_findings: bool | None = Field(
3788
+ True,
3789
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
3790
+ )
3675
3791
 
3676
3792
 
3677
3793
  class LakehouseStorageConnection(BaseModel):
@@ -3908,6 +4024,10 @@ class Microsoft365Input(CoreInput):
3908
4024
  )
3909
4025
  sampling: SamplingConfig
3910
4026
  resources: ResourceOverrides | None = None
4027
+ cleanup_removed_detector_findings: bool | None = Field(
4028
+ True,
4029
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
4030
+ )
3911
4031
 
3912
4032
 
3913
4033
  class GoogleWorkspaceRequiredServiceAccount(BaseModel):
@@ -4048,6 +4168,10 @@ class GoogleWorkspaceInput(CoreInput):
4048
4168
  )
4049
4169
  sampling: SamplingConfig
4050
4170
  resources: ResourceOverrides | None = None
4171
+ cleanup_removed_detector_findings: bool | None = Field(
4172
+ True,
4173
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
4174
+ )
4051
4175
 
4052
4176
 
4053
4177
  class YouTubeInput(CoreInput):
@@ -4066,6 +4190,10 @@ class YouTubeInput(CoreInput):
4066
4190
  )
4067
4191
  sampling: SamplingConfig
4068
4192
  resources: ResourceOverrides | None = None
4193
+ cleanup_removed_detector_findings: bool | None = Field(
4194
+ True,
4195
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
4196
+ )
4069
4197
 
4070
4198
 
4071
4199
  class DeltaLakeOptional(BaseModel):
@@ -4092,6 +4220,10 @@ class DeltaLakeInput(CoreInput):
4092
4220
  )
4093
4221
  sampling: SamplingConfig
4094
4222
  resources: ResourceOverrides | None = None
4223
+ cleanup_removed_detector_findings: bool | None = Field(
4224
+ True,
4225
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
4226
+ )
4095
4227
 
4096
4228
 
4097
4229
  class IcebergOptional(BaseModel):
@@ -4118,6 +4250,10 @@ class IcebergInput(CoreInput):
4118
4250
  )
4119
4251
  sampling: SamplingConfig
4120
4252
  resources: ResourceOverrides | None = None
4253
+ cleanup_removed_detector_findings: bool | None = Field(
4254
+ True,
4255
+ description='When enabled (default), findings produced by detectors that are no longer configured on this source (removed or disabled) are automatically resolved at the start of the next run, keeping the findings list in step with the current detector set.',
4256
+ )
4121
4257
 
4122
4258
 
4123
4259
  class SourceInput(
@@ -150,6 +150,15 @@ class DetectorPipeline:
150
150
  "application/x.asset-links",
151
151
  )
152
152
  ]
153
+ # A detector can qualify for both the text and the binary bucket — a
154
+ # vision-enabled LLM detector supports text plus image/pdf types — and
155
+ # would then run twice per asset (2x cost, 2x provider rate-limit
156
+ # pressure). Keep both passes only for IMAGE assets, where OCR text and
157
+ # pixels are complementary evidence; everywhere else (e.g. extensionless
158
+ # text files typed BINARY) the raw-bytes pass duplicates the text pass.
159
+ if text_detectors and binary_detectors and asset.asset_type != OutputAssetType.IMAGE:
160
+ text_ids = {id(d) for d in text_detectors}
161
+ binary_detectors = [d for d in binary_detectors if id(d) not in text_ids]
153
162
  # Any asset we resolved a text content type for is expected to yield
154
163
  # text, so warn whenever it yields none. This previously covered only
155
164
  # TXT/TABLE/URL — excluding exactly the types whose text is *derived*
@@ -11,7 +11,7 @@ from unittest.mock import MagicMock
11
11
 
12
12
  import pytest
13
13
 
14
- from src.detectors.custom.runners._llm import LLMRunner
14
+ from src.detectors.custom.runners._llm import LLMCompletionError, LLMRunner
15
15
  from src.models.generated_detectors import (
16
16
  LLMLabelDefinition,
17
17
  LLMOutputField,
@@ -161,6 +161,63 @@ def test_malformed_json_returns_no_findings() -> None:
161
161
  assert runner.detect(TEXT, "text/plain") == []
162
162
 
163
163
 
164
+ # ── Provider-failure handling (BUG A) ─────────────────────────────────────────
165
+
166
+
167
+ class _RateLimitError(Exception):
168
+ status_code = 429
169
+
170
+
171
+ class _AuthError(Exception):
172
+ status_code = 401
173
+
174
+
175
+ def test_provider_error_raises_instead_of_empty_result(monkeypatch) -> None:
176
+ # A dead provider must surface as an error the pipeline records, never as
177
+ # a silent zero-findings result indistinguishable from a clean scan.
178
+ monkeypatch.setattr("src.detectors.custom.runners._llm.time.sleep", lambda _s: None)
179
+ completion = MagicMock(side_effect=_RateLimitError("rate limited"))
180
+ runner = _runner(_schema(), completion)
181
+ with pytest.raises(LLMCompletionError, match="sentiment"):
182
+ runner.detect(TEXT, "text/plain")
183
+
184
+
185
+ def test_transient_provider_error_retries_then_succeeds(monkeypatch) -> None:
186
+ sleeps: list[float] = []
187
+ monkeypatch.setattr("src.detectors.custom.runners._llm.time.sleep", sleeps.append)
188
+ good = _mock_completion({"labels": [{"name": "bad", "confidence": 0.9}]})
189
+ completion = MagicMock(
190
+ side_effect=[_RateLimitError("429"), _RateLimitError("503"), good.return_value]
191
+ )
192
+ runner = _runner(_schema(), completion)
193
+
194
+ results = runner.detect(TEXT, "text/plain")
195
+
196
+ assert [r.finding_type for r in results] == ["bad"]
197
+ assert completion.call_count == 3
198
+ assert len(sleeps) == 2
199
+ assert sleeps[1] > sleeps[0] # exponential backoff
200
+
201
+
202
+ def test_non_retryable_provider_error_fails_fast(monkeypatch) -> None:
203
+ monkeypatch.setattr("src.detectors.custom.runners._llm.time.sleep", lambda _s: None)
204
+ completion = MagicMock(side_effect=_AuthError("bad key"))
205
+ runner = _runner(_schema(), completion)
206
+ with pytest.raises(LLMCompletionError):
207
+ runner.detect(TEXT, "text/plain")
208
+ assert completion.call_count == 1
209
+
210
+
211
+ def test_completion_error_is_picklable() -> None:
212
+ # The error crosses the detector worker-pool process boundary.
213
+ import pickle
214
+
215
+ err = LLMCompletionError("LLM provider call failed for detector 'x': 429")
216
+ restored = pickle.loads(pickle.dumps(err))
217
+ assert isinstance(restored, LLMCompletionError)
218
+ assert "429" in str(restored)
219
+
220
+
164
221
  # ── Vision / file input ───────────────────────────────────────────────────────
165
222
 
166
223
 
@@ -554,6 +554,44 @@ async def test_pipeline_runs_mixed_detector_on_ocr_text_and_binary_payloads() ->
554
554
  assert mixed_detector.seen_content_types == ["text/plain", "image/png"]
555
555
 
556
556
 
557
+ @pytest.mark.asyncio
558
+ async def test_pipeline_runs_mixed_detector_once_on_binary_typed_text_assets() -> None:
559
+ # A detector supporting both text and binary types (e.g. a vision LLM
560
+ # detector) must not run twice on BINARY-typed assets whose text is the
561
+ # primary evidence — the raw-bytes pass would duplicate the text pass.
562
+ file_bytes = b"plain text stored without an extension"
563
+ source = OcrBinarySource(
564
+ {"type": "DUMMY"},
565
+ content="",
566
+ binary_data=file_bytes,
567
+ binary_mime="application/octet-stream",
568
+ ocr_pages=["extracted text"],
569
+ )
570
+ mixed_detector = RecordingDetector(["text/plain", "image/png"])
571
+
572
+ pipeline = DetectorPipeline(
573
+ detectors=[mixed_detector],
574
+ source=source,
575
+ runner_id="runner-binary-mixed",
576
+ )
577
+
578
+ now = datetime.now(UTC)
579
+ binary_asset = SingleAssetScanResults(
580
+ hash="bin-1",
581
+ checksum="checksum",
582
+ name="asset-bin-1",
583
+ external_url="urn:test/bin-1",
584
+ links=[],
585
+ asset_type=AssetType.BINARY,
586
+ created_at=now,
587
+ updated_at=now,
588
+ )
589
+ [_asset] = await pipeline.process([binary_asset])
590
+
591
+ assert mixed_detector.seen == ["extracted text"]
592
+ assert mixed_detector.seen_content_types == ["text/plain"]
593
+
594
+
557
595
  @pytest.mark.asyncio
558
596
  async def test_pipeline_resolves_effective_binary_mime_from_bytes_and_filename() -> None:
559
597
  image_bytes = b"\xff\xd8\xffjpeg-data"