django-spec-agents 0.1.0__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 (259) hide show
  1. django_spec_agents-0.1.0/.github/CODEOWNERS +1 -0
  2. django_spec_agents-0.1.0/.github/SECURITY.md +9 -0
  3. django_spec_agents-0.1.0/.github/dependabot.yml +26 -0
  4. django_spec_agents-0.1.0/.github/workflows/release.yml +97 -0
  5. django_spec_agents-0.1.0/.github/workflows/tests.yml +440 -0
  6. django_spec_agents-0.1.0/.github/workflows/upstream-drift.yml +149 -0
  7. django_spec_agents-0.1.0/.gitignore +122 -0
  8. django_spec_agents-0.1.0/.pre-commit-config.yaml +80 -0
  9. django_spec_agents-0.1.0/CHANGELOG.md +77 -0
  10. django_spec_agents-0.1.0/CLAUDE.md +282 -0
  11. django_spec_agents-0.1.0/LICENSE +21 -0
  12. django_spec_agents-0.1.0/Makefile +95 -0
  13. django_spec_agents-0.1.0/PKG-INFO +158 -0
  14. django_spec_agents-0.1.0/README.md +106 -0
  15. django_spec_agents-0.1.0/django_spec_agents/__init__.py +93 -0
  16. django_spec_agents-0.1.0/django_spec_agents/agent/__init__.py +0 -0
  17. django_spec_agents-0.1.0/django_spec_agents/agent/agent_factory.py +129 -0
  18. django_spec_agents-0.1.0/django_spec_agents/agent/attachment_toolset.py +136 -0
  19. django_spec_agents-0.1.0/django_spec_agents/agent/build_model.py +55 -0
  20. django_spec_agents-0.1.0/django_spec_agents/agent/build_tool_catalog.py +90 -0
  21. django_spec_agents-0.1.0/django_spec_agents/agent/types/__init__.py +0 -0
  22. django_spec_agents-0.1.0/django_spec_agents/agent/types/agent_config.py +64 -0
  23. django_spec_agents-0.1.0/django_spec_agents/agent/types/agent_deps.py +85 -0
  24. django_spec_agents-0.1.0/django_spec_agents/agent/types/agent_factory_fn.py +32 -0
  25. django_spec_agents-0.1.0/django_spec_agents/agent/types/attachment_inline_config.py +57 -0
  26. django_spec_agents-0.1.0/django_spec_agents/contrib/__init__.py +0 -0
  27. django_spec_agents-0.1.0/django_spec_agents/contrib/store/__init__.py +18 -0
  28. django_spec_agents-0.1.0/django_spec_agents/contrib/store/apps.py +19 -0
  29. django_spec_agents-0.1.0/django_spec_agents/contrib/store/attachment_storage.py +43 -0
  30. django_spec_agents-0.1.0/django_spec_agents/contrib/store/default_attachment_store.py +189 -0
  31. django_spec_agents-0.1.0/django_spec_agents/contrib/store/default_conversation_store.py +127 -0
  32. django_spec_agents-0.1.0/django_spec_agents/contrib/store/default_memory_store.py +412 -0
  33. django_spec_agents-0.1.0/django_spec_agents/contrib/store/default_step_store.py +304 -0
  34. django_spec_agents-0.1.0/django_spec_agents/contrib/store/management/__init__.py +0 -0
  35. django_spec_agents-0.1.0/django_spec_agents/contrib/store/management/commands/__init__.py +0 -0
  36. django_spec_agents-0.1.0/django_spec_agents/contrib/store/management/commands/agent_store_backfill_hashes.py +66 -0
  37. django_spec_agents-0.1.0/django_spec_agents/contrib/store/management/commands/agent_store_prune_attachments.py +98 -0
  38. django_spec_agents-0.1.0/django_spec_agents/contrib/store/management/commands/agent_store_purge_memory.py +48 -0
  39. django_spec_agents-0.1.0/django_spec_agents/contrib/store/management/commands/agent_store_strip_inline_bytes.py +59 -0
  40. django_spec_agents-0.1.0/django_spec_agents/contrib/store/migrations/0001_initial.py +639 -0
  41. django_spec_agents-0.1.0/django_spec_agents/contrib/store/migrations/__init__.py +0 -0
  42. django_spec_agents-0.1.0/django_spec_agents/contrib/store/models.py +346 -0
  43. django_spec_agents-0.1.0/django_spec_agents/contrib/store/reconcile_conversation_attachments.py +63 -0
  44. django_spec_agents-0.1.0/django_spec_agents/contrib/store/strip_inline_binary_parts.py +60 -0
  45. django_spec_agents-0.1.0/django_spec_agents/contrib/store/types/__init__.py +5 -0
  46. django_spec_agents-0.1.0/django_spec_agents/contrib/store/types/attachment_deletion.py +24 -0
  47. django_spec_agents-0.1.0/django_spec_agents/contrib/store/utils.py +106 -0
  48. django_spec_agents-0.1.0/django_spec_agents/integrations/__init__.py +0 -0
  49. django_spec_agents-0.1.0/django_spec_agents/integrations/build_spec_capability.py +75 -0
  50. django_spec_agents-0.1.0/django_spec_agents/integrations/drf_mcp.py +462 -0
  51. django_spec_agents-0.1.0/django_spec_agents/integrations/resolve_spec_mapping.py +27 -0
  52. django_spec_agents-0.1.0/django_spec_agents/integrations/types/__init__.py +0 -0
  53. django_spec_agents-0.1.0/django_spec_agents/integrations/types/spec_source.py +31 -0
  54. django_spec_agents-0.1.0/django_spec_agents/persistence/__init__.py +0 -0
  55. django_spec_agents-0.1.0/django_spec_agents/persistence/anonymous_operation_error.py +14 -0
  56. django_spec_agents-0.1.0/django_spec_agents/persistence/django_session_conversation_store.py +108 -0
  57. django_spec_agents-0.1.0/django_spec_agents/persistence/memory_namespace.py +49 -0
  58. django_spec_agents-0.1.0/django_spec_agents/persistence/memory_namespace_for_user.py +45 -0
  59. django_spec_agents-0.1.0/django_spec_agents/persistence/model_attachment_store.py +83 -0
  60. django_spec_agents-0.1.0/django_spec_agents/persistence/model_conversation_store.py +126 -0
  61. django_spec_agents-0.1.0/django_spec_agents/persistence/null_attachment_store.py +33 -0
  62. django_spec_agents-0.1.0/django_spec_agents/persistence/null_conversation_store.py +36 -0
  63. django_spec_agents-0.1.0/django_spec_agents/persistence/scoped_conversation_store.py +112 -0
  64. django_spec_agents-0.1.0/django_spec_agents/persistence/types/__init__.py +0 -0
  65. django_spec_agents-0.1.0/django_spec_agents/persistence/types/attachment_ref.py +27 -0
  66. django_spec_agents-0.1.0/django_spec_agents/persistence/types/attachment_store.py +45 -0
  67. django_spec_agents-0.1.0/django_spec_agents/persistence/types/conversation.py +25 -0
  68. django_spec_agents-0.1.0/django_spec_agents/persistence/types/conversation_meta.py +33 -0
  69. django_spec_agents-0.1.0/django_spec_agents/persistence/types/conversation_store.py +43 -0
  70. django_spec_agents-0.1.0/django_spec_agents/persistence/types/opened_attachment.py +27 -0
  71. django_spec_agents-0.1.0/django_spec_agents/persistence/utils.py +152 -0
  72. django_spec_agents-0.1.0/django_spec_agents/policy/__init__.py +0 -0
  73. django_spec_agents-0.1.0/django_spec_agents/policy/audit/__init__.py +0 -0
  74. django_spec_agents-0.1.0/django_spec_agents/policy/audit/audit_capability.py +470 -0
  75. django_spec_agents-0.1.0/django_spec_agents/policy/audit/logging_audit_logger.py +59 -0
  76. django_spec_agents-0.1.0/django_spec_agents/policy/audit/null_audit_logger.py +13 -0
  77. django_spec_agents-0.1.0/django_spec_agents/policy/audit/types/__init__.py +0 -0
  78. django_spec_agents-0.1.0/django_spec_agents/policy/audit/types/audit_event.py +43 -0
  79. django_spec_agents-0.1.0/django_spec_agents/policy/audit/types/audit_logger.py +20 -0
  80. django_spec_agents-0.1.0/django_spec_agents/policy/failure/__init__.py +0 -0
  81. django_spec_agents-0.1.0/django_spec_agents/policy/failure/tool_failure_policy.py +164 -0
  82. django_spec_agents-0.1.0/django_spec_agents/policy/failure/types/__init__.py +0 -0
  83. django_spec_agents-0.1.0/django_spec_agents/policy/failure/types/tool_failure_config.py +63 -0
  84. django_spec_agents-0.1.0/django_spec_agents/policy/failure/utils.py +43 -0
  85. django_spec_agents-0.1.0/django_spec_agents/policy/guard/__init__.py +0 -0
  86. django_spec_agents-0.1.0/django_spec_agents/policy/guard/tool_guard.py +115 -0
  87. django_spec_agents-0.1.0/django_spec_agents/policy/guard/types/__init__.py +0 -0
  88. django_spec_agents-0.1.0/django_spec_agents/policy/guard/types/tool_guard_config.py +39 -0
  89. django_spec_agents-0.1.0/django_spec_agents/py.typed +0 -0
  90. django_spec_agents-0.1.0/django_spec_agents/registry/__init__.py +0 -0
  91. django_spec_agents-0.1.0/django_spec_agents/registry/build_input_schema.py +121 -0
  92. django_spec_agents-0.1.0/django_spec_agents/registry/decorator.py +48 -0
  93. django_spec_agents-0.1.0/django_spec_agents/registry/tool_registry.py +131 -0
  94. django_spec_agents-0.1.0/django_spec_agents/registry/types/__init__.py +0 -0
  95. django_spec_agents-0.1.0/django_spec_agents/registry/types/tool_binding.py +21 -0
  96. django_spec_agents-0.1.0/django_spec_agents/registry/types/tool_spec.py +46 -0
  97. django_spec_agents-0.1.0/django_spec_agents/registry/utils.py +68 -0
  98. django_spec_agents-0.1.0/django_spec_agents/spec_tools/__init__.py +11 -0
  99. django_spec_agents-0.1.0/django_spec_agents/spec_tools/py.typed +0 -0
  100. django_spec_agents-0.1.0/django_spec_agents/spec_tools/spec_capability.py +182 -0
  101. django_spec_agents-0.1.0/django_spec_agents/spec_tools/spec_toolset.py +3532 -0
  102. django_spec_agents-0.1.0/django_spec_agents/spec_tools/types/__init__.py +3 -0
  103. django_spec_agents-0.1.0/django_spec_agents/spec_tools/types/agent_conventions.py +284 -0
  104. django_spec_agents-0.1.0/django_spec_agents/testing.py +193 -0
  105. django_spec_agents-0.1.0/django_spec_agents/types/__init__.py +0 -0
  106. django_spec_agents-0.1.0/django_spec_agents/types/attachment_storage_alias.py +14 -0
  107. django_spec_agents-0.1.0/django_spec_agents/types/destructive_metadata_key.py +15 -0
  108. django_spec_agents-0.1.0/django_spec_agents/types/tool_category.py +23 -0
  109. django_spec_agents-0.1.0/django_spec_agents/types/x_category_key.py +10 -0
  110. django_spec_agents-0.1.0/django_spec_agents/types/x_confirm_key.py +10 -0
  111. django_spec_agents-0.1.0/django_spec_agents/types/x_destructive_key.py +14 -0
  112. django_spec_agents-0.1.0/django_spec_agents/types/x_summary_key.py +10 -0
  113. django_spec_agents-0.1.0/django_spec_agents/utils.py +128 -0
  114. django_spec_agents-0.1.0/django_spec_agents/version.py +5 -0
  115. django_spec_agents-0.1.0/docs/concepts.md +170 -0
  116. django_spec_agents-0.1.0/docs/index.md +96 -0
  117. django_spec_agents-0.1.0/docs/integrations.md +325 -0
  118. django_spec_agents-0.1.0/docs/policy.md +478 -0
  119. django_spec_agents-0.1.0/docs/reference/agent.md +28 -0
  120. django_spec_agents-0.1.0/docs/reference/index.md +65 -0
  121. django_spec_agents-0.1.0/docs/reference/integrations.md +26 -0
  122. django_spec_agents-0.1.0/docs/reference/persistence.md +99 -0
  123. django_spec_agents-0.1.0/docs/reference/policy.md +47 -0
  124. django_spec_agents-0.1.0/docs/reference/registry.md +63 -0
  125. django_spec_agents-0.1.0/docs/spec-tools/agent-audience.md +114 -0
  126. django_spec_agents-0.1.0/docs/spec-tools/background-runs.md +223 -0
  127. django_spec_agents-0.1.0/docs/spec-tools/index.md +46 -0
  128. django_spec_agents-0.1.0/docs/spec-tools/polymorphic-actions.md +52 -0
  129. django_spec_agents-0.1.0/docs/spec-tools/quickstart.md +1200 -0
  130. django_spec_agents-0.1.0/docs/spec-tools/reference.md +64 -0
  131. django_spec_agents-0.1.0/docs/spec-tools/spec-registry.md +288 -0
  132. django_spec_agents-0.1.0/docs/storage.md +539 -0
  133. django_spec_agents-0.1.0/docs/tools.md +131 -0
  134. django_spec_agents-0.1.0/mkdocs.yml +110 -0
  135. django_spec_agents-0.1.0/pyproject.toml +283 -0
  136. django_spec_agents-0.1.0/scripts/check-changelog +148 -0
  137. django_spec_agents-0.1.0/scripts/release-publish.sh +246 -0
  138. django_spec_agents-0.1.0/tests/__init__.py +0 -0
  139. django_spec_agents-0.1.0/tests/agent/__init__.py +0 -0
  140. django_spec_agents-0.1.0/tests/agent/catalog_server.py +50 -0
  141. django_spec_agents-0.1.0/tests/agent/test_agent_factory.py +144 -0
  142. django_spec_agents-0.1.0/tests/agent/test_attachment_toolset.py +224 -0
  143. django_spec_agents-0.1.0/tests/agent/test_build_model.py +62 -0
  144. django_spec_agents-0.1.0/tests/agent/test_build_tool_catalog.py +80 -0
  145. django_spec_agents-0.1.0/tests/agent/test_registry_tools_reach_the_model.py +215 -0
  146. django_spec_agents-0.1.0/tests/agent/types/__init__.py +0 -0
  147. django_spec_agents-0.1.0/tests/agent/types/test_agent_deps.py +95 -0
  148. django_spec_agents-0.1.0/tests/conftest_settings.py +71 -0
  149. django_spec_agents-0.1.0/tests/contrib/__init__.py +0 -0
  150. django_spec_agents-0.1.0/tests/contrib/store/__init__.py +0 -0
  151. django_spec_agents-0.1.0/tests/contrib/store/management/__init__.py +0 -0
  152. django_spec_agents-0.1.0/tests/contrib/store/management/commands/__init__.py +0 -0
  153. django_spec_agents-0.1.0/tests/contrib/store/management/commands/test_agent_store_backfill_hashes.py +71 -0
  154. django_spec_agents-0.1.0/tests/contrib/store/management/commands/test_agent_store_prune_attachments.py +105 -0
  155. django_spec_agents-0.1.0/tests/contrib/store/management/commands/test_agent_store_purge_memory.py +56 -0
  156. django_spec_agents-0.1.0/tests/contrib/store/management/commands/test_agent_store_strip_inline_bytes.py +90 -0
  157. django_spec_agents-0.1.0/tests/contrib/store/migrations/__init__.py +0 -0
  158. django_spec_agents-0.1.0/tests/contrib/store/migrations/test_0001_initial.py +178 -0
  159. django_spec_agents-0.1.0/tests/contrib/store/rows.py +15 -0
  160. django_spec_agents-0.1.0/tests/contrib/store/test_apps.py +62 -0
  161. django_spec_agents-0.1.0/tests/contrib/store/test_attachment_storage.py +73 -0
  162. django_spec_agents-0.1.0/tests/contrib/store/test_default_attachment_store.py +372 -0
  163. django_spec_agents-0.1.0/tests/contrib/store/test_default_conversation_store.py +184 -0
  164. django_spec_agents-0.1.0/tests/contrib/store/test_default_memory_store.py +614 -0
  165. django_spec_agents-0.1.0/tests/contrib/store/test_default_step_store.py +329 -0
  166. django_spec_agents-0.1.0/tests/contrib/store/test_documented_imports.py +88 -0
  167. django_spec_agents-0.1.0/tests/contrib/store/test_models.py +54 -0
  168. django_spec_agents-0.1.0/tests/contrib/store/test_reconcile_conversation_attachments.py +132 -0
  169. django_spec_agents-0.1.0/tests/contrib/store/test_strip_inline_binary_parts.py +97 -0
  170. django_spec_agents-0.1.0/tests/contrib/store/test_upgrade_from_django_pydantic_agent.py +137 -0
  171. django_spec_agents-0.1.0/tests/contrib/store/test_utils.py +145 -0
  172. django_spec_agents-0.1.0/tests/contrib/store/upgrade/__init__.py +0 -0
  173. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/__init__.py +0 -0
  174. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/apps.py +16 -0
  175. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/attachment_storage.py +15 -0
  176. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/migrations/0001_initial.py +261 -0
  177. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/migrations/0002_snapshot_state.py +21 -0
  178. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/migrations/0003_attachment_content_hash_and_conversation_links.py +79 -0
  179. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/migrations/0004_alter_storedattachment_file.py +26 -0
  180. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/migrations/0005_storedmemory_storedmemoryoperation.py +75 -0
  181. django_spec_agents-0.1.0/tests/contrib/store/upgrade/legacy_store/migrations/__init__.py +0 -0
  182. django_spec_agents-0.1.0/tests/contrib/store/upgrade/settings_new.py +27 -0
  183. django_spec_agents-0.1.0/tests/contrib/store/upgrade/settings_old.py +29 -0
  184. django_spec_agents-0.1.0/tests/contrib/store/upgrade/steps.py +171 -0
  185. django_spec_agents-0.1.0/tests/host_only_settings.py +23 -0
  186. django_spec_agents-0.1.0/tests/host_only_store_settings.py +18 -0
  187. django_spec_agents-0.1.0/tests/integrations/__init__.py +0 -0
  188. django_spec_agents-0.1.0/tests/integrations/drf_server.py +96 -0
  189. django_spec_agents-0.1.0/tests/integrations/drf_server_lookup.py +19 -0
  190. django_spec_agents-0.1.0/tests/integrations/drf_specs.py +15 -0
  191. django_spec_agents-0.1.0/tests/integrations/drf_specs_lookup.py +64 -0
  192. django_spec_agents-0.1.0/tests/integrations/helpers.py +98 -0
  193. django_spec_agents-0.1.0/tests/integrations/test_build_spec_capability.py +259 -0
  194. django_spec_agents-0.1.0/tests/integrations/test_drf_mcp.py +861 -0
  195. django_spec_agents-0.1.0/tests/integrations/test_harness_memory.py +190 -0
  196. django_spec_agents-0.1.0/tests/integrations/test_resolve_spec_mapping.py +78 -0
  197. django_spec_agents-0.1.0/tests/persistence/__init__.py +0 -0
  198. django_spec_agents-0.1.0/tests/persistence/test_django_session_conversation_store.py +144 -0
  199. django_spec_agents-0.1.0/tests/persistence/test_memory_namespace.py +114 -0
  200. django_spec_agents-0.1.0/tests/persistence/test_memory_namespace_for_user.py +56 -0
  201. django_spec_agents-0.1.0/tests/persistence/test_model_attachment_store.py +56 -0
  202. django_spec_agents-0.1.0/tests/persistence/test_model_conversation_store.py +88 -0
  203. django_spec_agents-0.1.0/tests/persistence/test_null_attachment_store.py +24 -0
  204. django_spec_agents-0.1.0/tests/persistence/test_null_conversation_store.py +27 -0
  205. django_spec_agents-0.1.0/tests/persistence/test_scoped_conversation_store.py +134 -0
  206. django_spec_agents-0.1.0/tests/persistence/test_utils.py +116 -0
  207. django_spec_agents-0.1.0/tests/persistence/types/__init__.py +0 -0
  208. django_spec_agents-0.1.0/tests/persistence/types/test_attachment_ref.py +24 -0
  209. django_spec_agents-0.1.0/tests/persistence/types/test_conversation_meta.py +24 -0
  210. django_spec_agents-0.1.0/tests/persistence/types/test_opened_attachment.py +11 -0
  211. django_spec_agents-0.1.0/tests/policy/__init__.py +0 -0
  212. django_spec_agents-0.1.0/tests/policy/audit/__init__.py +0 -0
  213. django_spec_agents-0.1.0/tests/policy/audit/capturing_audit_logger.py +13 -0
  214. django_spec_agents-0.1.0/tests/policy/audit/test_audit_capability.py +1951 -0
  215. django_spec_agents-0.1.0/tests/policy/audit/test_logging_audit_logger.py +58 -0
  216. django_spec_agents-0.1.0/tests/policy/audit/test_null_audit_logger.py +15 -0
  217. django_spec_agents-0.1.0/tests/policy/audit/types/__init__.py +0 -0
  218. django_spec_agents-0.1.0/tests/policy/audit/types/test_audit_event.py +13 -0
  219. django_spec_agents-0.1.0/tests/policy/failure/__init__.py +0 -0
  220. django_spec_agents-0.1.0/tests/policy/failure/test_tool_failure_policy.py +726 -0
  221. django_spec_agents-0.1.0/tests/policy/failure/test_utils.py +39 -0
  222. django_spec_agents-0.1.0/tests/policy/guard/__init__.py +0 -0
  223. django_spec_agents-0.1.0/tests/policy/guard/test_tool_guard.py +226 -0
  224. django_spec_agents-0.1.0/tests/policy/guard/types/__init__.py +0 -0
  225. django_spec_agents-0.1.0/tests/registry/__init__.py +0 -0
  226. django_spec_agents-0.1.0/tests/registry/test_build_input_schema.py +186 -0
  227. django_spec_agents-0.1.0/tests/registry/test_decorator.py +81 -0
  228. django_spec_agents-0.1.0/tests/registry/test_tool_registry.py +146 -0
  229. django_spec_agents-0.1.0/tests/registry/types/__init__.py +0 -0
  230. django_spec_agents-0.1.0/tests/registry/types/test_tool_spec.py +25 -0
  231. django_spec_agents-0.1.0/tests/request_helpers.py +21 -0
  232. django_spec_agents-0.1.0/tests/spec_tools/__init__.py +0 -0
  233. django_spec_agents-0.1.0/tests/spec_tools/conftest.py +159 -0
  234. django_spec_agents-0.1.0/tests/spec_tools/test_default_wording.py +143 -0
  235. django_spec_agents-0.1.0/tests/spec_tools/test_documented_imports.py +77 -0
  236. django_spec_agents-0.1.0/tests/spec_tools/test_spec_capability.py +317 -0
  237. django_spec_agents-0.1.0/tests/spec_tools/test_spec_registry_source.py +357 -0
  238. django_spec_agents-0.1.0/tests/spec_tools/test_spec_toolset.py +7434 -0
  239. django_spec_agents-0.1.0/tests/spec_tools/test_spec_toolset_conventions.py +525 -0
  240. django_spec_agents-0.1.0/tests/spec_tools/test_spec_toolset_server_arguments.py +587 -0
  241. django_spec_agents-0.1.0/tests/spec_tools/test_spec_toolset_subclassing.py +89 -0
  242. django_spec_agents-0.1.0/tests/spec_tools/testapp/__init__.py +0 -0
  243. django_spec_agents-0.1.0/tests/spec_tools/testapp/apps.py +8 -0
  244. django_spec_agents-0.1.0/tests/spec_tools/testapp/models.py +15 -0
  245. django_spec_agents-0.1.0/tests/spec_tools/testapp/serializers.py +36 -0
  246. django_spec_agents-0.1.0/tests/spec_tools/types/__init__.py +0 -0
  247. django_spec_agents-0.1.0/tests/spec_tools/types/test_agent_conventions.py +133 -0
  248. django_spec_agents-0.1.0/tests/spec_tools/wording_specs.py +108 -0
  249. django_spec_agents-0.1.0/tests/test_boundary.py +283 -0
  250. django_spec_agents-0.1.0/tests/test_consumer_paths.py +103 -0
  251. django_spec_agents-0.1.0/tests/test_kept_spellings.py +102 -0
  252. django_spec_agents-0.1.0/tests/test_lock.py +84 -0
  253. django_spec_agents-0.1.0/tests/test_testing.py +226 -0
  254. django_spec_agents-0.1.0/tests/test_utils.py +142 -0
  255. django_spec_agents-0.1.0/tests/test_version.py +8 -0
  256. django_spec_agents-0.1.0/tests/types/__init__.py +0 -0
  257. django_spec_agents-0.1.0/tests/types/test_destructive_metadata_key.py +13 -0
  258. django_spec_agents-0.1.0/tests/types/test_tool_category.py +11 -0
  259. django_spec_agents-0.1.0/uv.lock +2598 -0
@@ -0,0 +1 @@
1
+ * @Artui
@@ -0,0 +1,9 @@
1
+ # Security Policy
2
+
3
+ ## Reporting a Vulnerability
4
+
5
+ Please report security issues **privately** via GitHub's
6
+ [Report a vulnerability](../../security/advisories/new) button rather than opening a public issue.
7
+ I aim to acknowledge reports within 72 hours.
8
+
9
+ Supported: the latest released version on the default branch.
@@ -0,0 +1,26 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
7
+ # Dependabot already holds a new version for 3 days before proposing it; a
8
+ # week is the more useful threshold. A compromised release is typically
9
+ # yanked within days, so the gap between "published" and "found" is exactly
10
+ # the window an automatic upgrade walks into. This costs nothing in
11
+ # responsiveness that matters: cooldown covers version updates only, and
12
+ # security advisories are proposed immediately regardless.
13
+ cooldown:
14
+ default-days: 7
15
+ groups:
16
+ actions:
17
+ patterns: ["*"]
18
+ - package-ecosystem: "uv"
19
+ directory: "/"
20
+ schedule:
21
+ interval: "weekly"
22
+ cooldown:
23
+ default-days: 7
24
+ groups:
25
+ python:
26
+ patterns: ["*"]
@@ -0,0 +1,97 @@
1
+ name: release
2
+
3
+ # Main-triggered: every merge to `main` runs `release-publish-prepare`, which
4
+ # short-circuits to a no-op unless the version in source has been bumped past
5
+ # the most recent `vX.Y.Z` tag.
6
+ on:
7
+ push:
8
+ branches: [main]
9
+
10
+ concurrency:
11
+ group: release-main
12
+ cancel-in-progress: false
13
+
14
+ # Actions are pinned to a commit SHA rather than a tag: a tag is mutable, and an
15
+ # action owner (or anyone who takes over the account) can silently repoint one at
16
+ # new code that then runs with this workflow's token. The trailing version comment
17
+ # is not decoration -- Dependabot reads it to know which release the SHA is, and
18
+ # rewrites both together when a newer one ships.
19
+ jobs:
20
+ release:
21
+ name: release
22
+ runs-on: ubuntu-latest
23
+ environment:
24
+ name: pypi
25
+ url: https://pypi.org/p/django-spec-agents
26
+ permissions:
27
+ id-token: write # PyPI OIDC trusted publishing
28
+ contents: write # tag push + gh release create
29
+ steps:
30
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
31
+ with:
32
+ fetch-depth: 0
33
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
34
+ with:
35
+ enable-cache: true
36
+ - name: Install
37
+ run: uv sync --all-groups
38
+
39
+ - name: Prepare release (version check, test, build dist)
40
+ id: prepare
41
+ run: make release-publish-prepare
42
+
43
+ - name: Publish to PyPI
44
+ if: steps.prepare.outputs.released == 'true'
45
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
46
+ with:
47
+ # Idempotent on purpose. What gates a release is the git tag, not the
48
+ # index (see `prepare`), so re-running this job after a failure in a
49
+ # later step arrives here having already uploaded these files. Without
50
+ # this the upload is the step that makes that re-run impossible, and
51
+ # every recovery becomes manual — which is how one release lost its tag,
52
+ # its GitHub Release and its docs deploy to a single transient push
53
+ # failure, with the package already published.
54
+ # PyPI never lets a file be replaced, so skipping one cannot overwrite
55
+ # a published artefact.
56
+ skip-existing: true
57
+
58
+ - name: Tag + GitHub Release
59
+ if: steps.prepare.outputs.released == 'true'
60
+ env:
61
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
62
+ run: make release-publish-finalize
63
+
64
+ # Embed a coverage-badge JSON into the docs build so `mkdocs gh-deploy`
65
+ # carries it to the gh-pages root, where the README's shields.io endpoint
66
+ # badge reads it. `release-publish-prepare`'s pytest run wrote .coverage
67
+ # to the workspace root.
68
+ - name: Render coverage badge for the docs site
69
+ if: steps.prepare.outputs.released == 'true' && hashFiles('mkdocs.yml') != ''
70
+ run: |
71
+ set -euo pipefail
72
+ uv run coverage xml -o coverage.xml
73
+ python - <<'PY'
74
+ import json
75
+ import xml.etree.ElementTree as ET
76
+
77
+ pct = round(float(ET.parse("coverage.xml").getroot().attrib["line-rate"]) * 100, 1)
78
+ color = (
79
+ "brightgreen" if pct >= 95
80
+ else "green" if pct >= 85
81
+ else "yellow" if pct >= 70
82
+ else "red"
83
+ )
84
+ payload = {"schemaVersion": 1, "label": "coverage", "message": f"{pct}%", "color": color}
85
+ with open("docs/coverage.json", "w") as fh:
86
+ json.dump(payload, fh)
87
+ fh.write("\n")
88
+ print(json.dumps(payload))
89
+ PY
90
+
91
+ - name: Deploy docs + coverage badge to gh-pages
92
+ if: steps.prepare.outputs.released == 'true' && hashFiles('mkdocs.yml') != ''
93
+ run: |
94
+ git config user.name 'github-actions[bot]'
95
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
96
+ DJANGO_SETTINGS_MODULE=tests.conftest_settings \
97
+ uv run mkdocs gh-deploy --force --clean --config-file mkdocs.yml
@@ -0,0 +1,440 @@
1
+ name: tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+
8
+ concurrency:
9
+ group: tests-${{ github.ref }}
10
+ cancel-in-progress: true
11
+
12
+ # Actions are pinned to a commit SHA rather than a tag: a tag is mutable, and an
13
+ # action owner (or anyone who takes over the account) can silently repoint one at
14
+ # new code that then runs with this workflow's token. The trailing version comment
15
+ # is not decoration -- Dependabot reads it to know which release the SHA is, and
16
+ # rewrites both together when a newer one ships.
17
+ jobs:
18
+ lint:
19
+ name: lint
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
24
+ with:
25
+ enable-cache: true
26
+ - name: Install
27
+ run: uv sync --all-groups
28
+ - name: ruff check
29
+ run: uv run ruff check .
30
+ - name: ruff format check
31
+ run: uv run ruff format --check --diff .
32
+ - name: ty
33
+ run: uv run ty check django_spec_agents
34
+
35
+ docs:
36
+ name: docs build
37
+ runs-on: ubuntu-latest
38
+ steps:
39
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
40
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
41
+ with:
42
+ enable-cache: true
43
+ - name: Install with docs group
44
+ run: uv sync --all-groups
45
+ - name: mkdocs build --strict
46
+ run: DJANGO_SETTINGS_MODULE=tests.conftest_settings uv run mkdocs build --strict
47
+
48
+ floor:
49
+ name: lowest declared versions
50
+ runs-on: ubuntu-latest
51
+ steps:
52
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
53
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
54
+ with:
55
+ enable-cache: true
56
+ # A floor is a claim about the *oldest* version that works, and a lockfile
57
+ # can only ever check the newest. Development and CI both resolve the
58
+ # newest in-range dependency, so every gate passes while a consumer whose
59
+ # own constraints pull an older one gets a package that does not import.
60
+ #
61
+ # That has already happened in this ecosystem: a sibling declared
62
+ # pydantic-ai >=2 while importing a symbol that first exists in 2.16, and
63
+ # the only thing that found it was a downstream consumer resolving from
64
+ # scratch.
65
+ #
66
+ # `lowest-direct` pins every *declared* dependency to the bottom of its
67
+ # window while letting transitives resolve normally, so this asserts the
68
+ # floors mean what they say. A per-PR gate rather than a scheduled job,
69
+ # because a floor stops being true the moment someone imports a newer
70
+ # upstream symbol -- a code change, not the passage of time.
71
+ # Also the oldest supported interpreter: a floor claim covers Python
72
+ # too, and a dependency old enough to sit at its floor is the one most
73
+ # likely to use something a newer Python removed.
74
+ - name: Use the floor Python
75
+ run: uv python install 3.10
76
+ # `--refresh` on both resolutions below. This job's whole claim is "what a
77
+ # consumer resolving from scratch would get", and the runner's uv cache is
78
+ # shared across runs: without it the answer comes from whatever package
79
+ # list that cache happens to hold, which can be hours behind the index.
80
+ # Not hypothetical -- a sibling's gate failed a floor raise as
81
+ # unsatisfiable while the index had been serving the release for some
82
+ # time, two re-runs reused the same cache and failed identically, and a
83
+ # cacheless resolve of the same requirement succeeded at once. A stale
84
+ # listing makes this gate report the past, in whichever direction happens
85
+ # to be wrong: inventing a broken floor, or hiding a real one.
86
+ - name: Resolve every declared dependency at its floor
87
+ run: uv lock --resolution lowest-direct --python 3.10 --refresh
88
+ # --frozen and --no-sync are load-bearing, not tidiness. uv records the
89
+ # resolution mode in the lockfile and silently discards a lock resolved
90
+ # in a different one -- a plain `uv sync` here prints "Ignoring existing
91
+ # lockfile due to change in resolution mode: lowest-direct vs. highest"
92
+ # and re-resolves at the newest versions, so the job reports the oldest
93
+ # end while testing the newest. It did exactly that when first written.
94
+ - name: Install exactly what was locked
95
+ run: uv sync --frozen --all-groups --all-extras --python 3.10
96
+ - name: Name the versions that produced
97
+ run: uv pip list
98
+ - name: Test
99
+ run: uv run --no-sync pytest
100
+
101
+ # The step above installs every extra, and an extra can hold a shared
102
+ # dependency *above* the floor being claimed -- masking the lie rather
103
+ # than exposing it. That is not hypothetical: reverting a sibling's
104
+ # pydantic-ai floor to its old value and resolving with all extras kept
105
+ # that dependency at 2.18 because another extra required it, and the
106
+ # suite passed. The same revert on a plain base install resolved 2.0 and
107
+ # failed at import, which is exactly how the original defect reached a
108
+ # consumer.
109
+ #
110
+ # So the floor claim has to be checked on the install shape that has the
111
+ # fewest constraints: the package alone, no extras, no dev group.
112
+ - name: Import the package installed alone, at its floors
113
+ run: |
114
+ uv venv --python 3.10 .venv-floor
115
+ uv pip install --python .venv-floor/bin/python --resolution lowest-direct --refresh -e .
116
+ uv pip list --python .venv-floor/bin/python
117
+ .venv-floor/bin/python - <<'PY'
118
+ import django
119
+ from django.conf import settings
120
+
121
+ # The package reaches for Django at import time (models, requests,
122
+ # storage), so a bare import cannot run unconfigured.
123
+ settings.configure(
124
+ SECRET_KEY="x",
125
+ INSTALLED_APPS=[
126
+ "django.contrib.contenttypes",
127
+ "django.contrib.auth",
128
+ ],
129
+ DATABASES={"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:"}},
130
+ )
131
+ django.setup()
132
+
133
+ import django_spec_agents
134
+ from django_spec_agents import AgentConfig, ToolRegistry, build_agent, tool
135
+
136
+ # Touch the public surface rather than only the package root: an
137
+ # upstream symbol that moved would be imported by a leaf module, and
138
+ # importing the root alone can miss it.
139
+ assert callable(build_agent)
140
+ assert callable(tool)
141
+ assert AgentConfig is not None and ToolRegistry is not None
142
+ print("BASE IMPORT OK", django.get_version(), django_spec_agents.__version__)
143
+ PY
144
+
145
+ # The package with no extra is what django-admin-agent ships, and the boundary
146
+ # tests are what keep it importable that way: the root loads nothing of
147
+ # ``spec_tools`` or the ``[specs]`` extra. Every other job installs every extra,
148
+ # where a root import of ``spec_tools`` would still import, so this one installs
149
+ # none and runs those tests alone. The floor job's base import proves the root
150
+ # imports at its floors; this proves what it leaves unloaded, under the settings
151
+ # of a project serving no specs. No coverage gate: these tests alone cover
152
+ # little of the package, and the suite that covers it needs the extras.
153
+ host-only:
154
+ name: host-only install
155
+ runs-on: ubuntu-latest
156
+ steps:
157
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
158
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
159
+ with:
160
+ enable-cache: true
161
+ - name: Boundary tests on an install with no extra
162
+ run: make test-host-only
163
+
164
+ # The store's upgrade renames PostgreSQL's ``_like`` index, which Django drops by
165
+ # a name it computes from the table rather than by looking it up, so a missed
166
+ # rename leaves a duplicate index behind the next type change. SQLite makes no
167
+ # such index, so only a real server can show the rename happened.
168
+ store-upgrade-postgresql:
169
+ name: store upgrade on PostgreSQL
170
+ runs-on: ubuntu-latest
171
+ services:
172
+ postgres:
173
+ image: postgres:16
174
+ env:
175
+ POSTGRES_DB: store_upgrade
176
+ POSTGRES_HOST_AUTH_METHOD: trust
177
+ ports:
178
+ - 5432:5432
179
+ options: >-
180
+ --health-cmd pg_isready
181
+ --health-interval 5s
182
+ --health-timeout 5s
183
+ --health-retries 10
184
+ steps:
185
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
186
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
187
+ with:
188
+ enable-cache: true
189
+ - name: Sync dev deps
190
+ run: uv sync --all-groups
191
+ - name: Install the PostgreSQL driver
192
+ run: uv pip install "psycopg[binary]"
193
+ - name: pytest
194
+ env:
195
+ STORE_UPGRADE_ENGINE: django.db.backends.postgresql
196
+ STORE_UPGRADE_DATABASE: store_upgrade
197
+ STORE_UPGRADE_HOST: localhost
198
+ STORE_UPGRADE_USER: postgres
199
+ run: |
200
+ uv run --no-sync pytest -q --no-cov \
201
+ tests/contrib/store/test_upgrade_from_django_pydantic_agent.py
202
+
203
+ test:
204
+ name: ${{ matrix.python-version }} / Django ${{ matrix.django-version }}
205
+ runs-on: ubuntu-latest
206
+ strategy:
207
+ fail-fast: false
208
+ matrix:
209
+ python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
210
+ django-version: ["4.2", "5.0", "5.1", "5.2", "6.0", "6.1"]
211
+ exclude:
212
+ # Django 4.2 supports Python 3.8–3.12
213
+ - python-version: "3.13"
214
+ django-version: "4.2"
215
+ - python-version: "3.14"
216
+ django-version: "4.2"
217
+ # Django 5.0 supports Python 3.10–3.12
218
+ - python-version: "3.13"
219
+ django-version: "5.0"
220
+ - python-version: "3.14"
221
+ django-version: "5.0"
222
+ # Django 5.1 supports Python 3.10–3.13
223
+ - python-version: "3.14"
224
+ django-version: "5.1"
225
+ # Django 6.0 requires Python 3.12+
226
+ - python-version: "3.10"
227
+ django-version: "6.0"
228
+ - python-version: "3.11"
229
+ django-version: "6.0"
230
+ # Django 6.1 requires Python 3.12+
231
+ - python-version: "3.10"
232
+ django-version: "6.1"
233
+ - python-version: "3.11"
234
+ django-version: "6.1"
235
+
236
+ steps:
237
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
238
+ - uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
239
+ with:
240
+ enable-cache: true
241
+ - name: Install Python ${{ matrix.python-version }}
242
+ run: uv python install ${{ matrix.python-version }}
243
+ - name: Sync dev deps
244
+ run: uv sync --all-groups --python ${{ matrix.python-version }}
245
+ - name: Pin Django ${{ matrix.django-version }}
246
+ run: uv pip install --reinstall-package django "django~=${{ matrix.django-version }}.0"
247
+ - name: pytest
248
+ run: |
249
+ uv run --no-sync pytest \
250
+ --cov-report=xml:coverage.xml \
251
+ --cov-report=html:htmlcov
252
+ - name: Upload coverage artifact
253
+ if: always()
254
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
255
+ with:
256
+ name: coverage-py${{ matrix.python-version }}-django${{ matrix.django-version }}
257
+ path: |
258
+ coverage.xml
259
+ htmlcov/
260
+ if-no-files-found: ignore
261
+
262
+ coverage-badge:
263
+ # Publishes a shields.io-compatible coverage.json to gh-pages so the README
264
+ # badge reflects reality. Runs only on push to main, after the matrix has
265
+ # produced an artifact we can read from.
266
+ name: coverage badge
267
+ needs: test
268
+ if: github.event_name == 'push' && github.ref == 'refs/heads/main'
269
+ runs-on: ubuntu-latest
270
+ permissions:
271
+ contents: write
272
+ steps:
273
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
274
+ with:
275
+ # Need the parent commit so the gate step below can diff
276
+ # version.py against ${{ github.event.before }}. The default
277
+ # shallow checkout (depth 1) would make `git show "$before:…"`
278
+ # fail with exit 128 and turn the whole job red.
279
+ fetch-depth: 2
280
+ # release.yml runs in parallel on the same push and owns gh-pages
281
+ # publishing on release commits (it bundles coverage.json into the
282
+ # mkdocs gh-deploy). If we also push here we get a second built-in
283
+ # "pages build and deployment" run that the first one cancels. The
284
+ # version-diff against ${{ github.event.before }} is the reliable
285
+ # signal — release.yml's tag isn't pushed until after PyPI publish,
286
+ # so a tag-existence check would race.
287
+ - name: Skip when release.yml owns this push
288
+ id: gate
289
+ run: |
290
+ set -euo pipefail
291
+ before="${{ github.event.before }}"
292
+ if [[ -z "$before" || "$before" =~ ^0+$ ]]; then
293
+ # First push to main on a brand-new repo — no prior state to diff.
294
+ echo "skip=false" >> "$GITHUB_OUTPUT"
295
+ exit 0
296
+ fi
297
+ # `before` may not be in the local object DB if the push
298
+ # contained more than one commit (fetch-depth: 2 only guarantees
299
+ # HEAD + 1 parent). Try to fetch just that commit; if it's not
300
+ # reachable, fall back to "don't skip" — better to double-push
301
+ # to gh-pages once than to turn the pipeline red.
302
+ if ! git cat-file -e "$before^{commit}" 2>/dev/null; then
303
+ git fetch --depth=1 origin "$before" 2>/dev/null || true
304
+ fi
305
+ old=""
306
+ if git cat-file -e "$before:django_spec_agents/version.py" 2>/dev/null; then
307
+ old="$(git show "$before:django_spec_agents/version.py" \
308
+ | awk -F'"' '/^__version__/ { print $2; exit }')"
309
+ fi
310
+ new="$(awk -F'"' '/^__version__/ { print $2; exit }' \
311
+ django_spec_agents/version.py)"
312
+ if [[ -n "$old" && "$old" != "$new" ]]; then
313
+ echo "Version bumped $old → $new; release.yml will deploy gh-pages."
314
+ echo "skip=true" >> "$GITHUB_OUTPUT"
315
+ else
316
+ echo "skip=false" >> "$GITHUB_OUTPUT"
317
+ fi
318
+ - name: Download canonical coverage artifact
319
+ if: steps.gate.outputs.skip != 'true'
320
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
321
+ with:
322
+ # Pin to one cell of the matrix — coverage is the same shape across
323
+ # all cells and we only need one number.
324
+ name: coverage-py3.13-django5.2
325
+ path: coverage-artifact/
326
+ - name: Render coverage.json
327
+ if: steps.gate.outputs.skip != 'true'
328
+ run: |
329
+ python - <<'PY'
330
+ import json
331
+ import xml.etree.ElementTree as ET
332
+
333
+ tree = ET.parse("coverage-artifact/coverage.xml")
334
+ pct = round(float(tree.getroot().attrib["line-rate"]) * 100, 1)
335
+ if pct >= 95:
336
+ color = "brightgreen"
337
+ elif pct >= 85:
338
+ color = "green"
339
+ elif pct >= 70:
340
+ color = "yellow"
341
+ else:
342
+ color = "red"
343
+ payload = {
344
+ "schemaVersion": 1,
345
+ "label": "coverage",
346
+ "message": f"{pct}%",
347
+ "color": color,
348
+ }
349
+ with open("coverage.json", "w") as fh:
350
+ json.dump(payload, fh)
351
+ fh.write("\n")
352
+ print(json.dumps(payload))
353
+ PY
354
+ - name: Publish coverage.json to gh-pages
355
+ if: steps.gate.outputs.skip != 'true'
356
+ env:
357
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
358
+ run: |
359
+ set -euo pipefail
360
+ git config user.name 'github-actions[bot]'
361
+ git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
362
+
363
+ tmp="$(mktemp -d)"
364
+ cp coverage.json "$tmp/coverage.json"
365
+
366
+ git fetch origin gh-pages --depth=1 2>/dev/null || true
367
+ if git rev-parse --verify origin/gh-pages >/dev/null 2>&1; then
368
+ git worktree add gh-pages origin/gh-pages
369
+ cp "$tmp/coverage.json" gh-pages/coverage.json
370
+ cd gh-pages
371
+ if [[ -n "$(git status --porcelain coverage.json)" ]]; then
372
+ git add coverage.json
373
+ git commit -m "coverage badge: update from ${GITHUB_SHA::7}"
374
+ git push origin HEAD:gh-pages
375
+ else
376
+ echo "coverage.json unchanged, skipping push"
377
+ fi
378
+ else
379
+ # First run before docs deploy. Lay down an orphan gh-pages with
380
+ # just the badge file; the docs deploy will overwrite the rest.
381
+ git worktree add --detach gh-pages
382
+ cd gh-pages
383
+ git checkout --orphan gh-pages
384
+ git rm -rf . >/dev/null 2>&1 || true
385
+ cp "$tmp/coverage.json" coverage.json
386
+ git add coverage.json
387
+ git commit -m "coverage badge: initial publish from ${GITHUB_SHA::7}"
388
+ git push origin gh-pages
389
+ fi
390
+
391
+ secrets:
392
+ name: secret scan
393
+ runs-on: ubuntu-latest
394
+ steps:
395
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
396
+ with:
397
+ fetch-depth: 0
398
+ - name: Install gitleaks
399
+ run: |
400
+ curl -sSL https://github.com/gitleaks/gitleaks/releases/download/v8.30.1/gitleaks_8.30.1_linux_x64.tar.gz | tar -xz gitleaks
401
+ sudo mv gitleaks /usr/local/bin/
402
+ - name: Scan repo + history
403
+ run: gitleaks git --redact --no-banner .
404
+
405
+ tests-passed:
406
+ name: tests
407
+ # always(), never !cancelled(): a skipped job reports a skipped check, and
408
+ # branch protection counts a skipped required check as passing.
409
+ if: always()
410
+ needs: [lint, docs, floor, host-only, store-upgrade-postgresql, test]
411
+ runs-on: ubuntu-latest
412
+ steps:
413
+ # Pass on an exact `success` from every job above, rather than failing on
414
+ # a list of bad results: such a list passes whatever it does not name, and
415
+ # GitHub reports a result it does not document. A job no hosted runner
416
+ # ever picks up ("The job was not acquired by Runner of type hosted even
417
+ # after multiple attempts") shows as cancelled on its own check but
418
+ # reaches `needs` as `abandoned`, and a gate that failed only on `failure`
419
+ # and `cancelled` went green over exactly that while the jobs it guards
420
+ # never ran. A matrix reaches `needs` as one result, and one cell that did
421
+ # not succeed is enough to make it something other than `success`.
422
+ #
423
+ # None of the jobs above has an `if:`, so `skipped` can only mean one of
424
+ # them never ran, and it fails like any other result. A job that gains a
425
+ # legitimate skip gets an exception naming it, never `skipped` accepted
426
+ # from all of them.
427
+ #
428
+ # The results come in through `env` rather than being spliced into the
429
+ # script, and every one is printed, so a red gate says which job and what
430
+ # GitHub reported for it.
431
+ - name: Verify every required job succeeded
432
+ env:
433
+ NEEDS: ${{ toJSON(needs) }}
434
+ run: |
435
+ echo "$NEEDS" | jq -r 'to_entries[] | "\(.key): \(.value.result)"'
436
+ not_ok="$(echo "$NEEDS" | jq -r '[to_entries[] | select(.value.result != "success") | "\(.key)=\(.value.result)"] | join(" ")')"
437
+ if [ -n "$not_ok" ]; then
438
+ echo "::error::required jobs did not succeed: $not_ok"
439
+ exit 1
440
+ fi