hyperi-ci 2.2.1__tar.gz → 2.2.3__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 (224) hide show
  1. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/CHANGELOG.md +17 -0
  2. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/PKG-INFO +2 -1
  3. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/TODO.md +18 -23
  4. hyperi_ci-2.2.3/VERSION +1 -0
  5. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/pyproject.toml +19 -2
  6. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/defaults.yaml +21 -0
  7. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config.py +29 -0
  8. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/detect.py +73 -7
  9. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/dispatch.py +24 -1
  10. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/init.py +6 -0
  11. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_detect_tier.py +99 -0
  12. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_config.py +69 -1
  13. hyperi_ci-2.2.3/tests/unit/test_dispatch_status.py +96 -0
  14. hyperi_ci-2.2.3/uv.lock +639 -0
  15. hyperi_ci-2.2.1/VERSION +0 -1
  16. hyperi_ci-2.2.1/uv.lock +0 -718
  17. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/.ai-version +0 -0
  18. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/doco.md +0 -0
  19. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/load.md +0 -0
  20. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/review.md +0 -0
  21. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/save.md +0 -0
  22. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/setup-claude.md +0 -0
  23. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/simplify.md +0 -0
  24. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/commands/standards.md +0 -0
  25. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/UNIVERSAL.md +0 -0
  26. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/ai-conduct.md +0 -0
  27. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/ansible.md +0 -0
  28. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/bash.md +0 -0
  29. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/chars-policy.md +0 -0
  30. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/ci.md +0 -0
  31. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/clickhouse-sql.md +0 -0
  32. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/code-header.md +0 -0
  33. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/code-style.md +0 -0
  34. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/config-and-logging.md +0 -0
  35. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/cpp.md +0 -0
  36. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/design-principles.md +0 -0
  37. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/dfe-metrics.md +0 -0
  38. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/docker.md +0 -0
  39. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/error-handling.md +0 -0
  40. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/git.md +0 -0
  41. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/golang.md +0 -0
  42. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/issue-management.md +0 -0
  43. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/k8s.md +0 -0
  44. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/licensing.md +0 -0
  45. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/mocks-policy.md +0 -0
  46. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/pki.md +0 -0
  47. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/python.md +0 -0
  48. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/repo-naming.md +0 -0
  49. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/rust.md +0 -0
  50. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/security.md +0 -0
  51. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/sql-clickhouse.md +0 -0
  52. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/terraform.md +0 -0
  53. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/testing.md +0 -0
  54. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/typescript.md +0 -0
  55. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/rules/universal.md +0 -0
  56. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/settings.json +0 -0
  57. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ai-common/SKILL.md +0 -0
  58. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ai-guidelines/SKILL.md +0 -0
  59. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/bleeding-edge/SKILL.md +0 -0
  60. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ci-check/SKILL.md +0 -0
  61. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ci-logs/SKILL.md +0 -0
  62. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/ci-watch/SKILL.md +0 -0
  63. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/deps/SKILL.md +0 -0
  64. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/docs-audit/SKILL.md +0 -0
  65. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/documentation/SKILL.md +0 -0
  66. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/python/SKILL.md +0 -0
  67. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/release/SKILL.md +0 -0
  68. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/standards/SKILL.md +0 -0
  69. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/test-review/SKILL.md +0 -0
  70. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.claude/skills/verification/SKILL.md +0 -0
  71. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.gitattributes +0 -0
  72. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/actions/predict-version/action.yml +0 -0
  73. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/_release-tail.yml +0 -0
  74. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/ci.yml +0 -0
  75. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/go-ci.yml +0 -0
  76. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/python-ci.yml +0 -0
  77. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/rust-ci.yml +0 -0
  78. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.github/workflows/ts-ci.yml +0 -0
  79. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.gitignore +0 -0
  80. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.gitmodules +0 -0
  81. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.hyperi-ci.yaml +0 -0
  82. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.mcp.json +0 -0
  83. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/.releaserc.yaml +0 -0
  84. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/AI-TRAINING-POLICY.md +0 -0
  85. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/CLAUDE.md +0 -0
  86. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/LICENSE +0 -0
  87. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/README.md +0 -0
  88. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/STATE.md +0 -0
  89. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/commit-types.yaml +0 -0
  90. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/org.yaml +0 -0
  91. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/runners.yaml +0 -0
  92. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/secrets-access.yaml +0 -0
  93. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/config/versions.yaml +0 -0
  94. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/ARC-RUNNERS.md +0 -0
  95. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/ARCHITECTURE.md +0 -0
  96. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/CI-LESSONS.md +0 -0
  97. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/DESIGN.md +0 -0
  98. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/JFROG-MIGRATION.md +0 -0
  99. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/MIGRATION-GUIDE.md +0 -0
  100. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/PGO-WORKLOAD-GUIDE.md +0 -0
  101. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/deployment-contract.md +0 -0
  102. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/migration/deployment-contract-tier3.md +0 -0
  103. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/rust.md +0 -0
  104. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/docs/typescript.md +0 -0
  105. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/renovate.json +0 -0
  106. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/robots.txt +0 -0
  107. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/scripts/pre-commit-versions.sh +0 -0
  108. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/scripts/setup-rust-dev.py +0 -0
  109. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/scripts/sync-secrets-access.py +0 -0
  110. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/scripts/update-versions.py +0 -0
  111. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/__init__.py +0 -0
  112. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/cli.py +0 -0
  113. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/common.py +0 -0
  114. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/native-deps/golang.yaml +0 -0
  115. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/native-deps/python.yaml +0 -0
  116. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/native-deps/rust.yaml +0 -0
  117. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/native-deps/typescript.yaml +0 -0
  118. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/org.yaml +0 -0
  119. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/toolchains/gcc.yaml +0 -0
  120. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/config/toolchains/llvm.yaml +0 -0
  121. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/__init__.py +0 -0
  122. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/binary_stage.py +0 -0
  123. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/build.py +0 -0
  124. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/compose.py +0 -0
  125. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/detect.py +0 -0
  126. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/labels.py +0 -0
  127. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/manifest.py +0 -0
  128. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/registry.py +0 -0
  129. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/stage.py +0 -0
  130. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/container/templates.py +0 -0
  131. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/__init__.py +0 -0
  132. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/cli.py +0 -0
  133. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/contract.py +0 -0
  134. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/registry.py +0 -0
  135. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/scaffold.py +0 -0
  136. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/deployment/stage.py +0 -0
  137. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/detect.py +0 -0
  138. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/gh.py +0 -0
  139. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/install_deps.py +0 -0
  140. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/__init__.py +0 -0
  141. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/_build_common.py +0 -0
  142. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/__init__.py +0 -0
  143. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/build.py +0 -0
  144. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/publish.py +0 -0
  145. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/quality.py +0 -0
  146. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/golang/test.py +0 -0
  147. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/__init__.py +0 -0
  148. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/build.py +0 -0
  149. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/publish.py +0 -0
  150. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/quality.py +0 -0
  151. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/python/test.py +0 -0
  152. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/quality_common.py +0 -0
  153. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/__init__.py +0 -0
  154. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/build.py +0 -0
  155. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/optimize.py +0 -0
  156. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/pgo.py +0 -0
  157. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/publish.py +0 -0
  158. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/quality.py +0 -0
  159. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/rust/test.py +0 -0
  160. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/__init__.py +0 -0
  161. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/_common.py +0 -0
  162. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/build.py +0 -0
  163. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/install_deps.py +0 -0
  164. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/publish.py +0 -0
  165. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/quality.py +0 -0
  166. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/languages/typescript/test.py +0 -0
  167. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/logs.py +0 -0
  168. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/migrate.py +0 -0
  169. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/native_deps.py +0 -0
  170. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/publish/__init__.py +0 -0
  171. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/publish/binaries.py +0 -0
  172. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/publish/dispatch.py +0 -0
  173. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/publish_binaries.py +0 -0
  174. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/push.py +0 -0
  175. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/quality/__init__.py +0 -0
  176. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/quality/commit_validation.py +0 -0
  177. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/quality/gitleaks.py +0 -0
  178. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/release.py +0 -0
  179. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/trigger.py +0 -0
  180. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/upgrade.py +0 -0
  181. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/src/hyperi_ci/watch.py +0 -0
  182. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/README.md +0 -0
  183. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/grpc-server.sh +0 -0
  184. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/http-server.sh +0 -0
  185. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/kafka-consumer.sh +0 -0
  186. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/kafka-producer.sh +0 -0
  187. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/templates/pgo-workload/multi-protocol.sh +0 -0
  188. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/__init__.py +0 -0
  189. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/integration/__init__.py +0 -0
  190. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/integration/test_rust_build_optimize.py +0 -0
  191. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/__init__.py +0 -0
  192. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/__init__.py +0 -0
  193. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_contract.py +0 -0
  194. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_emit_artefacts_cli.py +0 -0
  195. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_registry_cascade.py +0 -0
  196. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_scaffold.py +0 -0
  197. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/deployment/test_stage.py +0 -0
  198. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_cli.py +0 -0
  199. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_commit_validation.py +0 -0
  200. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_common.py +0 -0
  201. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_binary_stage.py +0 -0
  202. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_build.py +0 -0
  203. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_compose.py +0 -0
  204. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_detect.py +0 -0
  205. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_labels.py +0 -0
  206. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_manifest.py +0 -0
  207. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_registry.py +0 -0
  208. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_stage.py +0 -0
  209. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_container_templates.py +0 -0
  210. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_detect.py +0 -0
  211. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_dispatch_alias.py +0 -0
  212. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_gh.py +0 -0
  213. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_init.py +0 -0
  214. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_migrate.py +0 -0
  215. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_native_deps.py +0 -0
  216. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_publish.py +0 -0
  217. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_push.py +0 -0
  218. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_rust_optimize.py +0 -0
  219. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_rust_pgo.py +0 -0
  220. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_rust_quality.py +0 -0
  221. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_typescript_quality.py +0 -0
  222. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_upgrade.py +0 -0
  223. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_watch.py +0 -0
  224. {hyperi_ci-2.2.1 → hyperi_ci-2.2.3}/tests/unit/test_workflow_consistency.py +0 -0
@@ -1,3 +1,20 @@
1
+ ## [2.2.3](https://github.com/hyperi-io/hyperi-ci/compare/v2.2.2...v2.2.3) (2026-05-13)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * **deployment:** exclude self-name from tier detection ([f61921b](https://github.com/hyperi-io/hyperi-ci/commit/f61921b9b0dcfe36c3c1809b0bacbedee4c78386))
7
+ * **deps:** declare typer explicitly in runtime dependencies ([895a4a6](https://github.com/hyperi-io/hyperi-ci/commit/895a4a637e2f7f55bd74e2276ddb51f9184fc7cd))
8
+ * **deps:** remove editable pylib source override to unblock Dependabot graph ([a4f1b28](https://github.com/hyperi-io/hyperi-ci/commit/a4f1b28551758e0e1287bf931f60e52194076021))
9
+
10
+ ## [2.2.2](https://github.com/hyperi-io/hyperi-ci/compare/v2.2.1...v2.2.2) (2026-05-13)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * **config:** add project.status lifecycle field (information-only) ([a66a1a2](https://github.com/hyperi-io/hyperi-ci/commit/a66a1a23d911548078ebc62ba9995392ce9ec087))
16
+ * **dispatch:** log project.status as INFO with clarifier phrase, not WARN ([c9de7cd](https://github.com/hyperi-io/hyperi-ci/commit/c9de7cdcae47939b93a830987e59e13643516027))
17
+
1
18
  ## [2.2.1](https://github.com/hyperi-io/hyperi-ci/compare/v2.2.0...v2.2.1) (2026-05-08)
2
19
 
3
20
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hyperi-ci
3
- Version: 2.2.1
3
+ Version: 2.2.3
4
4
  Summary: HyperI CI/CD CLI tool — multi-language build, test, and publish automation
5
5
  License: Proprietary
6
6
  License-File: LICENSE
@@ -10,3 +10,4 @@ Requires-Dist: packaging>=26.0
10
10
  Requires-Dist: pydantic>=2.13
11
11
  Requires-Dist: pyyaml>=6.0
12
12
  Requires-Dist: tomli-w>=1.2.0
13
+ Requires-Dist: typer>=0.25
@@ -6,36 +6,22 @@ This is the **single source of truth** for all tasks and progress.
6
6
 
7
7
  ## Active Tasks
8
8
 
9
- ### CI consolidation (KISS) — predict-first plan-job per language
9
+ ### CI consolidation (KISS) — predict-first plan-job per language ✅ DONE 2026-05-08
10
10
 
11
11
  **Plan:** [`docs/superpowers/plans/2026-05-08-ci-consolidation-kiss.md`](docs/superpowers/plans/2026-05-08-ci-consolidation-kiss.md)
12
12
 
13
- **Architecture:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) (will be created in Task 1 of the plan)
13
+ **Architecture:** [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)
14
14
 
15
- **Why:** chore: commits + AI submodule bumps were burning ~25 minutes of CI compute per push. Two earlier consolidation attempts in this session (`_setup.yml` reusable workflow, sketched `_ci.yml` orchestrator) added indirection without solving the root cause. Web research (astral-sh/uv, tokio-rs/tokio, vercel/turborepo) showed the right shape: flat language workflow with a `plan` first job + conditional gates. No reusable-workflow chains.
15
+ **Outcome:** v2.2.0 + v2.2.1 shipped to PyPI 2026-05-08. New plan-first gating validated on the most complex Rust consumer end-to-end. Python gate validated; full publish-canary on Python deferred to hyperi-pylib (clean library) instead of dfe-engine (in-progress feature branches).
16
16
 
17
- **Status:** plan written 2026-05-08. Implementation tracked task-by-task in the plan file (17 tasks).
17
+ **Canary results:**
18
18
 
19
- **Resume from this directory** (`/projects/hyperi-ci`) when picking this up the plan, STATE.md, TODO.md, and code all live here. Recommended execution: inline for Tasks 1-12 (mechanical YAML + tests), subagent-driven for Tasks 13-15 (canaries that need fresh per-project context).
20
-
21
- **Canaries (no SEP fieldsfix bugs found, don't dismiss):**
22
-
23
- - [x] **Canary 0: hyperi-ci's own `test-projects/`** — integration tests green; one stale spike-allocator test fixed (commit `a331a0b`).
24
- - [x] **Canary 1a: dfe-loader chore-skip** — [run 25531398891](https://github.com/hyperi-io/dfe-loader/actions/runs/25531398891) — 16 seconds total, plan ✅, every other job skipped. **Killer feature validated.**
25
- - [ ] **Canary 1b: dfe-loader publish-dispatch** — [run 25531808212](https://github.com/hyperi-io/dfe-loader/actions/runs/25531808212) in flight as of 2026-05-08T01:40Z. Plan/Quality/Test green; both Build (linux-amd64, linux-arm64) jobs in_progress. BOLT/PGO; expect 30-45 min.
19
+ - [x] **Canary 0: hyperi-ci's own `test-projects/`**integration tests green (7 slow tests pass); one stale spike-allocator test fixed (commit `a331a0b`).
20
+ - [x] **Canary 1a: dfe-loader chore-skip** — [run 25531398891](https://github.com/hyperi-io/dfe-loader/actions/runs/25531398891) — **16 seconds total**, plan ✅, every other job skipped. **Killer feature validated.**
21
+ - [x] **Canary 1b: dfe-loader publish-dispatch**[run 25531808212](https://github.com/hyperi-io/dfe-loader/actions/runs/25531808212) — **38 minutes total**, all 7 jobs ✅ green end-to-end (Plan, Quality, Test, Build linux-amd64, Build linux-arm64, Container, Tag & Publish). Multi-arch BOLT+PGO Rust publish chain proven on the most complex Rust consumer.
26
22
  - [x] **Canary 2a: dfe-engine chore-skip** — [run 25531462469](https://github.com/hyperi-io/dfe-engine/actions/runs/25531462469) — 33 seconds, all heavy jobs skipped. **Gate validated.**
27
- - [ ] **Canary 2b: dfe-engine publish-dispatch** — failed at quality stage on real pre-existing CVEs (pytest, cryptography). NOT a gate regression. Handed back to dfe-engine maintainers via [`dfe-engine/docs/superpowers/plans/2026-05-08-ci-canary-publish-fix.md`](../../dfe-engine/docs/superpowers/plans/2026-05-08-ci-canary-publish-fix.md). Blocked on merge coordination (Derek + Kaz have WIP on `feat/transport-filter-helpers`).
28
- - [ ] **Canary 3 (added 2026-05-08): hyperi-pylib publish-dispatch** — preferred Python canary now: clean lib, no parallel WIP. User to drive.
29
-
30
- **Ordering update 2026-05-08:** Python canary order is hyperi-pylib FIRST (clean), then dfe-engine after merge coordination. dfe-engine is the most complex Python case but its current branch state makes it a poor first canary.
31
-
32
- **Done means:**
33
-
34
- - All 17 plan tasks checked off
35
- - All four `<lang>-ci.yml` workflows match the same shape (verified by `tests/unit/test_workflow_consistency.py`)
36
- - A chore: commit on each canary completes in <2 min, skips quality/test/build/container/publish
37
- - A `Publish: true` commit on each canary still runs the full pipeline and ships to the right registries
38
- - v2.2.0+ of hyperi-ci is published to PyPI
23
+ - [ ] **Canary 2b: dfe-engine publish-dispatch** — failed at quality on real pre-existing CVEs (pytest GHSA-6w46-j5rx-g56g, cryptography GHSA-p423-j2cm-9vmq). NOT a gate regression. Handed back to dfe-engine maintainers via [`dfe-engine/docs/superpowers/plans/2026-05-08-ci-canary-publish-fix.md`](../../dfe-engine/docs/superpowers/plans/2026-05-08-ci-canary-publish-fix.md). Blocked on Derek + Kaz merge coordination.
24
+ - [ ] **Canary 3 (Python publish-dispatch on hyperi-pylib)** — preferred Python canary: clean lib, no parallel WIP. User to drive directly.
39
25
 
40
26
  **Bugs discovered during rollout (no SEP fields):**
41
27
 
@@ -43,6 +29,15 @@ This is the **single source of truth** for all tasks and progress.
43
29
  - [x] `_release-tail.yml` expects a single `build-dist-*` artifact containing both `dist/` AND `ci-tmp/`. The plan's draft proposed splitting into separate artifacts which would have broken release-tail's download step. Combined-upload pattern preserved.
44
30
  - [x] `ts-ci.yml` had a duplicate `setup:` job key (silent YAML override) and `test:` job missing `needs:`/`if:`. Fixed during Task 8.
45
31
  - [x] `go-ci.yml` ran quality + test unconditionally before any gate decision (setup ran AFTER them). Fixed during Task 9.
32
+ - [x] `init.py` scaffold generated `workflow_dispatch:` with no `tag` input → `hyperi-ci publish` returned HTTP 422. Every project initialised with v2.2.0 inherited the bug. Fixed in v2.2.1 (commit `fa7d79b`) — surfaced by dfe-engine canary, fixed in dfe-engine consumer ci.yml AND in hyperi-ci scaffold.
33
+ - [x] `hyperi-ci watch <run-id>` invoked from a different repo's cwd silently 404'd 10× before giving up. Added `--repo / -R` flag in v2.2.1 (commit `c90c96a`) — surfaced by dfe-loader canary subagent.
34
+
35
+ **Versions shipped:**
36
+
37
+ - v2.2.0 ([run 25531099781](https://github.com/hyperi-io/hyperi-ci/actions/runs/25531099781)) — initial KISS rollout
38
+ - v2.2.1 ([run 25533443889](https://github.com/hyperi-io/hyperi-ci/actions/runs/25533443889)) — init.py + watch --repo + jemalloc test fixes
39
+
40
+ **Next:** monitor hyperi-pylib's Python canary (when user runs it) and dfe-engine's CVE-cleanup re-canary. If both come back green, the rollout is fully closed. If new bugs surface, fix and ship as v2.2.x.
46
41
 
47
42
  ---
48
43
 
@@ -0,0 +1 @@
1
+ 2.2.3
@@ -23,6 +23,11 @@ dependencies = [
23
23
  "pydantic>=2.13",
24
24
  "pyyaml>=6.0",
25
25
  "tomli-w>=1.2.0",
26
+ # typer is used directly by src/hyperi_ci/cli.py — declare
27
+ # explicitly rather than relying on the transitive pull from
28
+ # hyperi-pylib's CLI infra. If pylib ever drops typer, this
29
+ # keeps us pinned to a known surface.
30
+ "typer>=0.25",
26
31
  ]
27
32
 
28
33
  [project.scripts]
@@ -83,8 +88,20 @@ source = ["hyperi_ci"]
83
88
  # Keep this low so `pytest --cov` reports without failing the run.
84
89
  fail_under = 30
85
90
 
86
- [tool.uv.sources]
87
- hyperi-pylib = { path = "/projects/hyperi-pylib", editable = true }
91
+ # NOTE: no [tool.uv.sources] block here on purpose.
92
+ #
93
+ # Tempting to point `hyperi-pylib` at a local editable checkout
94
+ # (/projects/hyperi-pylib) so dev changes flow through `uv sync`,
95
+ # but that breaks two things:
96
+ # - Dependabot's update_graph job can't resolve the path inside its
97
+ # container — fails on every push to main, leaving the dep-graph
98
+ # stale and GitHub's vulnerability banner inaccurate.
99
+ # - Reproducibility for anyone whose checkout isn't at that exact
100
+ # path.
101
+ #
102
+ # For local pylib co-development, run from this repo:
103
+ # uv pip install -e /path/to/hyperi-pylib
104
+ # Manual install is per-developer scratch state; not in pyproject.
88
105
 
89
106
  [dependency-groups]
90
107
  dev = [
@@ -13,6 +13,27 @@
13
13
  # Values: python, typescript, golang, rust, none
14
14
  language: none
15
15
 
16
+ # Project metadata
17
+ #
18
+ # project.status: lifecycle stage of THIS PROJECT (not of any given release).
19
+ # Information-only field — surfaced in `hyperi-ci config` and at stage
20
+ # startup in CI logs so consumers can immediately see "oh, it's not GA".
21
+ # Does not affect publish behaviour (that's `publish.channel`).
22
+ #
23
+ # Values:
24
+ # experimental — early R&D, no API commitment, expect anything
25
+ # alpha — usable, expect breaks, no semver guarantees
26
+ # beta — feature-stable, polishing, mild breaks possible
27
+ # ga — semver guarantees apply, breaking only on major bump
28
+ # legacy — still works, successor exists, plan migration
29
+ # deprecated — active sunset, do not adopt
30
+ #
31
+ # Unset = "not declared" — the log line says "(status not declared)" which
32
+ # is itself a useful signal. New projects scaffolded by `hyperi-ci init`
33
+ # default to `experimental`.
34
+ project:
35
+ status: ""
36
+
16
37
  # Runner Settings
17
38
  #
18
39
  # mode: controls which GitHub Actions runners are used
@@ -22,6 +22,19 @@ import yaml
22
22
 
23
23
  _CONFIG_DIR = Path(__file__).resolve().parent / "config"
24
24
 
25
+ # Lifecycle stages a project can declare via `project.status` in
26
+ # `.hyperi-ci.yaml`. Information-only — does not gate any behaviour.
27
+ # Empty string (default) means "not declared" — the field is optional.
28
+ # See defaults.yaml for what each stage means.
29
+ VALID_PROJECT_STATUSES: tuple[str, ...] = (
30
+ "experimental",
31
+ "alpha",
32
+ "beta",
33
+ "ga",
34
+ "legacy",
35
+ "deprecated",
36
+ )
37
+
25
38
  # Re-exported for callers that just want the constant without going through
26
39
  # the full CIConfig load (e.g. quality-stage drift checks). Authoritative
27
40
  # value is defined alongside the Pydantic model that uses it as a field
@@ -229,6 +242,22 @@ def load_config(
229
242
  publish.get("target", "oss") if isinstance(publish, dict) else "oss"
230
243
  )
231
244
 
245
+ # Validate project.status if set. Warn on unknown values rather than
246
+ # failing — the field is information-only and a typo shouldn't break
247
+ # the build.
248
+ project = config.get("project", {})
249
+ if isinstance(project, dict):
250
+ status = str(project.get("status") or "").strip().lower()
251
+ if status and status not in VALID_PROJECT_STATUSES:
252
+ # Lazy import to avoid circular dep at module load.
253
+ from hyperi_ci.common import warn
254
+
255
+ warn(
256
+ f"Unknown project.status '{status}' — expected one of "
257
+ f"{', '.join(VALID_PROJECT_STATUSES)} (or unset). "
258
+ f"Treating as unset for logging purposes."
259
+ )
260
+
232
261
  _config_cache = CIConfig(
233
262
  language=config.get("language", "none"),
234
263
  ci_min_python_version=config.get("ci_min_python_version", "3.9"),
@@ -94,25 +94,91 @@ def _depends_on(manifest: Path, package_name: str) -> bool:
94
94
  dependencies = ["hyperi-pylib>=2.24"]
95
95
  dependencies = ["hyperi-pylib[metrics]>=2.24"]
96
96
 
97
- Doesn't try to parse TOML because:
97
+ **Self-match exclusion.** If the manifest declares its own package
98
+ name as ``package_name`` (i.e. this manifest IS the library, not a
99
+ consumer of it), returns False. Without this, the library's own
100
+ repo gets misdispatched as a Tier 1/2 consumer and the
101
+ deployment-artefact producer fails with "no Rust binary found" /
102
+ equivalent. The check is generic — applies to any rustlib /
103
+ pylib / future *lib and to consumer projects whose own name
104
+ happens to share a prefix.
105
+
106
+ Doesn't try to parse TOML beyond pulling the ``name`` field out of
107
+ a recognised top-level section because:
108
+
98
109
  1. Avoids hauling `tomllib` in just for tier detection.
99
110
  2. Catches every form (workspace inheritance, extras, comments)
100
111
  that a stricter parse would have to handle case by case.
101
- 3. False positives only if the package name appears in another
102
- context (e.g., a comment mentioning it) acceptable, since
103
- the consequence is "we try to invoke generate-artefacts and
104
- the binary fails clearly" rather than silent miscategorisation.
112
+ 3. False positives in the dep-match are bounded: the consequence
113
+ is "we try to invoke generate-artefacts and the binary fails
114
+ clearly" rather than silent miscategorisation.
105
115
 
106
116
  Args:
107
117
  manifest: Path to Cargo.toml or pyproject.toml.
108
118
  package_name: Dependency name to look for.
109
119
 
110
120
  Returns:
111
- True if the substring appears in the manifest's text.
121
+ True if the substring appears AND the manifest isn't itself
122
+ the named package; False otherwise.
112
123
 
113
124
  """
114
125
  try:
115
126
  text = manifest.read_text(encoding="utf-8", errors="replace")
116
127
  except OSError:
117
128
  return False
118
- return package_name in text
129
+ if package_name not in text:
130
+ return False
131
+ return _manifest_self_name(text) != package_name
132
+
133
+
134
+ # Top-level tables whose ``name`` field is the manifest's own package
135
+ # name. Listed in scan precedence — the first match wins, so a Cargo
136
+ # manifest's ``[package] name`` beats any later table (unlikely in
137
+ # Cargo, but pyproject.toml can legitimately have both ``[project]``
138
+ # and ``[tool.poetry]`` and we treat them equivalently).
139
+ _SELF_NAME_SECTIONS: frozenset[str] = frozenset(
140
+ {"[package]", "[project]", "[tool.poetry]"}
141
+ )
142
+
143
+
144
+ def _manifest_self_name(text: str) -> str | None:
145
+ """Extract the manifest's own package name, if declared.
146
+
147
+ Scans for a ``name = "..."`` (or single-quoted) line inside one of
148
+ the recognised self-name sections (:data:`_SELF_NAME_SECTIONS`).
149
+ Returns the first match. No TOML parser — line-scoped, tolerant of
150
+ extra whitespace around ``=``.
151
+
152
+ Args:
153
+ text: Full manifest text.
154
+
155
+ Returns:
156
+ The declared package name, or ``None`` if no recognised
157
+ declaration is found.
158
+
159
+ """
160
+ current_section: str | None = None
161
+ for raw in text.splitlines():
162
+ stripped = raw.strip()
163
+ if stripped.startswith("[") and stripped.endswith("]"):
164
+ current_section = stripped
165
+ continue
166
+ if current_section not in _SELF_NAME_SECTIONS:
167
+ continue
168
+ # Cheap pre-filter: skip lines that don't even start with "name".
169
+ if not stripped.startswith("name"):
170
+ continue
171
+ eq = stripped.find("=")
172
+ if eq < 0:
173
+ continue
174
+ # Confirm the LHS is exactly "name" (avoids matching "name-foo").
175
+ lhs = stripped[:eq].strip()
176
+ if lhs != "name":
177
+ continue
178
+ rhs = stripped[eq + 1 :].strip()
179
+ # Strip a trailing inline comment if present.
180
+ if "#" in rhs:
181
+ rhs = rhs[: rhs.index("#")].strip()
182
+ if len(rhs) >= 2 and rhs[0] in {'"', "'"} and rhs[-1] == rhs[0]:
183
+ return rhs[1:-1]
184
+ return None
@@ -29,7 +29,7 @@ from hyperi_ci.common import (
29
29
  success,
30
30
  warn,
31
31
  )
32
- from hyperi_ci.config import CIConfig, load_config
32
+ from hyperi_ci.config import VALID_PROJECT_STATUSES, CIConfig, load_config
33
33
  from hyperi_ci.detect import detect_language
34
34
  from hyperi_ci.quality import commit_validation, gitleaks
35
35
 
@@ -62,6 +62,19 @@ VALID_STAGES = (
62
62
  "publish",
63
63
  )
64
64
 
65
+ # Parenthetical clarifier appended to the "Project status: <X>" log line
66
+ # so a reader of the CI output immediately knows what each value means
67
+ # without having to consult the docs. `ga` and `legacy` defaults are
68
+ # bare — the clarifier carries the signal only where it adds something.
69
+ _STATUS_CLARIFIER: dict[str, str] = {
70
+ "experimental": " — pre-GA, no API commitment",
71
+ "alpha": " — pre-GA, expect breaks",
72
+ "beta": " — pre-GA, polishing",
73
+ "ga": "",
74
+ "legacy": " — being phased out, plan migration",
75
+ "deprecated": " — do not adopt, scheduled for removal",
76
+ }
77
+
65
78
  # Languages that share a handler package. The left-hand name is what
66
79
  # `detect_language()` returns (honest — describes what the project actually
67
80
  # is); the right-hand name is the handler module to dispatch to. Keeping
@@ -387,6 +400,16 @@ def run_stage(
387
400
 
388
401
  config = load_config(reload=True, project_dir=project_dir)
389
402
 
403
+ # Surface project lifecycle status so consumers reading CI logs can
404
+ # immediately see "oh, this isn't GA". Skipped silently when the
405
+ # field is unset — `.hyperi-ci.yaml` need not declare it. Always
406
+ # logged as INFO; the parenthetical clarifier on non-ga values
407
+ # carries the signal without elevating log level (a beta project
408
+ # isn't an error, just a fact).
409
+ status = str(config.get("project.status") or "").strip().lower()
410
+ if status in VALID_PROJECT_STATUSES:
411
+ info(f"Project status: {status}{_STATUS_CLARIFIER.get(status, '')}")
412
+
390
413
  handler = _STAGE_HANDLERS[stage]
391
414
  if stage == "build":
392
415
  rc = handler(language, config, local=local)
@@ -158,6 +158,12 @@ def _render_hyperi_ci_yaml(
158
158
  """Render .hyperi-ci.yaml with language-specific defaults."""
159
159
  config: dict = {
160
160
  "language": language,
161
+ # Information-only: lifecycle stage of the project. Surfaced
162
+ # in CI logs and `hyperi-ci config`. Does not gate any
163
+ # behaviour. New projects default to `experimental` — bump as
164
+ # the project matures. Values: experimental | alpha | beta |
165
+ # ga | legacy | deprecated.
166
+ "project": {"status": "experimental"},
161
167
  "quality": {"enabled": True},
162
168
  "test": {"enabled": True},
163
169
  "build": {"enabled": True, "strategies": ["native"]},
@@ -120,6 +120,105 @@ class TestDetectTier:
120
120
  assert detect_tier(tmp_path) == Tier.PYTHON
121
121
 
122
122
 
123
+ class TestSelfMatchExclusion:
124
+ """A library's own repo is NOT a Tier 1/2 consumer of itself.
125
+
126
+ These tests cover the case where a library's Cargo.toml /
127
+ pyproject.toml has its own package name (e.g. `hyperi-rustlib`)
128
+ in the `[package]` / `[project]` table. The substring match would
129
+ otherwise misdetect the library as a consumer and dispatch the
130
+ Tier 1/2 producer, which then fails with "no binary found".
131
+ """
132
+
133
+ def test_hyperi_rustlib_own_repo_is_not_rust(self, tmp_path: Path) -> None:
134
+ # The library's own Cargo.toml has `name = "hyperi-rustlib"` in
135
+ # [package] but no `hyperi-rustlib = ...` dep line.
136
+ (tmp_path / "Cargo.toml").write_text(
137
+ '[package]\nname = "hyperi-rustlib"\nversion = "2.7.0"\n',
138
+ encoding="utf-8",
139
+ )
140
+ assert detect_tier(tmp_path) == Tier.NONE
141
+
142
+ def test_hyperi_rustlib_with_self_in_dev_deps_still_excluded(
143
+ self, tmp_path: Path
144
+ ) -> None:
145
+ # Hypothetical: library lists its own name in [dev-dependencies]
146
+ # for an example-binary workspace pattern. Still not a consumer.
147
+ (tmp_path / "Cargo.toml").write_text(
148
+ '[package]\nname = "hyperi-rustlib"\nversion = "2.7.0"\n'
149
+ '[dev-dependencies]\nhyperi-rustlib = { path = "." }\n',
150
+ encoding="utf-8",
151
+ )
152
+ assert detect_tier(tmp_path) == Tier.NONE
153
+
154
+ def test_hyperi_pylib_own_repo_is_not_python(self, tmp_path: Path) -> None:
155
+ (tmp_path / "pyproject.toml").write_text(
156
+ '[project]\nname = "hyperi-pylib"\nversion = "2.24.0"\n',
157
+ encoding="utf-8",
158
+ )
159
+ assert detect_tier(tmp_path) == Tier.NONE
160
+
161
+ def test_hyperi_pylib_poetry_section_also_excluded(self, tmp_path: Path) -> None:
162
+ # Poetry-managed projects use [tool.poetry] instead of [project].
163
+ (tmp_path / "pyproject.toml").write_text(
164
+ '[tool.poetry]\nname = "hyperi-pylib"\nversion = "2.24.0"\n',
165
+ encoding="utf-8",
166
+ )
167
+ assert detect_tier(tmp_path) == Tier.NONE
168
+
169
+ def test_single_quoted_name_excluded(self, tmp_path: Path) -> None:
170
+ # TOML allows single-quoted strings — must still match.
171
+ (tmp_path / "Cargo.toml").write_text(
172
+ "[package]\nname = 'hyperi-rustlib'\nversion = '2.7.0'\n",
173
+ encoding="utf-8",
174
+ )
175
+ assert detect_tier(tmp_path) == Tier.NONE
176
+
177
+ def test_consumer_with_real_dep_still_detected(self, tmp_path: Path) -> None:
178
+ # A real consumer has its own name AND lists the library as a dep.
179
+ # Self-match exclusion must not break this case.
180
+ (tmp_path / "Cargo.toml").write_text(
181
+ '[package]\nname = "dfe-loader"\n[dependencies]\nhyperi-rustlib = "2.5"\n',
182
+ encoding="utf-8",
183
+ )
184
+ assert detect_tier(tmp_path) == Tier.RUST
185
+
186
+ def test_consumer_with_similar_prefix_not_misdetected(self, tmp_path: Path) -> None:
187
+ # `name = "hyperi-rustlib-extras"` should NOT count as self-match
188
+ # for `hyperi-rustlib` because the full string differs. This
189
+ # consumer DOES depend on hyperi-rustlib.
190
+ (tmp_path / "Cargo.toml").write_text(
191
+ '[package]\nname = "hyperi-rustlib-extras"\n'
192
+ '[dependencies]\nhyperi-rustlib = "2.5"\n',
193
+ encoding="utf-8",
194
+ )
195
+ assert detect_tier(tmp_path) == Tier.RUST
196
+
197
+ def test_name_with_trailing_comment_handled(self, tmp_path: Path) -> None:
198
+ # Inline comments after the name field shouldn't break parsing.
199
+ (tmp_path / "Cargo.toml").write_text(
200
+ '[package]\nname = "hyperi-rustlib" # the library itself\n',
201
+ encoding="utf-8",
202
+ )
203
+ assert detect_tier(tmp_path) == Tier.NONE
204
+
205
+ def test_name_outside_recognised_section_not_self_match(
206
+ self, tmp_path: Path
207
+ ) -> None:
208
+ # A `name = "hyperi-rustlib"` in some other table (e.g.
209
+ # [features.something]) shouldn't count as the manifest's own
210
+ # name. We only treat [package] / [project] / [tool.poetry] as
211
+ # self-name sections.
212
+ (tmp_path / "Cargo.toml").write_text(
213
+ '[package]\nname = "consumer-app"\n'
214
+ "[features.weird]\n"
215
+ 'name = "hyperi-rustlib"\n'
216
+ '[dependencies]\nhyperi-rustlib = "2.5"\n',
217
+ encoding="utf-8",
218
+ )
219
+ assert detect_tier(tmp_path) == Tier.RUST
220
+
221
+
123
222
  class TestTierEnum:
124
223
  """`Tier` enum has the four values expected by callers."""
125
224
 
@@ -11,7 +11,13 @@ from pathlib import Path
11
11
 
12
12
  import pytest
13
13
 
14
- from hyperi_ci.config import CIConfig, _merge_deep, _parse_env_value, load_config
14
+ from hyperi_ci.config import (
15
+ VALID_PROJECT_STATUSES,
16
+ CIConfig,
17
+ _merge_deep,
18
+ _parse_env_value,
19
+ load_config,
20
+ )
15
21
 
16
22
 
17
23
  class TestMergeDeep:
@@ -159,3 +165,65 @@ class TestLoadConfig:
159
165
  monkeypatch.setenv("HYPERCI_LANGUAGE", "golang")
160
166
  config = load_config(reload=True, project_dir=tmp_path)
161
167
  assert config.get("language") == "golang"
168
+
169
+
170
+ class TestProjectStatus:
171
+ """`project.status` is an information-only lifecycle stage field.
172
+
173
+ Surfaced in CI logs and `hyperi-ci config`. Does not gate any
174
+ behaviour. Six valid values; unknown values warn but don't fail.
175
+ """
176
+
177
+ def test_valid_statuses_enum(self) -> None:
178
+ # Lock the vocabulary so a rename/typo elsewhere can't silently
179
+ # break the contract every consumer's `.hyperi-ci.yaml` expects.
180
+ assert VALID_PROJECT_STATUSES == (
181
+ "experimental",
182
+ "alpha",
183
+ "beta",
184
+ "ga",
185
+ "legacy",
186
+ "deprecated",
187
+ )
188
+
189
+ def test_set_status_reads_back(self, tmp_path: Path) -> None:
190
+ (tmp_path / ".hyperi-ci.yaml").write_text(
191
+ "language: rust\nproject:\n status: beta\n",
192
+ )
193
+ import hyperi_ci.config as cfg_mod
194
+
195
+ cfg_mod._config_cache = None
196
+ config = load_config(reload=True, project_dir=tmp_path)
197
+ assert config.get("project.status") == "beta"
198
+
199
+ def test_unset_status_returns_empty_or_none(self, tmp_path: Path) -> None:
200
+ # Default value in defaults.yaml is "" (empty string) — meaning
201
+ # "not declared". Skipping the field in `.hyperi-ci.yaml`
202
+ # leaves the default in place.
203
+ (tmp_path / ".hyperi-ci.yaml").write_text("language: rust\n")
204
+ import hyperi_ci.config as cfg_mod
205
+
206
+ cfg_mod._config_cache = None
207
+ config = load_config(reload=True, project_dir=tmp_path)
208
+ # Either "" (default from defaults.yaml) or None (no key at all)
209
+ # is acceptable — both mean "not declared".
210
+ status = config.get("project.status")
211
+ assert status in ("", None)
212
+
213
+ def test_unknown_status_warns_but_loads(
214
+ self,
215
+ tmp_path: Path,
216
+ capsys: pytest.CaptureFixture[str],
217
+ ) -> None:
218
+ (tmp_path / ".hyperi-ci.yaml").write_text(
219
+ "language: rust\nproject:\n status: stable\n",
220
+ )
221
+ import hyperi_ci.config as cfg_mod
222
+
223
+ cfg_mod._config_cache = None
224
+ config = load_config(reload=True, project_dir=tmp_path)
225
+ # Config must still load — typos can't break the build.
226
+ assert config.language == "rust"
227
+ # The unknown value is preserved in raw config so operators can
228
+ # see what they wrote; only the log line warns.
229
+ assert config.get("project.status") == "stable"
@@ -0,0 +1,96 @@
1
+ # Project: HyperI CI
2
+ # File: tests/unit/test_dispatch_status.py
3
+ # Purpose: Unit tests for project.status logging at stage start
4
+ #
5
+ # License: Proprietary — HYPERI PTY LIMITED
6
+ # Copyright: (c) 2026 HYPERI PTY LIMITED
7
+ """Tests for the `Project status:` log line emitted by the dispatcher.
8
+
9
+ `project.status` is an information-only lifecycle field; the dispatcher
10
+ surfaces it at the top of every stage so log readers see what kind of
11
+ project they're looking at. Behaviour locked in here:
12
+
13
+ - Unset (default) → no log line emitted.
14
+ - Known status (any of VALID_PROJECT_STATUSES) → one INFO line with the
15
+ status and a clarifier phrase that explains what the status means.
16
+ - Unknown status (typo) → warned at config-load time and skipped by the
17
+ dispatcher's status block — verified in `test_config.py`.
18
+
19
+ The clarifier phrase is what makes non-GA stand out without elevating
20
+ log level. A line that reads `Project status: beta — pre-GA, polishing`
21
+ is unmistakable in a wall of INFO without being a WARN.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import pytest
27
+
28
+ from hyperi_ci.dispatch import _STATUS_CLARIFIER
29
+
30
+
31
+ class TestStatusClarifierMap:
32
+ """The clarifier map drives the user-visible signal — lock it in."""
33
+
34
+ def test_ga_has_no_clarifier(self) -> None:
35
+ # GA is the unmarked default — clarifier adds nothing, so the
36
+ # line reads simply "Project status: ga".
37
+ assert _STATUS_CLARIFIER["ga"] == ""
38
+
39
+ def test_pre_ga_statuses_say_pre_ga(self) -> None:
40
+ for pre_ga in ("experimental", "alpha", "beta"):
41
+ assert "pre-GA" in _STATUS_CLARIFIER[pre_ga], (
42
+ f"{pre_ga} clarifier missing 'pre-GA' marker"
43
+ )
44
+
45
+ def test_legacy_signals_migration(self) -> None:
46
+ assert "phased out" in _STATUS_CLARIFIER["legacy"]
47
+ assert "migration" in _STATUS_CLARIFIER["legacy"]
48
+
49
+ def test_deprecated_says_do_not_adopt(self) -> None:
50
+ assert "do not adopt" in _STATUS_CLARIFIER["deprecated"]
51
+
52
+ def test_all_valid_statuses_have_clarifier_entry(self) -> None:
53
+ # If we add a new status to the enum, this test forces us to
54
+ # decide what its clarifier phrase says rather than silently
55
+ # falling through to an empty default.
56
+ from hyperi_ci.config import VALID_PROJECT_STATUSES
57
+
58
+ for status in VALID_PROJECT_STATUSES:
59
+ assert status in _STATUS_CLARIFIER, (
60
+ f"{status} added to VALID_PROJECT_STATUSES but not to "
61
+ f"_STATUS_CLARIFIER — decide what the log line says"
62
+ )
63
+
64
+ def test_clarifier_has_no_warn_style_words(self) -> None:
65
+ # The whole point of the redesign: clarifier carries signal
66
+ # without being alarming. Reject "WARNING", "ERROR", "DANGER",
67
+ # "DO NOT USE" — these elevate tone past the info level the
68
+ # value deserves.
69
+ forbidden = ("WARNING", "ERROR", "DANGER", "DO NOT USE")
70
+ for status, phrase in _STATUS_CLARIFIER.items():
71
+ for word in forbidden:
72
+ assert word.lower() not in phrase.lower(), (
73
+ f"{status} clarifier '{phrase}' contains alarming "
74
+ f"word '{word}' — keep the tone at info level"
75
+ )
76
+
77
+
78
+ class TestStatusLineFormat:
79
+ """Format the dispatcher emits: 'Project status: <value><clarifier>'."""
80
+
81
+ @pytest.mark.parametrize(
82
+ "status,expected_substring",
83
+ [
84
+ ("ga", "Project status: ga"),
85
+ ("beta", "Project status: beta — pre-GA, polishing"),
86
+ ("legacy", "Project status: legacy — being phased out, plan migration"),
87
+ ],
88
+ )
89
+ def test_format_matches_clarifier_map(
90
+ self, status: str, expected_substring: str
91
+ ) -> None:
92
+ # Construct the exact line the dispatcher emits so the format
93
+ # contract is locked. If someone changes the prefix to
94
+ # "Lifecycle:" or drops the em-dash, this test catches it.
95
+ line = f"Project status: {status}{_STATUS_CLARIFIER[status]}"
96
+ assert expected_substring in line