classifyre-cli 0.4.53__tar.gz → 0.4.55__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 (247) hide show
  1. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/.turbo/turbo-build.log +1 -1
  2. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/PKG-INFO +1 -1
  3. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/package.json +1 -1
  4. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/pyproject.toml +1 -1
  5. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_base.py +15 -9
  6. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_gliner2.py +81 -15
  7. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/pii/detector.py +38 -4
  8. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/models/generated_detectors.py +4 -0
  9. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/models/generated_single_asset_scan_results.py +65 -25
  10. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/pipeline/detector_pipeline.py +109 -7
  11. classifyre_cli-0.4.55/tests/detectors/custom/test_gliner2_classification.py +247 -0
  12. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/custom/test_invoice_extraction.py +3 -1
  13. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/custom/test_pipeline_integration.py +8 -4
  14. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/custom/test_runner_hardening.py +12 -9
  15. classifyre_cli-0.4.55/tests/detectors/pii/test_pii_chunking.py +121 -0
  16. classifyre_cli-0.4.55/tests/detectors/pii/test_pii_severity.py +96 -0
  17. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/test_custom_detector_examples_runtime.py +3 -3
  18. classifyre_cli-0.4.55/tests/pipeline/test_detector_outcomes.py +353 -0
  19. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/uv.lock +105 -105
  20. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/.gitignore +0 -0
  21. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/.python-version +0 -0
  22. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/README.md +0 -0
  23. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/main.py +0 -0
  24. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/scripts/generate_models.py +0 -0
  25. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/__init__.py +0 -0
  26. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/config.py +0 -0
  27. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/__init__.py +0 -0
  28. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/base.py +0 -0
  29. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/broken_links/__init__.py +0 -0
  30. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/broken_links/detector.py +0 -0
  31. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/config.py +0 -0
  32. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/content/__init__.py +0 -0
  33. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/__init__.py +0 -0
  34. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/detector.py +0 -0
  35. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/extractor.py +0 -0
  36. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/__init__.py +0 -0
  37. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_factory.py +0 -0
  38. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_feature_extraction.py +0 -0
  39. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_image_classification.py +0 -0
  40. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_llm.py +0 -0
  41. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_object_detection.py +0 -0
  42. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_regex.py +0 -0
  43. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/runners/_text_classification.py +0 -0
  44. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/custom/trainer.py +0 -0
  45. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/dependencies.py +0 -0
  46. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/pii/__init__.py +0 -0
  47. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/secrets/__init__.py +0 -0
  48. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/secrets/detector.py +0 -0
  49. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/threat/__init__.py +0 -0
  50. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/threat/code_security_detector.py +0 -0
  51. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/detectors/threat/yara_detector.py +0 -0
  52. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/main.py +0 -0
  53. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/models/generated_input.py +0 -0
  54. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/outputs/__init__.py +0 -0
  55. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/outputs/base.py +0 -0
  56. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/outputs/console.py +0 -0
  57. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/outputs/factory.py +0 -0
  58. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/outputs/file.py +0 -0
  59. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/outputs/rest.py +0 -0
  60. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/pipeline/__init__.py +0 -0
  61. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/pipeline/content_provider.py +0 -0
  62. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/pipeline/parsed_content_provider.py +0 -0
  63. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/pipeline/worker_pool.py +0 -0
  64. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sandbox/__init__.py +0 -0
  65. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sandbox/runner.py +0 -0
  66. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/__init__.py +0 -0
  67. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/asset_metadata.py +0 -0
  68. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/atlassian_common.py +0 -0
  69. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/azure_blob_storage/__init__.py +0 -0
  70. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/azure_blob_storage/source.py +0 -0
  71. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/base.py +0 -0
  72. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/confluence/__init__.py +0 -0
  73. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/confluence/source.py +0 -0
  74. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/databricks/__init__.py +0 -0
  75. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/databricks/source.py +0 -0
  76. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/delta_lake/__init__.py +0 -0
  77. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/delta_lake/source.py +0 -0
  78. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/dependencies.py +0 -0
  79. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/elasticsearch/__init__.py +0 -0
  80. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/elasticsearch/source.py +0 -0
  81. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/email/__init__.py +0 -0
  82. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/email/source.py +0 -0
  83. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/google_cloud_storage/__init__.py +0 -0
  84. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/google_cloud_storage/source.py +0 -0
  85. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/google_workspace/__init__.py +0 -0
  86. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/google_workspace/source.py +0 -0
  87. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/hive/__init__.py +0 -0
  88. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/hive/source.py +0 -0
  89. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/iceberg/__init__.py +0 -0
  90. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/iceberg/source.py +0 -0
  91. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/jira/__init__.py +0 -0
  92. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/jira/source.py +0 -0
  93. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/kafka/__init__.py +0 -0
  94. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/kafka/source.py +0 -0
  95. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/lakehouse_base.py +0 -0
  96. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/local_folder/__init__.py +0 -0
  97. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/local_folder/source.py +0 -0
  98. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/meilisearch/__init__.py +0 -0
  99. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/meilisearch/source.py +0 -0
  100. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/microsoft_365/__init__.py +0 -0
  101. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/microsoft_365/source.py +0 -0
  102. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/mongodb/__init__.py +0 -0
  103. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/mongodb/source.py +0 -0
  104. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/mssql/__init__.py +0 -0
  105. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/mssql/source.py +0 -0
  106. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/mysql/__init__.py +0 -0
  107. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/mysql/source.py +0 -0
  108. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/neo4j/__init__.py +0 -0
  109. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/neo4j/source.py +0 -0
  110. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/notion/__init__.py +0 -0
  111. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/notion/client.py +0 -0
  112. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/notion/source.py +0 -0
  113. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/object_storage/base.py +0 -0
  114. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/opensearch/__init__.py +0 -0
  115. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/opensearch/source.py +0 -0
  116. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/oracle/__init__.py +0 -0
  117. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/oracle/source.py +0 -0
  118. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/postgresql/__init__.py +0 -0
  119. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/postgresql/source.py +0 -0
  120. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/powerbi/__init__.py +0 -0
  121. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/powerbi/source.py +0 -0
  122. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/recipe_normalizer.py +0 -0
  123. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/s3_client.py +0 -0
  124. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/s3_compatible_storage/README.md +0 -0
  125. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/s3_compatible_storage/__init__.py +0 -0
  126. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/s3_compatible_storage/source.py +0 -0
  127. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/search_engine_base.py +0 -0
  128. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/servicedesk/__init__.py +0 -0
  129. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/servicedesk/source.py +0 -0
  130. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/slack/__init__.py +0 -0
  131. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/slack/source.py +0 -0
  132. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/snowflake/__init__.py +0 -0
  133. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/snowflake/source.py +0 -0
  134. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/sqlite/__init__.py +0 -0
  135. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/sqlite/source.py +0 -0
  136. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/tableau/__init__.py +0 -0
  137. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/tableau/source.py +0 -0
  138. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/tabular_base.py +0 -0
  139. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/tabular_utils.py +0 -0
  140. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/wordpress/__init__.py +0 -0
  141. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/wordpress/source.py +0 -0
  142. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/youtube/__init__.py +0 -0
  143. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/sources/youtube/source.py +0 -0
  144. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/telemetry.py +0 -0
  145. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/__init__.py +0 -0
  146. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/content_extraction.py +0 -0
  147. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/dependency_groups.py +0 -0
  148. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/embedded_images.py +0 -0
  149. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/file_metadata.py +0 -0
  150. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/file_parser.py +0 -0
  151. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/file_to_images.py +0 -0
  152. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/hashing.py +0 -0
  153. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/resources.py +0 -0
  154. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/transcription.py +0 -0
  155. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/uv_sync.py +0 -0
  156. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/validation.py +0 -0
  157. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/src/utils/video_processing.py +0 -0
  158. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/__init__.py +0 -0
  159. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/_lakehouse_fakes.py +0 -0
  160. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/conftest.py +0 -0
  161. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/__init__.py +0 -0
  162. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/broken_links/test_broken_links_detector.py +0 -0
  163. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/conftest.py +0 -0
  164. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/content/__init__.py +0 -0
  165. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/custom/__init__.py +0 -0
  166. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/custom/conftest.py +0 -0
  167. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/custom/test_llm_runner.py +0 -0
  168. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/custom/test_regex_runner.py +0 -0
  169. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/custom/test_transformer_runners.py +0 -0
  170. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/pii/__init__.py +0 -0
  171. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/pii/conftest.py +0 -0
  172. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/pii/sample_invoice.pdf +0 -0
  173. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/pii/test_pii_detector.py +0 -0
  174. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/pii/test_pii_detector_extended.py +0 -0
  175. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/secrets/__init__.py +0 -0
  176. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/secrets/test_secrets_detector.py +0 -0
  177. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/secrets/test_secrets_detector_extended.py +0 -0
  178. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/test_base_detector.py +0 -0
  179. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/test_detector_catalog_commercial.py +0 -0
  180. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/test_detector_pipeline_types.py +0 -0
  181. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/test_detector_schema_examples.py +0 -0
  182. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/test_detector_types.py +0 -0
  183. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/test_phase2_detectors.py +0 -0
  184. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/test_registry.py +0 -0
  185. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/threat/__init__.py +0 -0
  186. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/threat/test_code_security_detector.py +0 -0
  187. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/detectors/threat/test_yara_detector.py +0 -0
  188. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/integration/test_wordpress_broken_links_detector.py +0 -0
  189. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/integration/test_wordpress_links_assets.py +0 -0
  190. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/pipeline/test_detector_pipeline.py +0 -0
  191. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/pipeline/test_worker_pool.py +0 -0
  192. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_assets_metadata_catalog.py +0 -0
  193. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_azure_blob_storage_source.py +0 -0
  194. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_base_source_attachment.py +0 -0
  195. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_base_source_sampling.py +0 -0
  196. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_config.py +0 -0
  197. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_confluence_source.py +0 -0
  198. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_custom_extractor.py +0 -0
  199. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_databricks_source.py +0 -0
  200. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_delta_lake_source.py +0 -0
  201. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_dependency_groups.py +0 -0
  202. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_elasticsearch_source.py +0 -0
  203. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_email_source.py +0 -0
  204. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_google_cloud_storage_source.py +0 -0
  205. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_google_workspace_source.py +0 -0
  206. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_hashing.py +0 -0
  207. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_hive_source.py +0 -0
  208. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_iceberg_source.py +0 -0
  209. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_jira_source.py +0 -0
  210. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_kafka_source.py +0 -0
  211. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_local_folder_source.py +0 -0
  212. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_meilisearch_source.py +0 -0
  213. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_microsoft_365_source.py +0 -0
  214. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_mongodb_source.py +0 -0
  215. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_mssql_source.py +0 -0
  216. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_mysql_source.py +0 -0
  217. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_neo4j_source.py +0 -0
  218. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_notion_source.py +0 -0
  219. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_opensearch_source.py +0 -0
  220. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_oracle_source.py +0 -0
  221. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_outputs.py +0 -0
  222. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_postgresql_source.py +0 -0
  223. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_powerbi_source.py +0 -0
  224. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_recipe_normalizer.py +0 -0
  225. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_s3_compatible_storage_source.py +0 -0
  226. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_sampling_automatic.py +0 -0
  227. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_sandbox_runner.py +0 -0
  228. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_servicedesk_source.py +0 -0
  229. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_slack_source.py +0 -0
  230. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_snowflake_source.py +0 -0
  231. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_source_dependency_groups.py +0 -0
  232. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_source_strategy_conformance.py +0 -0
  233. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_sqlite_source.py +0 -0
  234. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_tableau_source.py +0 -0
  235. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_tabular_automatic_sampling.py +0 -0
  236. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_tabular_utils.py +0 -0
  237. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_uv_sync.py +0 -0
  238. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_wordpress_source.py +0 -0
  239. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_youtube_source.py +0 -0
  240. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/test_youtube_source_integration.py +0 -0
  241. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/utils/test_content_extraction.py +0 -0
  242. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/utils/test_embedded_images.py +0 -0
  243. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/utils/test_file_metadata.py +0 -0
  244. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/utils/test_file_parser.py +0 -0
  245. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/utils/test_file_to_images.py +0 -0
  246. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/utils/test_transcription.py +0 -0
  247. {classifyre_cli-0.4.53 → classifyre_cli-0.4.55}/tests/utils/test_video_processing.py +0 -0
@@ -1,3 +1,3 @@
1
1
  $ uv sync
2
- Resolved 292 packages in 384ms
2
+ Resolved 292 packages in 12ms
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.53
3
+ Version: 0.4.55
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.53",
3
+ "version": "0.4.55",
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.53"
3
+ version = "0.4.55"
4
4
  description = "Classifyre CLI — scan and classify unstructured data sources"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -47,10 +47,10 @@ _IMAGE_INPUT_CONTENT_TYPES = [*_IMAGE_CONTENT_TYPES, "application/pdf"]
47
47
 
48
48
  logger = logging.getLogger(__name__)
49
49
 
50
- # Cap per-label entity spans embedded in each finding's metadata. Every finding
51
- # carries a copy of the pipeline result (the API builds extraction rows from it),
52
- # so an uncapped regex with thousands of matches would blow up the scan payload
53
- # quadratically.
50
+ # Cap per-label entity spans carried through the dedicated extracted_data field.
51
+ # The API moves this into CustomDetectorExtraction and does not persist it in
52
+ # Finding.metadata. It is still repeated in the wire payload until extraction
53
+ # becomes page-scoped, so the cap prevents quadratic request growth.
54
54
  _MAX_EMBEDDED_SPANS_PER_LABEL = 25
55
55
 
56
56
 
@@ -139,6 +139,7 @@ class BaseRunner(ABC):
139
139
  matched_content: str,
140
140
  location: Location | None,
141
141
  metadata: dict[str, Any],
142
+ extracted_data: dict[str, Any] | None = None,
142
143
  ) -> DetectionResult:
143
144
  return DetectionResult(
144
145
  detector_type=DetectorType.CUSTOM,
@@ -152,14 +153,19 @@ class BaseRunner(ABC):
152
153
  custom_detector_name=self._detector_name,
153
154
  detected_at=datetime.now(UTC),
154
155
  metadata=metadata,
156
+ extracted_data=extracted_data,
155
157
  )
156
158
 
157
159
  def _result_to_findings(self, text: str, result: PipelineResult) -> list[DetectionResult]:
158
160
  findings: list[DetectionResult] = []
159
- runner_type = result.metadata.get("runner", "GLINER2")
161
+ # Every field on PipelineResult is optional, and runners return a bare
162
+ # PipelineResult() when a model fails to load — so none of these may be
163
+ # dereferenced directly. Doing so turned a handled model-load failure
164
+ # into an AttributeError from deep inside finding construction.
165
+ runner_type = (result.metadata or {}).get("runner", "GLINER2")
160
166
  pipeline_result_dump = _slim_pipeline_result(result)
161
167
 
162
- for label, spans in result.entities.items():
168
+ for label, spans in (result.entities or {}).items():
163
169
  for span in spans:
164
170
  confidence = float(span.get("confidence", 0.0))
165
171
  value = str(span.get("value", ""))
@@ -181,7 +187,6 @@ class BaseRunner(ABC):
181
187
  meta: dict[str, Any] = {
182
188
  "runner": runner_type,
183
189
  "entity_label": label,
184
- "pipeline_result": pipeline_result_dump,
185
190
  }
186
191
  if "groups" in span:
187
192
  meta["capture_groups"] = span["groups"]
@@ -195,10 +200,11 @@ class BaseRunner(ABC):
195
200
  matched_content=value,
196
201
  location=loc,
197
202
  metadata=meta,
203
+ extracted_data=pipeline_result_dump,
198
204
  )
199
205
  )
200
206
 
201
- for task, outcome in result.classification.items():
207
+ for task, outcome in (result.classification or {}).items():
202
208
  label = str(outcome.get("label", ""))
203
209
  confidence = float(outcome.get("confidence", 0.0))
204
210
  if not label:
@@ -216,8 +222,8 @@ class BaseRunner(ABC):
216
222
  "runner": runner_type,
217
223
  "task": task,
218
224
  "label": label,
219
- "pipeline_result": pipeline_result_dump,
220
225
  },
226
+ extracted_data=pipeline_result_dump,
221
227
  )
222
228
  )
223
229
 
@@ -80,23 +80,41 @@ class GLiNER2Runner(BaseRunner):
80
80
  raw_entities: dict[str, list[dict[str, object]]] = {}
81
81
  raw_classification: dict[str, dict[str, object]] = {}
82
82
 
83
- try:
84
- if entity_schema:
83
+ # Entity extraction and each classification task are isolated. They used
84
+ # to share one try/except, so a classification failure discarded the
85
+ # entities already extracted in the same run and returned an empty
86
+ # result — the whole detector went silent because of one broken task.
87
+ if entity_schema:
88
+ try:
85
89
  raw = self._extract_entities(model, text, entity_schema)
86
90
  raw_entities = _normalise_entity_output(raw, text)
91
+ except Exception as exc: # pragma: no cover - runtime specific
92
+ logger.error(
93
+ "GLiNER2 entity extraction failed for detector '%s': %s",
94
+ self._detector_key,
95
+ exc,
96
+ )
87
97
 
88
- for task_name, labels in classification_tasks.items():
98
+ for task_name, labels in classification_tasks.items():
99
+ try:
89
100
  setfit = self._get_setfit_model(task_name)
90
101
  if setfit is not None:
91
102
  raw_cls: Any = self._run_setfit(setfit, task_name, text)
92
103
  else:
93
- raw_cls = self._classify_chunked(model, text, labels)
104
+ raw_cls = self._classify_chunked(model, text, task_name, labels)
94
105
  raw_classification[task_name] = _normalise_classification_output(raw_cls)
106
+ except Exception as exc: # pragma: no cover - runtime specific
107
+ logger.error(
108
+ "GLiNER2 classification task '%s' failed for detector '%s': %s",
109
+ task_name,
110
+ self._detector_key,
111
+ exc,
112
+ )
95
113
 
96
- except Exception as exc: # pragma: no cover - runtime specific
97
- logger.error("GLiNER2 pipeline failed for detector '%s': %s", self._detector_key, exc)
98
- return PipelineResult()
99
-
114
+ # No early return for an empty result: "ran and found nothing" is a
115
+ # legitimate outcome and must still produce a well-formed result with
116
+ # metadata. Failures are reported by the logs above and by the runner's
117
+ # detector outcome, not by an anonymous empty return.
100
118
  validation = self._schema.validation or PipelineValidationConfig()
101
119
  filtered_entities = _apply_entity_validation(raw_entities, validation, self._schema)
102
120
  filtered_classification = _apply_classification_validation(raw_classification, validation)
@@ -138,15 +156,46 @@ class GLiNER2Runner(BaseRunner):
138
156
  )
139
157
  return model.extract_entities(text, entity_schema, **kwargs)
140
158
 
141
- def _classify_chunked(self, model: Any, text: str, labels: list[str]) -> dict[str, object]:
159
+ def _classify_once(self, model: Any, text: str, task_name: str, labels: list[str]) -> Any:
160
+ """Run one classification task against whichever API the runtime exposes.
161
+
162
+ gliner2>=1.3 exposes `classify_text(text, tasks: dict, ...)`, where tasks
163
+ maps a task name to its labels and the result is keyed by that name.
164
+ Mirrors the defensive probing in _extract_entities rather than assuming
165
+ a method exists.
166
+ """
167
+ classify_text = getattr(model, "classify_text", None)
168
+ if classify_text is not None:
169
+ raw = classify_text(
170
+ text,
171
+ {task_name: list(labels)},
172
+ threshold=0.0,
173
+ include_confidence=True,
174
+ )
175
+ # classify_text returns {task_name: <result>}; unwrap to the result.
176
+ if isinstance(raw, dict) and task_name in raw:
177
+ return raw[task_name]
178
+ return raw
179
+
180
+ legacy_classify = getattr(model, "classify", None)
181
+ if legacy_classify is None:
182
+ raise AttributeError(
183
+ f"GLiNER2 model exposes neither 'classify_text' nor 'classify'; "
184
+ f"cannot run classification task '{task_name}'"
185
+ )
186
+ return legacy_classify(text, labels, threshold=0.0)
187
+
188
+ def _classify_chunked(
189
+ self, model: Any, text: str, task_name: str, labels: list[str]
190
+ ) -> dict[str, object]:
142
191
  """Classify text, chunking long inputs and keeping the max-confidence label."""
143
192
  chunks = _chunk_text(text, _CLS_CHUNK_SIZE, _CLS_CHUNK_OVERLAP)[:_MAX_CLS_CHUNKS]
144
- if len(chunks) == 1:
145
- return _normalise_classification_output(model.classify(text, labels, threshold=0.0))
146
193
 
147
194
  best: dict[str, object] = {}
148
195
  for chunk in chunks:
149
- outcome = _normalise_classification_output(model.classify(chunk, labels, threshold=0.0))
196
+ outcome = _normalise_classification_output(
197
+ self._classify_once(model, chunk, task_name, labels)
198
+ )
150
199
  if outcome and float(outcome.get("confidence", 0.0)) > float(
151
200
  best.get("confidence", -1.0)
152
201
  ):
@@ -312,14 +361,31 @@ def _normalise_span(span: Any, text: str) -> dict[str, object] | None:
312
361
  return {"value": value, "confidence": round(confidence, 4), "start": start, "end": end}
313
362
 
314
363
 
364
+ def _confidence_of(item: Any) -> float:
365
+ """Read a confidence from either key the runtime may use.
366
+
367
+ gliner2's formatter emits {"label", "confidence"} for multi-label results.
368
+ Reading only "score" silently yields 0.0 for every label, which the
369
+ validation threshold then filters away — classification would appear to run
370
+ cleanly and produce nothing.
371
+ """
372
+ if not isinstance(item, dict):
373
+ return 0.0
374
+ value = item.get("confidence", item.get("score", 0.0))
375
+ try:
376
+ return float(value) # type: ignore[arg-type]
377
+ except (TypeError, ValueError):
378
+ return 0.0
379
+
380
+
315
381
  def _normalise_classification_output(raw: Any) -> dict[str, object]:
316
382
  if isinstance(raw, dict):
317
383
  label = raw.get("label", "")
318
- confidence = float(raw.get("confidence", raw.get("score", 0.0)))
384
+ confidence = _confidence_of(raw)
319
385
  elif isinstance(raw, (list, tuple)) and raw:
320
- best = max(raw, key=lambda x: x.get("score", 0.0) if isinstance(x, dict) else 0.0)
386
+ best = max(raw, key=_confidence_of)
321
387
  label = best.get("label", "") if isinstance(best, dict) else str(best)
322
- confidence = float(best.get("score", 0.0)) if isinstance(best, dict) else 1.0
388
+ confidence = _confidence_of(best) if isinstance(best, dict) else 1.0
323
389
  else:
324
390
  return {}
325
391
 
@@ -20,6 +20,19 @@ logger = logging.getLogger(__name__)
20
20
  _PRESIDIO_LOG_FILTER_INSTALLED = False
21
21
 
22
22
 
23
+ def _unwrap_int(value: Any) -> int | None:
24
+ """Unwrap a generated RootModel[int] config field to a plain int.
25
+
26
+ Numeric fields with constraints (chunk_size, chunk_overlap, max_length) are
27
+ generated as RootModel[int] wrappers. A wrapper instance is truthy and is
28
+ not an int, so `if not value` never short-circuits and any arithmetic or
29
+ comparison against it raises TypeError. Both failure modes are silent: the
30
+ detector fails per page while the run still reports success.
31
+ """
32
+ unwrapped = getattr(value, "root", value)
33
+ return unwrapped if isinstance(unwrapped, int) else None
34
+
35
+
23
36
  class _PresidioNoiseFilter(logging.Filter):
24
37
  """Suppresses noisy but harmless Presidio initialization warnings."""
25
38
 
@@ -342,7 +355,7 @@ class PIIDetector(BaseDetector):
342
355
  logger.warning("spaCy model '%s' not found; using default NLP engine", cfg_model)
343
356
  return None
344
357
 
345
- spacy_max_length: int | None = getattr(self._cfg, "max_length", None)
358
+ spacy_max_length = _unwrap_int(getattr(self._cfg, "max_length", None))
346
359
  if spacy_max_length is not None:
347
360
  nlp.max_length = spacy_max_length
348
361
  logger.debug("Set spaCy nlp.max_length = %d", spacy_max_length)
@@ -757,6 +770,22 @@ class PIIDetector(BaseDetector):
757
770
  def _get_severity_for_entity(self, entity_type: str) -> Severity:
758
771
  e = entity_type.upper()
759
772
 
773
+ # An explicit override always wins: a recognizer's severity depends on
774
+ # the corpus, not on its label. A CREDIT_CARD hit is critical in a
775
+ # payments export and noise in an OCR'd court transcript.
776
+ overrides = getattr(self._cfg, "severity_overrides", None) or {}
777
+ if isinstance(overrides, dict):
778
+ override = overrides.get(e) or overrides.get(entity_type)
779
+ if override is not None:
780
+ try:
781
+ return Severity(str(override).lower())
782
+ except ValueError:
783
+ logger.warning(
784
+ "Ignoring invalid severity override '%s' for entity %s",
785
+ override,
786
+ e,
787
+ )
788
+
760
789
  # Critical — government IDs, financial account numbers, biometric IDs
761
790
  if e in {
762
791
  "CREDIT_CARD",
@@ -807,7 +836,12 @@ class PIIDetector(BaseDetector):
807
836
  if e in {"PERSON", "LOCATION", "DATE_TIME", "NRP", "URL"}:
808
837
  return Severity.medium
809
838
 
810
- return Severity.high
839
+ # Unknown entity types — including every custom recognizer and anything
840
+ # Presidio adds upstream — default to medium rather than high. Severity
841
+ # is a claim about the label, not about evidence quality, and defaulting
842
+ # unrecognised labels to HIGH inflated the corpus's severity histogram
843
+ # without any review having happened.
844
+ return Severity.medium
811
845
 
812
846
  # ------------------------------------------------------------------
813
847
  # Public API
@@ -827,10 +861,10 @@ class PIIDetector(BaseDetector):
827
861
 
828
862
  def _chunk_text(self, text: str) -> list[tuple[str, int]]:
829
863
  """Return (chunk, offset) pairs. When chunk_size is null returns the full text at offset 0."""
830
- chunk_size: int | None = getattr(self._cfg, "chunk_size", None)
864
+ chunk_size = _unwrap_int(getattr(self._cfg, "chunk_size", None))
831
865
  if not chunk_size:
832
866
  return [(text, 0)]
833
- overlap: int = getattr(self._cfg, "chunk_overlap", None) or 0
867
+ overlap = _unwrap_int(getattr(self._cfg, "chunk_overlap", None)) or 0
834
868
  step = max(1, chunk_size - overlap)
835
869
  return [(text[i : i + chunk_size], i) for i in range(0, len(text), step)]
836
870
 
@@ -460,6 +460,10 @@ class PIIDetectorConfig(DetectorConfig):
460
460
  None,
461
461
  description='Presidio entity types to detect. When null, all supported entities are enabled. Use PIIEnabledPattern values (e.g. EMAIL_ADDRESS, US_SSN, CREDIT_CARD).',
462
462
  )
463
+ severity_overrides: dict[str, Severity] | None = Field(
464
+ None,
465
+ description='Per-entity severity overrides, keyed by Presidio entity type (e.g. {"CREDIT_CARD": "medium"}). Severity describes the label, not evidence quality: a recognizer that fires on OCR artifacts in one corpus may be genuinely critical in another. Overrides win over the built-in defaults.',
466
+ )
463
467
  language: str | None = Field(
464
468
  'en', description='BCP-47 language code for NER models (e.g. en, de, es)'
465
469
  )
@@ -87,31 +87,6 @@ class Location(BaseModel):
87
87
  end: int | None = Field(None, description='End offset (0-indexed)')
88
88
 
89
89
 
90
- class ScanStats(BaseModel):
91
- """
92
- Statistics about detector scan for an asset
93
- """
94
-
95
- scanned_at: AwareDatetime = Field(
96
- ..., description='Timestamp when the scan started'
97
- )
98
- duration_ms: int = Field(..., description='Duration of the scan in milliseconds')
99
- detectors_run: list[DetectorType] = Field(
100
- ..., description='List of detector types that were run'
101
- )
102
- content_size_bytes: int | None = Field(
103
- None, description='Size of the content that was scanned'
104
- )
105
- findings_count: int | None = Field(
106
- None, description='Total number of findings detected'
107
- )
108
- warnings: list[str] | None = Field(
109
- None,
110
- description='Non-fatal issues during scan (e.g. content truncation, empty content)',
111
- )
112
- errors: list[str] | None = Field(None, description='Detector errors during scan')
113
-
114
-
115
90
  class DetectionResult(BaseModel):
116
91
  """
117
92
  Result from detector scan
@@ -191,6 +166,71 @@ class DetectionResult(BaseModel):
191
166
  )
192
167
 
193
168
 
169
+ class Status(StrEnum):
170
+ """
171
+ OK means the detector completed; it may still have found nothing. ERROR means it raised and its result is unknown.
172
+ """
173
+
174
+ OK = 'OK'
175
+ ERROR = 'ERROR'
176
+
177
+
178
+ class DetectorOutcome(BaseModel):
179
+ """
180
+ Outcome of a single detector on a single asset. A detector that raised is reported with status ERROR: it produced no findings, but that absence is a failure rather than a clean result. Consumers must not treat an ERROR detector's silence as evidence that its previous findings are gone.
181
+ """
182
+
183
+ detector_type: DetectorType = Field(
184
+ ..., description='Detector family that was attempted'
185
+ )
186
+ custom_detector_key: str | None = Field(
187
+ None,
188
+ description='Stable key of the custom detector when detector_type is CUSTOM. Required to tell two custom detectors apart, since both report detector_type CUSTOM.',
189
+ title='Custom Detector Key',
190
+ )
191
+ status: Status = Field(
192
+ ...,
193
+ description='OK means the detector completed; it may still have found nothing. ERROR means it raised and its result is unknown.',
194
+ title='Status',
195
+ )
196
+ error: str | None = Field(
197
+ None, description='Failure detail when status is ERROR', title='Error'
198
+ )
199
+
200
+
201
+ class ScanStats(BaseModel):
202
+ """
203
+ Statistics about detector scan for an asset
204
+ """
205
+
206
+ scanned_at: AwareDatetime = Field(
207
+ ..., description='Timestamp when the scan started'
208
+ )
209
+ duration_ms: int = Field(..., description='Duration of the scan in milliseconds')
210
+ detectors_run: list[DetectorType] = Field(
211
+ ..., description='List of detector types that were run'
212
+ )
213
+ detector_outcomes: list[DetectorOutcome] | None = Field(
214
+ None,
215
+ description='Per-detector outcome for this asset. detectors_run lists what was attempted and cannot distinguish a detector that completed cleanly from one that crashed; this does.',
216
+ )
217
+ content_size_bytes: int | None = Field(
218
+ None, description='Size of the content that was scanned'
219
+ )
220
+ empty_text: bool | None = Field(
221
+ None,
222
+ description="True when an asset that should carry text yielded none. Most significant for assets whose text is derived rather than native (OCR'd images and scanned PDFs, transcribed audio/video): empty derived text means the content may have been missed entirely, not that the document is blank. Distinct from an error — nothing failed — so it is reported as coverage, not as a run failure.",
223
+ )
224
+ findings_count: int | None = Field(
225
+ None, description='Total number of findings detected'
226
+ )
227
+ warnings: list[str] | None = Field(
228
+ None,
229
+ description='Non-fatal issues during scan (e.g. content truncation, empty content)',
230
+ )
231
+ errors: list[str] | None = Field(None, description='Detector errors during scan')
232
+
233
+
194
234
  class SingleAssetScanResults(BaseModel):
195
235
  """
196
236
  Single asset scan results with detector findings