nthlayer-workers 2.0.0__tar.gz → 2.0.1__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 (189) hide show
  1. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/PKG-INFO +2 -2
  2. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/pyproject.toml +35 -9
  3. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers.egg-info/PKG-INFO +2 -2
  4. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers.egg-info/SOURCES.txt +1 -0
  5. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers.egg-info/requires.txt +1 -1
  6. nthlayer_workers-2.0.1/tests/test_dependency_declarations.py +297 -0
  7. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/README.md +0 -0
  8. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/setup.cfg +0 -0
  9. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/__init__.py +0 -0
  10. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/cli.py +0 -0
  11. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/cli_output.py +0 -0
  12. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/__init__.py +0 -0
  13. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/cli.py +0 -0
  14. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/config.py +0 -0
  15. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/correlation/__init__.py +0 -0
  16. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/correlation/changes.py +0 -0
  17. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/correlation/dedup.py +0 -0
  18. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/correlation/engine.py +0 -0
  19. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/correlation/temporal.py +0 -0
  20. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/correlation/topology.py +0 -0
  21. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/ingestion/__init__.py +0 -0
  22. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/ingestion/protocol.py +0 -0
  23. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/ingestion/severity.py +0 -0
  24. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/ingestion/webhook.py +0 -0
  25. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/notifications.py +0 -0
  26. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/prometheus.py +0 -0
  27. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/reasoning.py +0 -0
  28. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/session.py +0 -0
  29. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/snapshot/__init__.py +0 -0
  30. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/snapshot/generator.py +0 -0
  31. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/snapshot/model.py +0 -0
  32. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/snapshot/token.py +0 -0
  33. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/state.py +0 -0
  34. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/store/__init__.py +0 -0
  35. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/store/protocol.py +0 -0
  36. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/store/sqlite.py +0 -0
  37. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/summary.py +0 -0
  38. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/traces/__init__.py +0 -0
  39. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/traces/protocol.py +0 -0
  40. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/traces/tempo.py +0 -0
  41. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/traces/topology.py +0 -0
  42. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/types.py +0 -0
  43. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/verdict_chain.py +0 -0
  44. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/correlate/worker.py +0 -0
  45. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/__init__.py +0 -0
  46. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/__main__.py +0 -0
  47. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/_apply.py +0 -0
  48. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/_gh.py +0 -0
  49. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/_interactive.py +0 -0
  50. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/_interactive_app.py +0 -0
  51. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/_preview.py +0 -0
  52. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/_trigger.py +0 -0
  53. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/_yaml.py +0 -0
  54. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/cli.py +0 -0
  55. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/recommendations.py +0 -0
  56. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/retrospective.py +0 -0
  57. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/trends.py +0 -0
  58. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/learn/worker.py +0 -0
  59. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/__init__.py +0 -0
  60. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/__main__.py +0 -0
  61. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/_parsing.py +0 -0
  62. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/adapters/__init__.py +0 -0
  63. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/adapters/_util.py +0 -0
  64. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/adapters/devin.py +0 -0
  65. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/adapters/gastown.py +0 -0
  66. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/adapters/prometheus.py +0 -0
  67. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/adapters/protocol.py +0 -0
  68. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/adapters/webhook.py +0 -0
  69. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/calibration/__init__.py +0 -0
  70. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/calibration/loop.py +0 -0
  71. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/calibration/slos.py +0 -0
  72. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/calibration/verdict_calibration.py +0 -0
  73. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/cli.py +0 -0
  74. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/config.py +0 -0
  75. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/detection/__init__.py +0 -0
  76. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/detection/detector.py +0 -0
  77. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/detection/protocol.py +0 -0
  78. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/governance/__init__.py +0 -0
  79. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/governance/engine.py +0 -0
  80. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/manifest.py +0 -0
  81. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/notifications.py +0 -0
  82. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/pipeline/__init__.py +0 -0
  83. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/pipeline/evaluator.py +0 -0
  84. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/pipeline/router.py +0 -0
  85. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/store/__init__.py +0 -0
  86. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/store/protocol.py +0 -0
  87. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/store/sqlite.py +0 -0
  88. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/telemetry.py +0 -0
  89. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/tiering/__init__.py +0 -0
  90. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/tiering/classifier.py +0 -0
  91. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/tiering/promotion.py +0 -0
  92. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/trends/__init__.py +0 -0
  93. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/trends/tracker.py +0 -0
  94. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/types.py +0 -0
  95. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/measure/worker.py +0 -0
  96. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/__init__.py +0 -0
  97. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/__main__.py +0 -0
  98. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/api/__init__.py +0 -0
  99. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/assessment.py +0 -0
  100. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/cli.py +0 -0
  101. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/config.py +0 -0
  102. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/db/__init__.py +0 -0
  103. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/decision_records.py +0 -0
  104. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/__init__.py +0 -0
  105. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/discovery.py +0 -0
  106. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/providers/__init__.py +0 -0
  107. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/providers/backstage.py +0 -0
  108. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/providers/base.py +0 -0
  109. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/providers/consul.py +0 -0
  110. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/providers/etcd.py +0 -0
  111. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/providers/kubernetes.py +0 -0
  112. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/providers/prometheus.py +0 -0
  113. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/dependencies/providers/zookeeper.py +0 -0
  114. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/deployments/__init__.py +0 -0
  115. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/discovery/__init__.py +0 -0
  116. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/discovery/classifier.py +0 -0
  117. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/discovery/client.py +0 -0
  118. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/discovery/models.py +0 -0
  119. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/drift/__init__.py +0 -0
  120. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/drift/analyzer.py +0 -0
  121. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/drift/models.py +0 -0
  122. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/drift/patterns.py +0 -0
  123. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/explanation.py +0 -0
  124. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/gate/__init__.py +0 -0
  125. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/gate/conditions.py +0 -0
  126. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/gate/correlator.py +0 -0
  127. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/gate/evaluator.py +0 -0
  128. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/gate/policies.py +0 -0
  129. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/gate_adapter.py +0 -0
  130. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/incident.py +0 -0
  131. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/portfolio/__init__.py +0 -0
  132. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/portfolio/aggregator.py +0 -0
  133. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/portfolio/scorer.py +0 -0
  134. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/slo/__init__.py +0 -0
  135. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/slo/collector.py +0 -0
  136. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/slo/spec_loader.py +0 -0
  137. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/sqlite_store.py +0 -0
  138. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/store.py +0 -0
  139. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/verification/__init__.py +0 -0
  140. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/verification/exporter_guidance.py +0 -0
  141. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/verification/extractor.py +0 -0
  142. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/verification/models.py +0 -0
  143. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/verification/verifier.py +0 -0
  144. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/observe/worker.py +0 -0
  145. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/__init__.py +0 -0
  146. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/__main__.py +0 -0
  147. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/agents/__init__.py +0 -0
  148. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/agents/base.py +0 -0
  149. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/agents/communication.py +0 -0
  150. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/agents/investigation.py +0 -0
  151. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/agents/remediation.py +0 -0
  152. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/agents/response_models.py +0 -0
  153. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/agents/triage.py +0 -0
  154. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/cli.py +0 -0
  155. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/config.py +0 -0
  156. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/context_store.py +0 -0
  157. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/coordinator.py +0 -0
  158. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/metrics.py +0 -0
  159. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/notification_backends/__init__.py +0 -0
  160. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/notification_backends/ntfy_backend.py +0 -0
  161. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/notification_backends/protocol.py +0 -0
  162. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/notification_backends/slack_backend.py +0 -0
  163. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/notification_backends/stdout_backend.py +0 -0
  164. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/notifications.py +0 -0
  165. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/oncall/__init__.py +0 -0
  166. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/oncall/escalation.py +0 -0
  167. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/oncall/runner.py +0 -0
  168. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/oncall/schedule.py +0 -0
  169. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/safe_actions/__init__.py +0 -0
  170. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/safe_actions/actions.py +0 -0
  171. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/safe_actions/registry.py +0 -0
  172. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/safe_actions/webhook.py +0 -0
  173. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/server.py +0 -0
  174. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/sre/__init__.py +0 -0
  175. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/sre/brief.py +0 -0
  176. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/sre/delegation.py +0 -0
  177. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/sre/post_incident.py +0 -0
  178. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/sre/shift_report.py +0 -0
  179. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/sre/suppression.py +0 -0
  180. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/types.py +0 -0
  181. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/verdict_submission.py +0 -0
  182. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/worker.py +0 -0
  183. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/respond/worker_helpers.py +0 -0
  184. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers/runner.py +0 -0
  185. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers.egg-info/dependency_links.txt +0 -0
  186. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers.egg-info/entry_points.txt +0 -0
  187. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/src/nthlayer_workers.egg-info/top_level.txt +0 -0
  188. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/tests/test_cli_gate.py +0 -0
  189. {nthlayer_workers-2.0.0 → nthlayer_workers-2.0.1}/tests/test_runner.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nthlayer-workers
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: NthLayer workers — Tier 2 background computation (observe, measure, correlate, respond, learn)
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: nthlayer-common<3.0.0,>=2.1.2
7
+ Requires-Dist: nthlayer-common<4.0.0,>=3.0.0
8
8
  Requires-Dist: httpx>=0.27
9
9
  Requires-Dist: pyyaml>=6.0
10
10
  Requires-Dist: structlog>=24.1.0
@@ -4,24 +4,50 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "nthlayer-workers"
7
- version = "2.0.0"
7
+ version = "2.0.1"
8
8
  description = "NthLayer workers — Tier 2 background computation (observe, measure, correlate, respond, learn)"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
11
11
  dependencies = [
12
- # >=2.1.2, NOT >=2.1.0 or 2.1.1. 2.1.0 introduced foreign_yaml_reason with an
13
- # INERT body-recovery step: it tested spec.slos as a list, but v1 writes
14
- # a mapping and v2 has no spec.slos at all, so a header-stripped manifest
15
- # was dropped with parse_failures == 0 — the exact silent drop this
16
- # package uses the gate to prevent. Fixed across 2.1.1 and 2.1.2;
17
- # 2.1.1 still counted every Backstage catalog-info.yaml as a broken
18
- # manifest, which 2.1.2 corrected.
12
+ # >=3.0.0, NOT >=2.1.2. Two of the six breaks documented in
13
+ # nthlayer-common/docs/upgrading-3.0.md reach this package. Labelled rather
14
+ # than numbered, because that doc numbers its items 1-6 globally and its
15
+ # item 2 is a different break from either of these.
16
+ #
17
+ # BREAK A (that doc's item 1) — target conversion moved to the INBOUND
18
+ # manifest boundary (opensrm-ocvu), so a v2 objective target of 0.999 now
19
+ # parses to 99.9 where it used to parse to 0.999. The measure adapter
20
+ # compares SLODefinition.target against a measured value directly, so the
21
+ # two conventions disagree on the v2 path — exactly 100x for classical
22
+ # targets, while judgment maxima both complement AND scale (0.05 -> 95.0).
23
+ #
24
+ # BREAK B (that doc's item 6) — iter_manifest_files returns one manifest per
25
+ # stem (opensrm-xvwt), which retires this package's own same-stem
26
+ # duplicate handling. The remaining service-name duplicate branch in
27
+ # learn/retrospective.py is still live and is covered by
28
+ # TestDuplicateServiceAcrossDifferentStems.
29
+ #
30
+ # So the floor must admit only the convention this code is written against.
31
+ #
32
+ # The historical 2.1.2 floor is inherited and still satisfied: 2.1.0's
33
+ # foreign_yaml_reason had an INERT body-recovery step (it tested spec.slos
34
+ # as a list, but v1 writes a mapping and v2 has no such key), fixed across
35
+ # 2.1.1 and 2.1.2 — and what made it 2.1.2 specifically rather than 2.1.1
36
+ # is that 2.1.1 still counted every Backstage catalog-info.yaml as a broken
37
+ # manifest.
19
38
  #
20
39
  # CI resolves nthlayer-common from a sibling checkout, so it would not
21
40
  # notice a bad resolve. tests/observe/test_slo_spec_loader.py's
22
41
  # TestSharedGateResolvesCorrectly exists for that: it uses the shapes the
23
42
  # PARSERS produce and fails against 2.1.0.
24
- "nthlayer-common>=2.1.2,<3.0.0",
43
+ #
44
+ # tests/test_dependency_declarations.py guards THIS declaration as of
45
+ # [opensrm-ir5m] — ported from nthlayer-core once a mutation showed the
46
+ # range could be reverted to >=2.1.2,<3.0.0 with all 1958 tests still
47
+ # green. Still missing, and tracked as opensrm-p62o: the EXPECTED_MAJORS
48
+ # half, which reads the BUILT wheel's own metadata in the release
49
+ # container, where deps come from PyPI instead of the sibling checkout.
50
+ "nthlayer-common>=3.0.0,<4.0.0",
25
51
  "httpx>=0.27",
26
52
  "pyyaml>=6.0",
27
53
  "structlog>=24.1.0",
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nthlayer-workers
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Summary: NthLayer workers — Tier 2 background computation (observe, measure, correlate, respond, learn)
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: nthlayer-common<3.0.0,>=2.1.2
7
+ Requires-Dist: nthlayer-common<4.0.0,>=3.0.0
8
8
  Requires-Dist: httpx>=0.27
9
9
  Requires-Dist: pyyaml>=6.0
10
10
  Requires-Dist: structlog>=24.1.0
@@ -183,4 +183,5 @@ src/nthlayer_workers/respond/sre/post_incident.py
183
183
  src/nthlayer_workers/respond/sre/shift_report.py
184
184
  src/nthlayer_workers/respond/sre/suppression.py
185
185
  tests/test_cli_gate.py
186
+ tests/test_dependency_declarations.py
186
187
  tests/test_runner.py
@@ -1,4 +1,4 @@
1
- nthlayer-common<3.0.0,>=2.1.2
1
+ nthlayer-common<4.0.0,>=3.0.0
2
2
  httpx>=0.27
3
3
  pyyaml>=6.0
4
4
  structlog>=24.1.0
@@ -0,0 +1,297 @@
1
+ """The declared dependency ranges must admit the versions actually in use.
2
+
3
+ Reads pyproject.toml, the source of truth, and fails locally the moment a
4
+ sibling outgrows the declared range.
5
+
6
+ PORTED FROM nthlayer-core [opensrm-ir5m]. This package went without it until
7
+ nthlayer-common 3.0.0 shipped, and the gap was measured rather than supposed:
8
+ reverting this repo's declared range to `>=2.1.2,<3.0.0` — false, since 3.0.0
9
+ is installed and under test — left the full 1958-test suite GREEN, while the
10
+ identical mutation went red in all four siblings that had this file. workers is
11
+ the repo 3.0.0 actually broke, so it was the worst one to be missing it.
12
+
13
+ The OTHER half of nthlayer-core's pair is still absent here: it has a
14
+ tests/smoke/test_resolved_dependencies.py that reads the BUILT artifact's own
15
+ metadata and is decisive in the release container, where deps come from PyPI
16
+ rather than the sibling checkout. This repo's tests/release-smoke/ holds only
17
+ test_cli.py and test_imports.py. That remains opensrm-p62o — do not read this
18
+ file's presence as covering the container case.
19
+
20
+ The full account of why both exist is nthlayer-core CLAUDE.md hard rule 10
21
+ [opensrm-p3bm]. The short version: `tool.uv.sources` points nthlayer-common at
22
+ the sibling checkout, a path source REPLACES registry resolution rather than
23
+ being filtered by the version specifier, and nothing warned that the declared
24
+ range and the tested version had diverged.
25
+
26
+ The guarded set is DISCOVERED from project.dependencies, never hand-listed. A
27
+ hand-maintained roster can fall behind pyproject, and a roster that empties
28
+ turns every parametrised assertion below into skips with exit 0 — the same
29
+ class of bug this file exists to catch, one coordinate over.
30
+ test_at_least_one_sibling_is_guarded is the non-vacuity floor.
31
+ """
32
+ from __future__ import annotations
33
+
34
+ import tomllib
35
+ from collections import Counter
36
+ from importlib.metadata import version
37
+ from pathlib import Path
38
+
39
+ import pytest
40
+ from packaging.requirements import Requirement
41
+ from packaging.specifiers import SpecifierSet
42
+ from packaging.utils import canonicalize_name
43
+ from packaging.version import Version
44
+
45
+ PYPROJECT = Path(__file__).resolve().parents[1] / "pyproject.toml"
46
+
47
+ # Ecosystem siblings are the dependencies under this prefix, plus the front door
48
+ # itself: canonicalize_name("nthlayer") is "nthlayer", which does NOT start with
49
+ # "nthlayer-", so prefix matching alone would miss a real member. Discovery
50
+ # rather than a hand-listed roster means a newly added sibling is guarded the day
51
+ # it is declared. (`opensrm` is deliberately absent — it ships no Python package,
52
+ # so it can never be a dependency here.)
53
+ SIBLING_PREFIX = "nthlayer-"
54
+ FRONT_DOOR = "nthlayer"
55
+
56
+
57
+ def _is_sibling(canonical_name: str) -> bool:
58
+ return canonical_name == FRONT_DOOR or canonical_name.startswith(SIBLING_PREFIX)
59
+
60
+ # Operators that bound a range from above. "~=" is here on measurement, not on
61
+ # reasoning: packaging exposes a compatible-release specifier as the single
62
+ # operator "~=" and never decomposes it into ">=" plus "<", so a predicate
63
+ # looking only for "<" rejects `~=2.1` — which does exclude 3.0.0 and IS
64
+ # bounded. test_bounding_operators_match_packaging_behaviour pins that, because
65
+ # an earlier revision of this file asserted the decomposition in a comment and
66
+ # was wrong.
67
+ BOUNDING_OPERATORS = ("<", "<=", "==", "===", "~=")
68
+
69
+ # A version no realistic range admits. Lets the cross-check below ask a
70
+ # specifier what it actually does about the far future, instead of hardcoding
71
+ # one major and false-failing any case that lives at another.
72
+ #
73
+ # Epoch-blind by decision: "9999.0.0" is below any 1!x version, so a bounded
74
+ # epoch ceiling such as `<1!3.0` would false-fail the cross-check. Left alone
75
+ # because no sibling has ever used an epoch and there is no honest way to
76
+ # exercise the branch that would handle it.
77
+ UNBOUNDED_PROBE = "9999.0.0"
78
+
79
+
80
+ def _declared() -> list[Requirement]:
81
+ data = tomllib.loads(PYPROJECT.read_text())
82
+ return [Requirement(d) for d in data["project"]["dependencies"]]
83
+
84
+
85
+ def _siblings() -> dict[str, Requirement]:
86
+ """Canonical-name -> requirement, for every declared ecosystem sibling.
87
+
88
+ Names are canonicalised on the way in: `Requirement("nthlayer_common>=2")`
89
+ reports its name verbatim as `nthlayer_common`, so an underscore spelling
90
+ would otherwise read as a different, undeclared package.
91
+ """
92
+ return {
93
+ canonicalize_name(r.name): r
94
+ for r in _declared()
95
+ if _is_sibling(canonicalize_name(r.name))
96
+ }
97
+
98
+
99
+ # Evaluated at collection time. If it is ever empty the parametrised tests below
100
+ # skip rather than fail, which test_at_least_one_sibling_is_guarded prevents.
101
+ SIBLINGS = sorted(_siblings())
102
+
103
+
104
+ def test_at_least_one_sibling_is_guarded():
105
+ """Non-vacuity floor for every parametrised test here.
106
+
107
+ An empty parametrise list SKIPS and exits 0, so without this the file could
108
+ go quiet instead of red.
109
+
110
+ No literal skip count here on purpose. This docstring said `1 skipped` and
111
+ the module docstring said `3`, both marked "(measured)", while the real
112
+ figure was 4 — it is simply the number of SIBLINGS-parametrised tests, so it
113
+ goes stale every time one is added, and it drifted three times inside a
114
+ single review of this file. The count is not the point; going quiet is.
115
+ """
116
+ assert SIBLINGS, (
117
+ f"no dependency under '{SIBLING_PREFIX}' or named '{FRONT_DOOR}' "
118
+ f"found in {PYPROJECT.name}; "
119
+ f"every check in this file would silently skip"
120
+ )
121
+
122
+
123
+ def test_no_sibling_is_declared_twice():
124
+ """A duplicate silently last-wins, so the range checked need not be shipped.
125
+
126
+ Measured: `[nthlayer-common>=2.1.2,<3.0.0, nthlayer-common<2.0.0]` keyed by
127
+ name collapses to `<2.0.0`.
128
+ """
129
+ names = [canonicalize_name(r.name) for r in _declared()]
130
+ duplicated = sorted(n for n, count in Counter(names).items() if count > 1)
131
+ assert not duplicated, (
132
+ f"declared more than once in {PYPROJECT.name}: {duplicated}. The later "
133
+ f"entry wins silently, so the range checked here need not be the range "
134
+ f"shipped."
135
+ )
136
+
137
+
138
+ @pytest.mark.parametrize("name", SIBLINGS)
139
+ def test_declared_range_admits_the_installed_sibling(name):
140
+ """The range we publish must admit the build we test against.
141
+
142
+ Fails the moment a sibling outgrows the ceiling, in the developer's own
143
+ suite rather than at some consumer's install.
144
+ """
145
+ installed = version(name)
146
+ specifier = _siblings()[name].specifier
147
+
148
+ assert installed in specifier, (
149
+ f"{name} {installed} is installed and tested against, but "
150
+ f"project.dependencies declares '{name}{specifier}', which excludes "
151
+ f"it. Publishing this means consumers resolve a version no test here "
152
+ f"has ever run. Widen the declared range, or pin the sibling back."
153
+ )
154
+
155
+
156
+ @pytest.mark.parametrize("name", SIBLINGS)
157
+ def test_declared_floor_is_the_version_under_test(name):
158
+ """The floor must admit nothing this repo has never run.
159
+
160
+ test_declared_range_admits_the_installed_sibling checks the range does not
161
+ EXCLUDE the tested version; this checks it does not admit versions BELOW it.
162
+ Both are needed. Measured: `>=1.5.0,<3.0.0` passes every other check in this
163
+ file — the ceiling is present, 2.1.2 is inside the range, and the container
164
+ resolves the newest so the major assertion sees 2 — while letting a consumer
165
+ resolve this package against common 1.7.0. That is opensrm-p3bm restored,
166
+ with the guards reading as though it were covered.
167
+
168
+ The installed version is the only version under test, so it is the only
169
+ floor that admits nothing untested.
170
+
171
+ Consequence, intended: the floor must be deliberately bumped when the
172
+ sibling advances. That friction is the point — the declaration drifted
173
+ silently for four minor releases precisely because nothing demanded the
174
+ edit. Loosen this only on purpose, and record why. In particular do NOT
175
+ relax it to a major-or-minor comparison: `>=2.1.0` while 2.1.2 is tested is
176
+ the same hole as `>=1.5.0`, one coordinate smaller.
177
+
178
+ Where the red actually appears, which is not where you would guess:
179
+ .github/workflows/test.yml checks out rsionnach/nthlayer-common with no `ref`,
180
+ so CI builds against that repo's floating main. This test therefore goes red
181
+ when common's version-bump commit lands on ITS main — before any PyPI
182
+ release, triggered by a commit in another repository. If you are debugging
183
+ that failure from inside nthlayer-workers, the change you are looking for
184
+ is not here.
185
+ """
186
+ specifier = _siblings()[name].specifier
187
+ installed = version(name)
188
+
189
+ floors = [s.version for s in specifier if s.operator == ">="]
190
+ assert len(floors) == 1, (
191
+ f"'{name}{specifier}' declares {len(floors)} '>=' bounds; this guard "
192
+ f"reads exactly one. A pin or compatible-release form needs its own "
193
+ f"check rather than passing silently."
194
+ )
195
+ assert Version(floors[0]) == Version(installed), (
196
+ f"{name} floor is declared '>={floors[0]}' but the version installed "
197
+ f"and tested against is {installed}. Everything between them is "
198
+ f"published as supported and has never been run here — the shape of "
199
+ f"opensrm-p3bm. Confirm nthlayer-common's CHANGELOG carries no break "
200
+ f"for this repo, THEN bump the floor to {installed} — or pin the "
201
+ f"sibling back to {floors[0]}. Widening the published range to make "
202
+ f"this pass is how the bug got here."
203
+ )
204
+
205
+
206
+ @pytest.mark.parametrize("name", SIBLINGS)
207
+ def test_ceiling_excludes_the_next_major(name):
208
+ """An upper bound that EXISTS is not the same as one that bounds.
209
+
210
+ test_declared_range_has_an_upper_bound accepts any bounding operator, so
211
+ `<4.0.0` and `<9.0.0` pass every other check in this file identically while
212
+ declaring support for five majors nothing has run. (In the four siblings
213
+ that have a test_resolved_dependencies.py — under tests/smoke/ in core,
214
+ bench and override-adapter, tests/release-smoke/ in generate — their
215
+ EXPECTED_MAJORS does not close this either: it pins the INSTALLED major.
216
+ There is no EXPECTED_MAJORS in THIS repo at all; that half is
217
+ opensrm-p62o.) Measured
218
+ against the live range:
219
+
220
+ next-major probe 4.0.0 vs `<4.0.0` -> excluded (correct)
221
+ next-major probe 4.0.0 vs `<9.0.0` -> ADMITTED (the hole)
222
+
223
+ Pure specifier arithmetic, so no unreleased version need exist — the same
224
+ technique as UNBOUNDED_PROBE above, and it inherits that constant's
225
+ EPOCH-BLINDNESS, which is stated here rather than left implied: for an
226
+ installed `1!3.0.0` the probe is `4.0.0`, which even a completely unbounded
227
+ `>=1!3.0.0` already excludes, so this assertion would pass vacuously. Left
228
+ alone for the same reason UNBOUNDED_PROBE is — no sibling has ever used an
229
+ epoch — and test_declared_range_has_an_upper_bound still catches an outright
230
+ missing ceiling in that case. This is nthlayer-core CLAUDE.md hard
231
+ rule 10's "not a future major" stated as an assertion rather than prose
232
+ [opensrm-ir5m, from the opensrm-p62o review].
233
+
234
+ It deliberately does NOT assert anything about the range's upper edge
235
+ WITHIN the current major: `<3.5.0` would pass here, and whether a declared
236
+ range may stop short of the next major is a policy question rule 10 does
237
+ not answer. Left to opensrm-p62o rather than decided here.
238
+ """
239
+ specifier = _siblings()[name].specifier
240
+ next_major = Version(f"{Version(version(name)).major + 1}.0.0")
241
+
242
+ assert next_major not in specifier, (
243
+ f"'{name}{specifier}' admits {next_major}, a major this repo has never "
244
+ f"run. A ceiling that does not exclude the next major declares "
245
+ f"compatibility untested — the shape of opensrm-p3bm, one major up."
246
+ )
247
+
248
+
249
+ @pytest.mark.parametrize("name", SIBLINGS)
250
+ def test_declared_range_has_an_upper_bound(name):
251
+ """A missing ceiling is how core 1.0.0 became the resolver's escape hatch.
252
+
253
+ Without an upper bound, a future major of the sibling is silently
254
+ considered compatible, and this package becomes the one the resolver
255
+ reaches for when it needs to satisfy something incompatible — which is
256
+ exactly why `pip install nthlayer-workers==2.0.0 nthlayer-core` yielded
257
+ core 1.0.0 rather than an error.
258
+ """
259
+ specifier = _siblings()[name].specifier
260
+ assert any(s.operator in BOUNDING_OPERATORS for s in specifier), (
261
+ f"'{name}{specifier}' has no upper bound, so every future major of "
262
+ f"{name} is declared compatible without anything testing it"
263
+ )
264
+
265
+
266
+ @pytest.mark.parametrize(
267
+ ("spec", "bounded"),
268
+ [
269
+ (">=2.1.2,<3.0.0", True),
270
+ (">=2.1.2,<=2.9.9", True),
271
+ ("==2.1.2", True),
272
+ ("===2.1.2", True),
273
+ ("==2.*", True),
274
+ ("~=2.1", True),
275
+ ("~=2.1.2", True),
276
+ (">=4.0", False),
277
+ (">=2.1.2", False),
278
+ (">2.0", False),
279
+ ("!=2.0.0", False),
280
+ ("", False),
281
+ ],
282
+ )
283
+ def test_bounding_operators_match_packaging_behaviour(spec, bounded):
284
+ """BOUNDING_OPERATORS must agree with whether a range really bounds above.
285
+
286
+ Derived from what `packaging` does, not from what the operators look like
287
+ they should do. The cross-check asks each specifier about UNBOUNDED_PROBE
288
+ rather than a hardcoded next-major, so a case at any major — see `>=4.0` —
289
+ is judged on its own behaviour. Fails on drift in either direction: an
290
+ operator missing from the tuple that false-fails a bounded range, or a
291
+ spurious one that lets an unbounded range through.
292
+ """
293
+ specifier = SpecifierSet(spec)
294
+ predicate = any(s.operator in BOUNDING_OPERATORS for s in specifier)
295
+
296
+ assert predicate is bounded
297
+ assert (UNBOUNDED_PROBE in specifier) is not bounded