opencomplai-cli 0.9.0__tar.gz → 0.9.2__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 (180) hide show
  1. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/PKG-INFO +2 -2
  2. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/pyproject.toml +2 -2
  3. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/__init__.py +1 -1
  4. opencomplai_cli-0.9.2/src/opencomplai_cli/ai_status.py +37 -0
  5. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/dashboard.py +6 -6
  6. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/halt.py +1 -1
  7. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/oversight_log.py +3 -1
  8. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/portfolio.py +37 -1
  9. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/qms.py +3 -3
  10. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/github_actions.py +2 -2
  11. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/gitlab_ci.py +2 -2
  12. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/main.py +20 -8
  13. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agents_cmd.py +38 -1
  14. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ai_status.py +69 -0
  15. opencomplai_cli-0.9.2/tests/test_ai_status_cache.py +45 -0
  16. opencomplai_cli-0.9.2/tests/test_changelog_newest_block.py +82 -0
  17. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_changelog_unreleased.py +16 -15
  18. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_portfolio.py +131 -10
  19. opencomplai_cli-0.9.2/tests/test_check_report_exit_codes.py +60 -0
  20. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_reports.py +70 -8
  21. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ci_recipes_guide.py +78 -0
  22. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_cli_docs_pages.py +171 -0
  23. opencomplai_cli-0.9.2/tests/test_docs_closeout_guards.py +105 -0
  24. opencomplai_cli-0.9.2/tests/test_docs_wording_guards.py +77 -0
  25. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_works_alongside.py +57 -0
  26. opencomplai_cli-0.9.2/tests/test_encoding.py +258 -0
  27. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_github_action.py +102 -0
  28. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_golden_eu_output.py +9 -3
  29. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_install_scripts.py +32 -0
  30. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_oss_sync_gates.py +41 -0
  31. opencomplai_cli-0.9.2/tests/test_release_pins_parity.py +74 -0
  32. opencomplai_cli-0.9.0/tests/test_encoding.py +0 -128
  33. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/.gitignore +0 -0
  34. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/README.md +0 -0
  35. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/_encoding.py +0 -0
  36. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/acceptance_gate.py +0 -0
  37. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/agent_inventory_payload.py +0 -0
  38. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/check_signing.py +0 -0
  39. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/__init__.py +0 -0
  40. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/accept.py +0 -0
  41. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/agents.py +0 -0
  42. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/agents_attest.py +0 -0
  43. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/agents_runtime.py +0 -0
  44. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/agents_verify_log.py +0 -0
  45. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/checker.py +0 -0
  46. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/checker_manifest.py +0 -0
  47. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/controls.py +0 -0
  48. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/deployer_pack.py +0 -0
  49. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/diff.py +0 -0
  50. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/incident.py +0 -0
  51. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/instructions.py +0 -0
  52. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/interactive_init.py +0 -0
  53. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/push.py +0 -0
  54. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/rules.py +0 -0
  55. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/serve.py +0 -0
  56. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/verify.py +0 -0
  57. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/commands/verify_dossier.py +0 -0
  58. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/__init__.py +0 -0
  59. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/connectors/_check_args.py +0 -0
  60. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/data/checker-local.html +0 -0
  61. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/exit_codes.py +0 -0
  62. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/inputs.py +0 -0
  63. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/mapped_view.py +0 -0
  64. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/oidc_client.py +0 -0
  65. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/oversight_payload.py +0 -0
  66. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/publish.py +0 -0
  67. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/report_outputs.py +0 -0
  68. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/src/opencomplai_cli/summaries_emit.py +0 -0
  69. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/__init__.py +0 -0
  70. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/conftest.py +0 -0
  71. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/agents/sample-manifest.json +0 -0
  72. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/agents/sample-scan-report.json +0 -0
  73. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/check_reports_artifact.json +0 -0
  74. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/sarif-2.1.0.subset.schema.json +0 -0
  75. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/fixtures/trap/trap-manifest.json +0 -0
  76. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_eu_artifact.json +0 -0
  77. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_eu_stdout.json +0 -0
  78. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_nist_artifact.json +0 -0
  79. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_nist_stdout.json +0 -0
  80. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_reports.junit.xml +0 -0
  81. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_reports.sarif.json +0 -0
  82. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/check_reports.summary.md +0 -0
  83. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_chatbot.json +0 -0
  84. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_chatbot.txt +0 -0
  85. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_credit.json +0 -0
  86. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_credit.txt +0 -0
  87. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_credit_nist.json +0 -0
  88. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/gaps_credit_nist.txt +0 -0
  89. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/init_manifest.json +0 -0
  90. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/recommend_files.txt +0 -0
  91. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/golden/report.html +0 -0
  92. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_accept_cmd.py +0 -0
  93. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agent_inventory_payload.py +0 -0
  94. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agents_attest.py +0 -0
  95. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agents_import_cmd.py +0 -0
  96. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_agents_verify_log.py +0 -0
  97. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ai_docs_claims.py +0 -0
  98. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ai_preload.py +0 -0
  99. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_acceptance.py +0 -0
  100. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_acceptance_matrix.py +0 -0
  101. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_gate.py +0 -0
  102. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_output_dir.py +0 -0
  103. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_roles.py +0 -0
  104. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_sign_gate.py +0 -0
  105. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_signing.py +0 -0
  106. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_check_summaries.py +0 -0
  107. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_checker_cmd.py +0 -0
  108. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_checker_roles.py +0 -0
  109. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ci_integration_guide.py +0 -0
  110. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_ci_templates.py +0 -0
  111. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_cli.py +0 -0
  112. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_connector_check_args.py +0 -0
  113. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_connectors.py +0 -0
  114. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_controls_cmd.py +0 -0
  115. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_controls_sync.py +0 -0
  116. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_dashboard_cmd.py +0 -0
  117. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_dead_code_nits.py +0 -0
  118. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_deployer_pack_cmd.py +0 -0
  119. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_diff_cmd.py +0 -0
  120. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_generate_push.py +0 -0
  121. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_generate_render.py +0 -0
  122. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_generate_wiring.py +0 -0
  123. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_golive_wording.py +0 -0
  124. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_python_snippets.py +0 -0
  125. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_signing_claims.py +0 -0
  126. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_docs_truth.py +0 -0
  127. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_eu_ai_scan_golden.py +0 -0
  128. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_eval_provider_base_url.py +0 -0
  129. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_eval_seed_cmd.py +0 -0
  130. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_evidence_provenance.py +0 -0
  131. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_exit_codes.py +0 -0
  132. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_fria_generate.py +0 -0
  133. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gap_report_input.py +0 -0
  134. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_applicability.py +0 -0
  135. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_gpai.py +0 -0
  136. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_map_to.py +0 -0
  137. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_mapped_column.py +0 -0
  138. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_multi_target.py +0 -0
  139. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gaps_nist_rmf_target.py +0 -0
  140. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_gate_demo.py +0 -0
  141. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_halt_wire.py +0 -0
  142. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_homebrew_formula.py +0 -0
  143. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_incident_cmd.py +0 -0
  144. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_incident_log_cli.py +0 -0
  145. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_init_model_card.py +0 -0
  146. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_input_robustness.py +0 -0
  147. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_instructions_generate_cmd.py +0 -0
  148. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_iso_42001_target.py +0 -0
  149. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_no_direct_click.py +0 -0
  150. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_oidc_client.py +0 -0
  151. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_oversight_log.py +0 -0
  152. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_oversight_payload.py +0 -0
  153. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_precommit_hooks_pin.py +0 -0
  154. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_provenance_stamp.py +0 -0
  155. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_public_tree_safety.py +0 -0
  156. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_publish.py +0 -0
  157. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_push_cmd.py +0 -0
  158. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_push_cmd_dossier.py +0 -0
  159. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_qms_generate_cmd.py +0 -0
  160. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_qms_generate_manifest.py +0 -0
  161. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_qms_scaffold_cmd.py +0 -0
  162. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_readme_command_table.py +0 -0
  163. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_readme_quickstart_pin.py +0 -0
  164. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_recommend_gpai.py +0 -0
  165. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_recommend_qms_clauses.py +0 -0
  166. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_regulatory_timeline_cmd.py +0 -0
  167. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_rules_changelog_cmd.py +0 -0
  168. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_scan_cmd.py +0 -0
  169. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_scan_quick.py +0 -0
  170. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_security_claims.py +0 -0
  171. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_serve_docs.py +0 -0
  172. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_service_check.py +0 -0
  173. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_sort_priority.py +0 -0
  174. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_summaries_emit.py +0 -0
  175. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_validate_manifest_agents.py +0 -0
  176. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_validate_manifest_oversight.py +0 -0
  177. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_verify_cmd.py +0 -0
  178. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_verify_dossier_kind.py +0 -0
  179. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_verify_kinds.py +0 -0
  180. {opencomplai_cli-0.9.0 → opencomplai_cli-0.9.2}/tests/test_verify_sbom.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: opencomplai-cli
3
- Version: 0.9.0
3
+ Version: 0.9.2
4
4
  Summary: Opencomplai command-line interface for AI compliance: the EU AI Act, and NIST AI RMF derived from the same evidence
5
5
  Project-URL: Homepage, https://opencomplai.com
6
6
  Project-URL: Documentation, https://docs.opencomplai.com
@@ -18,7 +18,7 @@ Classifier: Programming Language :: Python :: 3.11
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Software Development :: Quality Assurance
20
20
  Requires-Python: >=3.11
21
- Requires-Dist: opencomplai-core[reports]>=0.9.0
21
+ Requires-Dist: opencomplai-core[reports]>=0.9.2
22
22
  Requires-Dist: questionary>=2.0.0
23
23
  Requires-Dist: rich>=13.0.0
24
24
  Requires-Dist: typer>=0.9.0
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "opencomplai-cli"
3
- version = "0.9.0"
3
+ version = "0.9.2"
4
4
  description = "Opencomplai command-line interface for AI compliance: the EU AI Act, and NIST AI RMF derived from the same evidence"
5
5
  readme = "README.md"
6
6
  license = { text = "AGPL-3.0-only" }
@@ -17,7 +17,7 @@ classifiers = [
17
17
  "Topic :: Software Development :: Quality Assurance",
18
18
  ]
19
19
  dependencies = [
20
- "opencomplai-core[reports]>=0.9.0",
20
+ "opencomplai-core[reports]>=0.9.2",
21
21
  "typer>=0.9.0",
22
22
  "rich>=13.0.0",
23
23
  "questionary>=2.0.0",
@@ -1,6 +1,6 @@
1
1
  """Opencomplai CLI."""
2
2
 
3
- __version__ = "0.9.0"
3
+ __version__ = "0.9.2"
4
4
 
5
5
  # Canonical project metadata — single source of truth for the `--version`
6
6
  # flag and the `opencomplai info` command. Mirrors the [project] table in
@@ -0,0 +1,37 @@
1
+ """Pure helpers for `opencomplai ai status` (stdlib only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+
8
+
9
+ def cache_files(cache_dir: Path) -> list[tuple[str, int]]:
10
+ """Regular files under cache_dir (recursive) as (relative posix path, bytes), sorted."""
11
+ root = Path(cache_dir)
12
+ if not root.is_dir():
13
+ return []
14
+ found: list[tuple[str, int]] = []
15
+ for dirpath, _dirs, names in os.walk(root, followlinks=False):
16
+ for name in names:
17
+ p = Path(dirpath) / name
18
+ try:
19
+ if p.is_symlink() or not p.is_file():
20
+ continue
21
+ size = p.stat().st_size
22
+ except OSError:
23
+ continue
24
+ found.append((p.relative_to(root).as_posix(), size))
25
+ return sorted(found)
26
+
27
+
28
+ def format_cache_size(total_bytes: int) -> str:
29
+ if total_bytes <= 0:
30
+ return "0 MB"
31
+ if total_bytes < 1024 * 1024:
32
+ return "<1 MB"
33
+ return f"{total_bytes // (1024 * 1024)} MB"
34
+
35
+
36
+ def spec_size_label(size_mb: int) -> str:
37
+ return "unknown" if size_mb <= 0 else f"~{size_mb} MB"
@@ -71,7 +71,7 @@ def _write_client_secret(raw_secret: str) -> None:
71
71
  doesn't apply the same way.
72
72
  """
73
73
  _OPENCOMPLAI_DIR.mkdir(parents=True, exist_ok=True)
74
- _CLIENT_SECRET_FILE.write_text(raw_secret)
74
+ _CLIENT_SECRET_FILE.write_text(raw_secret, encoding="utf-8")
75
75
  try:
76
76
  os.chmod(_CLIENT_SECRET_FILE, 0o600)
77
77
  except (OSError, NotImplementedError):
@@ -81,7 +81,7 @@ def _write_client_secret(raw_secret: str) -> None:
81
81
  def _read_client_secret() -> str | None:
82
82
  if not _CLIENT_SECRET_FILE.exists():
83
83
  return None
84
- secret = _CLIENT_SECRET_FILE.read_text().strip()
84
+ secret = _CLIENT_SECRET_FILE.read_text(encoding="utf-8").strip()
85
85
  return secret or None
86
86
 
87
87
 
@@ -117,14 +117,14 @@ def _load_egress_allowlist() -> dict:
117
117
  if not _EGRESS_ALLOWLIST.exists():
118
118
  return {}
119
119
  try:
120
- return json.loads(_EGRESS_ALLOWLIST.read_text())
120
+ return json.loads(_EGRESS_ALLOWLIST.read_text(encoding="utf-8"))
121
121
  except Exception:
122
122
  return {}
123
123
 
124
124
 
125
125
  def _write_egress_allowlist(data: dict) -> None:
126
126
  _OPENCOMPLAI_DIR.mkdir(parents=True, exist_ok=True)
127
- _EGRESS_ALLOWLIST.write_text(json.dumps(data, indent=2) + "\n")
127
+ _EGRESS_ALLOWLIST.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
128
128
 
129
129
 
130
130
  def _append_ledger_event(event: dict) -> None:
@@ -137,7 +137,7 @@ def _ledger_events() -> list[dict]:
137
137
  if not _LEDGER_FILE.exists():
138
138
  return []
139
139
  events: list[dict] = []
140
- for line in _LEDGER_FILE.read_text().splitlines():
140
+ for line in _LEDGER_FILE.read_text(encoding="utf-8").splitlines():
141
141
  try:
142
142
  events.append(json.loads(line))
143
143
  except Exception:
@@ -236,7 +236,7 @@ def enroll(
236
236
  "[red]Error:[/red] signing.pub not found — run `opencomplai init` first."
237
237
  )
238
238
  sys.exit(2)
239
- public_key_pem = _SIGNING_PUB.read_text().strip()
239
+ public_key_pem = _SIGNING_PUB.read_text(encoding="utf-8").strip()
240
240
 
241
241
  # --- POST /v1/admin/enroll ---
242
242
  try:
@@ -235,7 +235,7 @@ def resume_cmd(
235
235
  if token_str.startswith("@"):
236
236
  token_path = Path(token_str[1:])
237
237
  try:
238
- token_str = token_path.read_text().strip()
238
+ token_str = token_path.read_text(encoding="utf-8").strip()
239
239
  except OSError as exc:
240
240
  main_mod.err_console.print(
241
241
  f"[red]Error:[/red] cannot read approval token file: {exc}"
@@ -56,7 +56,9 @@ def declared_roles(manifest_path: Path) -> list[str]:
56
56
  if not manifest_path.exists():
57
57
  return []
58
58
  try:
59
- manifest = SystemManifest.model_validate_json(manifest_path.read_text())
59
+ manifest = SystemManifest.model_validate_json(
60
+ manifest_path.read_text(encoding="utf-8")
61
+ )
60
62
  except (OSError, ValueError) as exc:
61
63
  raise ValueError(f"cannot read manifest {manifest_path}: {exc}") from None
62
64
  if manifest.human_oversight is None:
@@ -66,6 +66,11 @@ def safe_dir_name(system_id: str) -> str:
66
66
  return "_" if name in ("", ".", "..") else name
67
67
 
68
68
 
69
+ def _per_system_path(path: Path, sid: str) -> Path:
70
+ """`r.xml` + `alpha` -> `r.alpha.xml`, so systems never share a report file."""
71
+ return path.with_name(f"{path.stem}.{safe_dir_name(sid)}{path.suffix}")
72
+
73
+
69
74
  def _read_raw(path: Path) -> object:
70
75
  from opencomplai_cli.inputs import read_json_file
71
76
 
@@ -75,6 +80,30 @@ def _read_raw(path: Path) -> object:
75
80
  return None
76
81
 
77
82
 
83
+ def _per_system_sample_set(path: Path | None, sid: str, sys_dir: Path) -> Path | None:
84
+ """Copy of the shared sample set with `system_id` set to `sid`, written to
85
+ `sys_dir`. Anything unreadable or malformed returns `path` unchanged so the
86
+ child run prints its normal error; the shared file is never modified."""
87
+ if path is None:
88
+ return None
89
+ try:
90
+ raw = json.loads(Path(path).read_text(encoding="utf-8-sig"))
91
+ except (OSError, ValueError):
92
+ return path
93
+ if not isinstance(raw, dict) or not isinstance(raw.get("system_id"), str):
94
+ return path
95
+ try:
96
+ sys_dir.mkdir(parents=True, exist_ok=True)
97
+ copy = sys_dir / "sample-set.json"
98
+ copy.write_text(
99
+ json.dumps({**raw, "system_id": sid}, indent=2, ensure_ascii=False),
100
+ encoding="utf-8",
101
+ )
102
+ except OSError:
103
+ return path
104
+ return copy
105
+
106
+
78
107
  def _system_id(path: Path) -> str:
79
108
  raw = _read_raw(path)
80
109
  sid = raw.get("system_id") if isinstance(raw, dict) else None
@@ -123,6 +152,13 @@ def run_portfolio(
123
152
  "output_dir": sys_dir,
124
153
  "output": _consoles()[2].human,
125
154
  }
155
+ for key in ("report_junit", "sarif_output", "summary_md"):
156
+ if params.get(key) is not None:
157
+ params[key] = _per_system_path(Path(params[key]), sid)
158
+ if params.get("sample_set_file") is not None:
159
+ params["sample_set_file"] = _per_system_sample_set(
160
+ params["sample_set_file"], sid, sys_dir
161
+ )
126
162
  try:
127
163
  invoke(**params)
128
164
  code = 0
@@ -159,7 +195,7 @@ def run_portfolio(
159
195
  out = ctx_params["output"]
160
196
  console = _consoles()[0]
161
197
  if getattr(out, "value", out) == "json":
162
- console.print_json(path.read_text())
198
+ console.print_json(path.read_text(encoding="utf-8"))
163
199
  else:
164
200
  console.print("\n[bold]Portfolio[/bold]")
165
201
  for e in sorted(entries, key=lambda e: e["system_id"]):
@@ -124,7 +124,7 @@ def generate_cmd(
124
124
  sys.exit(2)
125
125
  try:
126
126
  manifest = SystemManifest.model_validate(
127
- json.loads(manifest_file.read_text())
127
+ json.loads(manifest_file.read_text(encoding="utf-8"))
128
128
  )
129
129
  except Exception as e:
130
130
  _main.err_console.print(f"[red]Validation error:[/red] {e}")
@@ -134,7 +134,7 @@ def generate_cmd(
134
134
  if scan_report_file.exists():
135
135
  try:
136
136
  scan_report = CorroborationReport.model_validate(
137
- json.loads(scan_report_file.read_text())
137
+ json.loads(scan_report_file.read_text(encoding="utf-8"))
138
138
  )
139
139
  except Exception as exc:
140
140
  _main.err_console.print(
@@ -145,7 +145,7 @@ def generate_cmd(
145
145
  if eval_report_file.exists():
146
146
  try:
147
147
  eval_report = EvalReport.model_validate(
148
- json.loads(eval_report_file.read_text())
148
+ json.loads(eval_report_file.read_text(encoding="utf-8"))
149
149
  )
150
150
  except Exception as exc:
151
151
  _main.err_console.print(
@@ -125,7 +125,7 @@ def _gha_cmd(cmd: str, value: str) -> None:
125
125
  def _set_output(name: str, value: str) -> None:
126
126
  output_file = os.environ.get("GITHUB_OUTPUT", "")
127
127
  if output_file:
128
- with open(output_file, "a") as fh:
128
+ with open(output_file, "a", encoding="utf-8") as fh:
129
129
  fh.write(f"{name}={value}\n")
130
130
  else:
131
131
  _gha_cmd("set-output", f"name={name}::{value}")
@@ -134,7 +134,7 @@ def _set_output(name: str, value: str) -> None:
134
134
  def _append_summary(markdown: str) -> None:
135
135
  summary_file = os.environ.get("GITHUB_STEP_SUMMARY", "")
136
136
  if summary_file:
137
- with open(summary_file, "a") as fh:
137
+ with open(summary_file, "a", encoding="utf-8") as fh:
138
138
  fh.write(markdown + "\n")
139
139
 
140
140
 
@@ -216,7 +216,7 @@ def run_connector(
216
216
  # JUnit XML artifact.
217
217
  xml_content = _build_junit_xml(artifact_result, stdout)
218
218
  try:
219
- with open(junit_path, "w") as fh:
219
+ with open(junit_path, "w", encoding="utf-8") as fh:
220
220
  fh.write(xml_content)
221
221
  except OSError:
222
222
  pass
@@ -307,7 +307,7 @@ def _write_dotenv(path: str, artifact: dict) -> None:
307
307
  f"OPENCOMPLAI_CONTENT_HASH={artifact.get('content_hash', '')}",
308
308
  ]
309
309
  try:
310
- with open(path, "w") as fh:
310
+ with open(path, "w", encoding="utf-8") as fh:
311
311
  fh.write("\n".join(lines) + "\n")
312
312
  except OSError:
313
313
  pass
@@ -5028,9 +5028,11 @@ def ai_configure_cmd(
5028
5028
  else:
5029
5029
  import questionary
5030
5030
 
5031
+ from opencomplai_cli.ai_status import spec_size_label
5032
+
5031
5033
  choices = []
5032
5034
  for mid, spec in MODEL_CATALOG.items():
5033
- label = f"{mid:<28} [{spec.size_mb} MB, {spec.license}, {spec.runtime}]"
5035
+ label = f"{mid:<28} [{spec_size_label(spec.size_mb)}, {spec.license}, {spec.runtime}]"
5034
5036
  if mid == "qwen2.5-coder-1.5b":
5035
5037
  label += " ← recommended"
5036
5038
  if mid == "codebert-onnx":
@@ -5068,6 +5070,12 @@ def ai_status_cmd() -> None:
5068
5070
  from opencomplai_ai.config import get_active_model, get_cache_dir
5069
5071
  from opencomplai_ai.models import MODEL_CATALOG
5070
5072
 
5073
+ from opencomplai_cli.ai_status import (
5074
+ cache_files,
5075
+ format_cache_size,
5076
+ spec_size_label,
5077
+ )
5078
+
5071
5079
  active = get_active_model()
5072
5080
  spec = MODEL_CATALOG.get(active)
5073
5081
  cache_dir = get_cache_dir()
@@ -5076,7 +5084,7 @@ def ai_status_cmd() -> None:
5076
5084
  console.print(f" active model : {active}")
5077
5085
  if spec:
5078
5086
  console.print(f" display name : {spec.display_name}")
5079
- console.print(f" size : ~{spec.size_mb} MB")
5087
+ console.print(f" size : {spec_size_label(spec.size_mb)}")
5080
5088
  console.print(f" runtime : {spec.runtime}")
5081
5089
  console.print(f" license : {spec.license}")
5082
5090
  deep_status = (
@@ -5088,17 +5096,21 @@ def ai_status_cmd() -> None:
5088
5096
  console.print(f" llama-cpp : {deep_status}")
5089
5097
  console.print(f" cache dir : {cache_dir}")
5090
5098
 
5091
- if cache_dir.exists():
5092
- total = sum(f.stat().st_size for f in cache_dir.iterdir() if f.is_file())
5093
- console.print(f" cache size : {total // (1024 * 1024)} MB")
5094
- cached = [f.name for f in cache_dir.iterdir() if f.is_file()]
5095
- if cached:
5096
- console.print(f" cached files : {', '.join(cached)}")
5099
+ files = cache_files(cache_dir)
5100
+ if files:
5101
+ from rich.markup import escape
5102
+
5103
+ console.print(
5104
+ f" cache size : {format_cache_size(sum(n for _, n in files))}"
5105
+ )
5106
+ console.print(f" cached files : {', '.join(escape(p) for p, _ in files)}")
5097
5107
  elif spec is None or spec.needs_preload:
5098
5108
  # codebert-onnx (deterministic matcher) and saas (cloud client) have no
5099
5109
  # local artifact, so an empty cache is not a missing download. Models
5100
5110
  # downloaded earlier are still listed above when the cache exists.
5101
5111
  console.print(" cache dir : (empty — model not yet downloaded)")
5112
+ elif cache_dir.exists():
5113
+ console.print(" cache size : 0 MB")
5102
5114
  console.print()
5103
5115
 
5104
5116
 
@@ -17,7 +17,9 @@ runner = CliRunner()
17
17
  FIXTURES = Path(__file__).parent / "fixtures" / "agents"
18
18
  MANIFEST = str(FIXTURES / "sample-manifest.json")
19
19
  SCAN = str(FIXTURES / "sample-scan-report.json")
20
- _MET = re.compile(r"\bMet\b")
20
+ _MET = re.compile(
21
+ r'\bMet\b|\bMET\b|(?i:"(?:status|verdict|result)"\s*:\s*"met"|\|\s*met\s*\||\bstatus\s*[:=]\s*met\b)'
22
+ )
21
23
 
22
24
 
23
25
  @pytest.fixture(autouse=True)
@@ -130,6 +132,41 @@ def test_check_never_emits_met(tmp_path):
130
132
  assert "not a compliance verdict" in out
131
133
 
132
134
 
135
+ @pytest.mark.parametrize(
136
+ "text",
137
+ ['{"status": "met"}', '"status": "MET"', "| Met |", "Met"],
138
+ )
139
+ def test_met_guard_matches_every_spelling(text):
140
+ assert _MET.search(text)
141
+
142
+
143
+ @pytest.mark.parametrize(
144
+ "text",
145
+ ['"status": "partial"', "evidence only; not a compliance verdict"],
146
+ )
147
+ def test_met_guard_ignores_other_values(text):
148
+ assert not _MET.search(text)
149
+
150
+
151
+ def _strings(node):
152
+ if isinstance(node, dict):
153
+ for key, value in node.items():
154
+ yield from _strings(key)
155
+ yield from _strings(value)
156
+ elif isinstance(node, list):
157
+ for item in node:
158
+ yield from _strings(item)
159
+ elif isinstance(node, str):
160
+ yield node
161
+
162
+
163
+ def test_check_json_has_no_met_value(tmp_path):
164
+ for scan in (None, SCAN, _clean_scan(tmp_path)):
165
+ extra = ["--scan-report", scan] if scan else []
166
+ data = json.loads(_run("check", "-m", MANIFEST, *extra, "-o", "json").output)
167
+ assert all(s.lower() != "met" for s in _strings(data))
168
+
169
+
133
170
  def test_report_lists_responsibilities_with_review_flag():
134
171
  expected = {r.id for r in get_responsibilities()}
135
172
  data = json.loads(_run("report", "-m", MANIFEST, "--format", "json").output)
@@ -74,3 +74,72 @@ def test_status_for_gguf_model_still_reports_missing_download(monkeypatch, tmp_p
74
74
 
75
75
  assert "llama-cpp" in out
76
76
  assert "not yet downloaded" in out
77
+
78
+
79
+ def _status_with_cache(monkeypatch, cache, model_id: str) -> str:
80
+ monkeypatch.setattr("opencomplai_ai.config.get_active_model", lambda: model_id)
81
+ monkeypatch.setattr("opencomplai_ai.config.get_cache_dir", lambda: cache)
82
+ result = runner.invoke(app, ["ai", "status"])
83
+ assert result.exit_code == 0, result.output
84
+ return result.output
85
+
86
+
87
+ def test_status_counts_nested_cache_file(monkeypatch, tmp_path):
88
+ cache = tmp_path / "cache"
89
+ (cache / "codebert-base").mkdir(parents=True)
90
+ (cache / "codebert-base" / "model.onnx").write_bytes(b"x" * (3 * 1024 * 1024))
91
+
92
+ out = _status_with_cache(monkeypatch, cache, "codebert-onnx")
93
+
94
+ assert "cache size : 3 MB" in out
95
+ assert "codebert-base/model.onnx" in out
96
+ assert "not yet downloaded" not in out
97
+
98
+
99
+ def test_status_small_nested_cache_file_reports_less_than_one_mb(monkeypatch, tmp_path):
100
+ cache = tmp_path / "cache"
101
+ (cache / "sub").mkdir(parents=True)
102
+ (cache / "sub" / "f.bin").write_bytes(b"x" * 10)
103
+
104
+ out = _status_with_cache(monkeypatch, cache, "codebert-onnx")
105
+
106
+ assert "<1 MB" in out
107
+
108
+
109
+ @pytest.mark.parametrize("sub", [False, True])
110
+ def test_status_empty_cache_dir_prints_not_yet_downloaded(monkeypatch, tmp_path, sub):
111
+ cache = tmp_path / "cache"
112
+ cache.mkdir()
113
+ if sub:
114
+ (cache / "empty-sub").mkdir()
115
+
116
+ out = _status_with_cache(monkeypatch, cache, "qwen2.5-coder-1.5b")
117
+
118
+ assert "not yet downloaded" in out
119
+ assert "cache size" not in out
120
+
121
+
122
+ @pytest.mark.parametrize("model_id", ["codebert-onnx", "saas"])
123
+ def test_status_empty_cache_dir_for_no_download_backend_has_no_not_yet_downloaded_line(
124
+ monkeypatch, tmp_path, model_id
125
+ ):
126
+ cache = tmp_path / "cache"
127
+ cache.mkdir()
128
+
129
+ out = _status_with_cache(monkeypatch, cache, model_id)
130
+
131
+ assert "not yet downloaded" not in out
132
+
133
+
134
+ @pytest.mark.parametrize("model_id", ["codebert-onnx", "saas"])
135
+ def test_status_size_mb_zero_prints_unknown(monkeypatch, tmp_path, model_id):
136
+ out = _status(monkeypatch, tmp_path, model_id)
137
+
138
+ assert "size : unknown" in out
139
+ assert "~0 MB" not in out
140
+
141
+
142
+ def test_status_known_size_still_prints_approximate_mb(monkeypatch, tmp_path):
143
+ out = _status(monkeypatch, tmp_path, "qwen2.5-coder-1.5b")
144
+
145
+ assert "~1000 MB" in out
@@ -0,0 +1,45 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+ import pytest
6
+ from opencomplai_cli.ai_status import cache_files, format_cache_size, spec_size_label
7
+
8
+
9
+ def test_cache_files_counts_nested_files_sorted(tmp_path):
10
+ (tmp_path / "b.gguf").write_bytes(b"x" * 5)
11
+ (tmp_path / "codebert-base").mkdir()
12
+ (tmp_path / "codebert-base" / "model.onnx").write_bytes(b"y" * 7)
13
+ assert cache_files(tmp_path) == [("b.gguf", 5), ("codebert-base/model.onnx", 7)]
14
+
15
+
16
+ def test_cache_files_ignores_missing_dir_and_empty_subdirs(tmp_path):
17
+ assert cache_files(tmp_path / "nope") == []
18
+ (tmp_path / "a" / "b").mkdir(parents=True)
19
+ assert cache_files(tmp_path) == []
20
+
21
+
22
+ def test_cache_files_skips_symlinks(tmp_path):
23
+ target = tmp_path / "real.bin"
24
+ target.write_bytes(b"z")
25
+ cache = tmp_path / "cache"
26
+ cache.mkdir()
27
+ try:
28
+ os.symlink(target, cache / "link.bin")
29
+ except (OSError, NotImplementedError):
30
+ pytest.skip("symlinks unavailable")
31
+ assert cache_files(cache) == []
32
+
33
+
34
+ def test_format_cache_size_boundaries():
35
+ assert format_cache_size(0) == "0 MB"
36
+ assert format_cache_size(1) == "<1 MB"
37
+ assert format_cache_size(1048575) == "<1 MB"
38
+ assert format_cache_size(1048576) == "1 MB"
39
+ assert format_cache_size(3 * 1048576 + 5) == "3 MB"
40
+
41
+
42
+ def test_spec_size_label():
43
+ assert spec_size_label(0) == "unknown"
44
+ assert spec_size_label(-1) == "unknown"
45
+ assert spec_size_label(1000) == "~1000 MB"
@@ -0,0 +1,82 @@
1
+ """Structure and hygiene guard for the newest release block of CHANGELOG.md.
2
+
3
+ Text only: the newest ``## [x.y.z]`` block must match the CLI version, keep the
4
+ Keep a Changelog heading order, have its footer link, carry no internal words,
5
+ and share no bullet with the dashboard notes.
6
+ """
7
+
8
+ import re
9
+ import tomllib
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+
14
+ ROOT = Path(__file__).resolve().parents[3]
15
+ CHANGELOG = ROOT / "CHANGELOG.md"
16
+ SAAS_CHANGELOG = ROOT / "dashboard-saas" / "CHANGELOG.md"
17
+ CLI_PYPROJECT = ROOT / "packages" / "cli" / "pyproject.toml"
18
+
19
+ pytestmark = pytest.mark.skipif(
20
+ not CHANGELOG.is_file(), reason="CHANGELOG.md is not in this tree"
21
+ )
22
+
23
+ ORDER = ["Breaking", "Added", "Changed", "Removed", "Fixed", "Security"]
24
+ FORBIDDEN = (
25
+ r"dashboard|hosted|SaaS|tenant|\bSU-\d|\bCL-[A-Z]|HANDOFF|lawyer"
26
+ r"|trust pack|enterprise|private"
27
+ )
28
+ BLOCK_START = re.compile(r"^## \[(\d+\.\d+\.\d+)\]", re.M)
29
+
30
+
31
+ def _newest(path: Path) -> tuple[str, str]:
32
+ text = path.read_text(encoding="utf-8")
33
+ first = BLOCK_START.search(text)
34
+ assert first, f"no release block in {path}"
35
+ nxt = BLOCK_START.search(text, first.end())
36
+ end = nxt.start() if nxt else len(text)
37
+ return first.group(1), text[first.start() : end]
38
+
39
+
40
+ def _bullets(block: str) -> set[str]:
41
+ return {ln for ln in block.splitlines() if ln.startswith("- ")}
42
+
43
+
44
+ def test_newest_block_version_matches_cli():
45
+ version, _ = _newest(CHANGELOG)
46
+ cli = tomllib.loads(CLI_PYPROJECT.read_text(encoding="utf-8"))["project"]["version"]
47
+ assert version == cli
48
+
49
+
50
+ def test_newest_block_headings_in_order():
51
+ _, block = _newest(CHANGELOG)
52
+ headings = re.findall(r"^### (.+?)\s*$", block, re.M)
53
+ assert len(headings) == len(set(headings)), f"duplicate headings: {headings}"
54
+ unknown = [h for h in headings if h not in ORDER]
55
+ assert not unknown, f"unknown headings: {unknown}"
56
+ assert headings == sorted(headings, key=ORDER.index), headings
57
+
58
+
59
+ def test_footer_link_present():
60
+ version, _ = _newest(CHANGELOG)
61
+ link = (
62
+ f"[{version}]: https://github.com/Opencomplai/opencomplai"
63
+ f"/releases/tag/v{version}"
64
+ )
65
+ assert link in CHANGELOG.read_text(encoding="utf-8").splitlines()
66
+
67
+
68
+ def test_newest_release_has_no_forbidden_words():
69
+ _, block = _newest(CHANGELOG)
70
+ pattern = re.compile(FORBIDDEN, re.I)
71
+ hits = [ln for ln in block.splitlines() if pattern.search(ln)]
72
+ assert not hits, f"internal words in the newest block: {hits}"
73
+
74
+
75
+ def test_saas_notes_share_no_bullet_with_public():
76
+ if not SAAS_CHANGELOG.is_file():
77
+ pytest.skip("dashboard-saas/CHANGELOG.md is not in this tree")
78
+ public_version, public = _newest(CHANGELOG)
79
+ saas_version, saas = _newest(SAAS_CHANGELOG)
80
+ assert saas_version == public_version
81
+ shared = _bullets(public) & _bullets(saas)
82
+ assert not shared, f"bullets in both blocks: {sorted(shared)}"