strictcli 0.17.0__tar.gz → 0.18.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 (158) hide show
  1. strictcli-0.18.0/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.18.0/.rlsbl/changes/0.17.0.md +9 -0
  3. strictcli-0.18.0/.rlsbl/changes/0.18.0.jsonl +4 -0
  4. strictcli-0.18.0/.rlsbl/changes/0.18.0.md +5 -0
  5. strictcli-0.18.0/.rlsbl/releases/v0.18.0.toml +8 -0
  6. strictcli-0.18.0/.rlsbl/version +1 -0
  7. {strictcli-0.17.0 → strictcli-0.18.0}/CHANGELOG.md +15 -2
  8. {strictcli-0.17.0 → strictcli-0.18.0}/PKG-INFO +1 -1
  9. {strictcli-0.17.0 → strictcli-0.18.0}/package-lock.json +2 -2
  10. {strictcli-0.17.0 → strictcli-0.18.0}/package.json +1 -1
  11. {strictcli-0.17.0 → strictcli-0.18.0}/pyproject.toml +1 -1
  12. {strictcli-0.17.0 → strictcli-0.18.0}/strictcli/__init__.py +78 -29
  13. strictcli-0.18.0/tests/test_check_public_api.py +435 -0
  14. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_check_runner.py +9 -9
  15. strictcli-0.18.0/todo/.done/public-check-runner-api.md +36 -0
  16. {strictcli-0.17.0 → strictcli-0.18.0}/uv.lock +1 -1
  17. strictcli-0.17.0/.rlsbl/changes/.validated +0 -1
  18. strictcli-0.17.0/.rlsbl/version +0 -1
  19. {strictcli-0.17.0 → strictcli-0.18.0}/.claude/settings.json +0 -0
  20. {strictcli-0.17.0 → strictcli-0.18.0}/.github/workflows/ci.yml +0 -0
  21. {strictcli-0.17.0 → strictcli-0.18.0}/.github/workflows/publish.yml +0 -0
  22. {strictcli-0.17.0 → strictcli-0.18.0}/.gitignore +0 -0
  23. {strictcli-0.17.0 → strictcli-0.18.0}/.npmignore +0 -0
  24. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.claude/settings.json +0 -0
  25. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  26. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  27. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.gitignore +0 -0
  28. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.npmignore +0 -0
  29. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  30. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  31. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  32. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  33. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  34. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  35. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  36. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/CLAUDE.md +0 -0
  37. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/bases/LICENSE +0 -0
  38. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
  39. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.10.0.md +0 -0
  40. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
  41. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.11.0.md +0 -0
  42. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.12.0.jsonl +0 -0
  43. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.12.0.md +0 -0
  44. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.13.0.jsonl +0 -0
  45. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.13.0.md +0 -0
  46. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.14.0.jsonl +0 -0
  47. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.14.0.md +0 -0
  48. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.15.0.jsonl +0 -0
  49. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.15.0.md +0 -0
  50. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.15.1.jsonl +0 -0
  51. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.15.1.md +0 -0
  52. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.16.0.jsonl +0 -0
  53. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.16.0.md +0 -0
  54. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.16.1.jsonl +0 -0
  55. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.16.1.md +0 -0
  56. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.17.0.jsonl +0 -0
  57. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  58. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.4.0.md +0 -0
  59. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  60. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.4.1.md +0 -0
  61. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  62. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.5.0.md +0 -0
  63. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  64. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.6.0.md +0 -0
  65. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  66. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.6.1.md +0 -0
  67. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  68. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.7.0.md +0 -0
  69. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  70. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.7.1.md +0 -0
  71. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  72. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.0.md +0 -0
  73. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  74. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.1.md +0 -0
  75. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  76. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.2.md +0 -0
  77. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  78. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.3.md +0 -0
  79. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  80. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.4.md +0 -0
  81. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  82. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.5.md +0 -0
  83. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  84. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.6.md +0 -0
  85. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  86. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.8.7.md +0 -0
  87. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
  88. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.9.0.md +0 -0
  89. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
  90. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/0.9.1.md +0 -0
  91. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  92. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/config.json +0 -0
  93. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/hashes.json +0 -0
  94. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/hooks/post-release.sh +0 -0
  95. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  96. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/hooks/pre-release.sh +0 -0
  97. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/lint/go.toml +0 -0
  98. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/lint/npm.toml +0 -0
  99. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/lint/python.toml +0 -0
  100. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/unreleased.toml +0 -0
  101. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.10.0.toml +0 -0
  102. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.11.0.toml +0 -0
  103. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.12.0.toml +0 -0
  104. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.13.0.toml +0 -0
  105. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.15.0.toml +0 -0
  106. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.15.1.toml +0 -0
  107. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.16.0.toml +0 -0
  108. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.16.1.toml +0 -0
  109. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.17.0.toml +0 -0
  110. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  111. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  112. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  113. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.9.0.toml +0 -0
  114. {strictcli-0.17.0 → strictcli-0.18.0}/.rlsbl/releases/v0.9.1.toml +0 -0
  115. {strictcli-0.17.0 → strictcli-0.18.0}/.strictcli/schema.json +0 -0
  116. {strictcli-0.17.0 → strictcli-0.18.0}/CLAUDE.md +0 -0
  117. {strictcli-0.17.0 → strictcli-0.18.0}/LICENSE +0 -0
  118. {strictcli-0.17.0 → strictcli-0.18.0}/README.md +0 -0
  119. {strictcli-0.17.0 → strictcli-0.18.0}/index.js +0 -0
  120. {strictcli-0.17.0 → strictcli-0.18.0}/postinstall.js +0 -0
  121. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_arg_default.py +0 -0
  122. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_at_prefix.py +0 -0
  123. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_auto_version.py +0 -0
  124. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_check_command.py +0 -0
  125. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_check_discovery.py +0 -0
  126. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_check_schema.py +0 -0
  127. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_check_types.py +0 -0
  128. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_choices.py +0 -0
  129. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_command_help_suggestion.py +0 -0
  130. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_config.py +0 -0
  131. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_config_file_path.py +0 -0
  132. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_config_set_bugs.py +0 -0
  133. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_deep_nesting.py +0 -0
  134. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_dependencies.py +0 -0
  135. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_deprecated.py +0 -0
  136. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_dump_schema.py +0 -0
  137. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_e2e.py +0 -0
  138. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_env.py +0 -0
  139. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_exit_codes.py +0 -0
  140. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_float_type.py +0 -0
  141. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_global_flags.py +0 -0
  142. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_help.py +0 -0
  143. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_int_type.py +0 -0
  144. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_mutex.py +0 -0
  145. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_nesting.py +0 -0
  146. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_parser.py +0 -0
  147. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_passthrough.py +0 -0
  148. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_registration.py +0 -0
  149. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_repeatable.py +0 -0
  150. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_tagdsl.py +0 -0
  151. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_tags.py +0 -0
  152. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_toml_loading.py +0 -0
  153. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_unique.py +0 -0
  154. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_utilities.py +0 -0
  155. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_validate.py +0 -0
  156. {strictcli-0.17.0 → strictcli-0.18.0}/tests/test_variadic.py +0 -0
  157. {strictcli-0.17.0 → strictcli-0.18.0}/todo/.defer/deferred.md +0 -0
  158. {strictcli-0.17.0 → strictcli-0.18.0}/todo/.done/original-idea.md +0 -0
@@ -0,0 +1 @@
1
+ 9fe0fe3a00effc0c9e39704a0bd0cc865542da83
@@ -0,0 +1,9 @@
1
+ ## 0.17.0
2
+
3
+ ### Breaking
4
+
5
+ - **Breaking.** Repeatable flag defaults are now validated at registration: must be a list, `default=[]` is rejected as redundant, and element types must match the flag type.
6
+
7
+ ### Features
8
+
9
+ - **New.** Help text now displays non-empty repeatable flag defaults as `[default: a, b, c]`.
@@ -0,0 +1,4 @@
1
+ {"commits":["ab63f2e29d95b834aef6e3307d23b53e3f845afe","fc7ae2927aa12b7627c3119c818d9a7e89209e39","db1924cbd143285c857324a8fcf2110718080bfa"],"user_facing":true,"description":"**New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.","type":"feature"}
2
+ {"commits":["6407ea33c3a16a83dcd7c362cda8e98980074a73","edfcf45b0eb1ba4896fcff1d31ea9993c9b363a1","fcaae9bb7895805308a6b9299158ada7f6d45f3b","b6720d2dd1063bff0b49f6d5624608672c0cac33"],"user_facing":false}
3
+ {"commits":["9cc04830fa5b7eef0d87b249e57248415fd769a5","4e89101a5fdb8c0a6f46b62829f22e09b79ec2d1"],"user_facing":false}
4
+ {"commits":["67ee39ff3cdf4d09a9a29c0bdcb9853999878ee9"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.18.0
2
+
3
+ ### Features
4
+
5
+ - **New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.
@@ -0,0 +1,8 @@
1
+ # Version bump type: patch, minor, or major
2
+ bump = "minor"
3
+ # Short description of this release (required)
4
+ description = "Public API for running checks programmatically"
5
+ # Optional context explaining why these changes were made
6
+ context = "Adds App.run_checks(), format_check_results(), format_check_results_json(), and CheckRunResult — a stable public API replacing the private _filter_checks/_resolve_check_order/_run_checks/_check_format_human functions that consumers like rlsbl were forced to use."
7
+ include = ["pypi", "npm"]
8
+ exclude = []
@@ -0,0 +1 @@
1
+ 0.67.1
@@ -2,9 +2,22 @@
2
2
 
3
3
  # Changelog
4
4
 
5
- ## 0.17.0
5
+ ## 0.18.0
6
+
7
+ Public API for running checks programmatically
8
+
9
+ <details>
10
+ <summary>Context</summary>
11
+
12
+ Adds App.run_checks(), format_check_results(), format_check_results_json(), and CheckRunResult — a stable public API replacing the private _filter_checks/_resolve_check_order/_run_checks/_check_format_human functions that consumers like rlsbl were forced to use.
13
+
14
+ </details>
6
15
 
7
- Repeatable flag default validation and help text display
16
+ ### Features
17
+
18
+ - **New.** Public API for running checks programmatically: `App.run_checks()`, `format_check_results()`, `format_check_results_json()`, `CheckRunResult`.
19
+
20
+ ## 0.17.0
8
21
 
9
22
  ### Breaking
10
23
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.17.0
3
+ Version: 0.18.0
4
4
  Summary: A strict, zero-dependency CLI framework for Python
5
5
  Project-URL: Homepage, https://github.com/smm-h/strictcli
6
6
  Project-URL: Repository, https://github.com/smm-h/strictcli
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "strictcli",
9
- "version": "0.17.0",
9
+ "version": "0.18.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.17.0",
3
+ "version": "0.18.0",
4
4
  "description": "A strict, zero-dependency CLI framework for Python (npm wrapper)",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "strictcli"
7
- version = "0.17.0"
7
+ version = "0.18.0"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,12 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.17.0"
5
+ __version__ = "0.18.0"
6
6
 
7
7
  __all__ = [
8
8
  "App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
9
9
  "Implies", "Passthrough", "DeprecatedCommand", "Result", "flag", "arg",
10
- "CheckResult", "CheckContext",
10
+ "CheckResult", "CheckContext", "CheckRunResult",
11
+ "format_check_results", "format_check_results_json",
11
12
  ]
12
13
 
13
14
  import contextlib
@@ -856,6 +857,14 @@ class CheckResult:
856
857
  raise ValueError("CheckResult.message must be a non-empty string")
857
858
 
858
859
 
860
+ @dataclass(frozen=True)
861
+ class CheckRunResult:
862
+ """A named check result returned by App.run_checks()."""
863
+
864
+ name: str
865
+ result: CheckResult
866
+
867
+
859
868
  @runtime_checkable
860
869
  class CheckContext(Protocol):
861
870
  """Minimal interface that tool-specific check contexts must satisfy."""
@@ -1001,6 +1010,38 @@ class App:
1001
1010
  """
1002
1011
  self._check_context_factory = factory
1003
1012
 
1013
+ def run_checks(
1014
+ self,
1015
+ context: CheckContext,
1016
+ *,
1017
+ tag_expr: str | None = None,
1018
+ name_glob: str | None = None,
1019
+ run_all: bool = False,
1020
+ ignore_warnings: bool = False,
1021
+ ) -> tuple[list[CheckRunResult], int]:
1022
+ """Run checks programmatically with filtering and dependency resolution.
1023
+
1024
+ Returns (results, exit_code) where results is a list of CheckRunResult
1025
+ and exit_code is 0 if all pass (or all warn with ignore_warnings), else 1.
1026
+ """
1027
+ if not self._checks_enabled:
1028
+ raise ValueError("checks are not enabled on this App")
1029
+ err = self._validate_check_registrations()
1030
+ if err:
1031
+ raise ValueError(err)
1032
+ selected = _filter_checks(self._check_defs, tag_expr, name_glob, run_all)
1033
+ if not selected:
1034
+ return ([], 0)
1035
+ order = _resolve_check_order(self._check_defs, selected)
1036
+ raw_results, exit_code = _run_checks(
1037
+ self._check_defs, order, context, ignore_warnings,
1038
+ )
1039
+ results = [
1040
+ CheckRunResult(name=name, result=result)
1041
+ for name, result in raw_results
1042
+ ]
1043
+ return (results, exit_code)
1044
+
1004
1045
  def _register_check_command(self) -> None:
1005
1046
  """Register the auto-generated 'check' command when checks.toml exists."""
1006
1047
  app_ref = self # capture for closure
@@ -1048,14 +1089,19 @@ class App:
1048
1089
  )
1049
1090
  return 1
1050
1091
  context = app_ref._check_context_factory()
1051
- results, exit_code = _run_checks(
1052
- app_ref, order, context, ignore_warnings,
1092
+ raw_results, exit_code = _run_checks(
1093
+ app_ref._check_defs, order, context, ignore_warnings,
1053
1094
  )
1054
1095
 
1096
+ results_wrapped = [
1097
+ CheckRunResult(name=n, result=r) for n, r in raw_results
1098
+ ]
1055
1099
  if json:
1056
- _check_format_json(results)
1100
+ print(format_check_results_json(results_wrapped))
1057
1101
  else:
1058
- _check_format_human(results, verbose)
1102
+ output = format_check_results(results_wrapped, verbose)
1103
+ if output:
1104
+ print(output)
1059
1105
 
1060
1106
  return exit_code
1061
1107
 
@@ -3330,7 +3376,7 @@ def _find_cycle(
3330
3376
 
3331
3377
 
3332
3378
  def _run_checks(
3333
- app: "App",
3379
+ check_defs: dict,
3334
3380
  check_names: list[str],
3335
3381
  context: CheckContext,
3336
3382
  ignore_warnings: bool,
@@ -3340,7 +3386,6 @@ def _run_checks(
3340
3386
  Returns (results_list, exit_code). exit_code is 0 if all pass (or all
3341
3387
  warn with ignore_warnings=True), 1 otherwise.
3342
3388
  """
3343
- check_defs = app._check_defs
3344
3389
  results: list[tuple[str, CheckResult]] = []
3345
3390
  failed_checks: set[str] = set()
3346
3391
  exit_code = 0
@@ -3440,39 +3485,43 @@ def _check_dry_run_mode(
3440
3485
  print(f" {i}. {name}")
3441
3486
 
3442
3487
 
3443
- def _check_format_human(
3444
- results: list[tuple[str, CheckResult]], verbose: bool,
3445
- ) -> None:
3446
- """Print check results in human-readable aligned format."""
3488
+
3489
+ def format_check_results(
3490
+ results: list[CheckRunResult], verbose: bool = False,
3491
+ ) -> str:
3492
+ """Format check results as a human-readable aligned string."""
3447
3493
  if not results:
3448
- return
3494
+ return ""
3449
3495
 
3450
- name_width = max(len(name) for name, _ in results)
3496
+ name_width = max(len(r.name) for r in results)
3497
+ lines: list[str] = []
3451
3498
 
3452
- for name, result in results:
3453
- label = _CHECK_STATUS_LABELS[result.status]
3454
- print(f"{label} {name:<{name_width}} {result.message}")
3499
+ for r in results:
3500
+ label = _CHECK_STATUS_LABELS[r.result.status]
3501
+ lines.append(f"{label} {r.name:<{name_width}} {r.result.message}")
3455
3502
 
3456
- show_details = result.details and (
3457
- verbose or result.status in ("fail", "warn", "skip")
3503
+ show_details = r.result.details and (
3504
+ verbose or r.result.status in ("fail", "warn", "skip")
3458
3505
  )
3459
3506
  if show_details:
3460
- for detail in result.details:
3461
- print(f" {detail}")
3507
+ for detail in r.result.details:
3508
+ lines.append(f" {detail}")
3509
+
3510
+ return "\n".join(lines)
3462
3511
 
3463
3512
 
3464
- def _check_format_json(results: list[tuple[str, CheckResult]]) -> None:
3465
- """Print check results as JSON."""
3513
+ def format_check_results_json(results: list[CheckRunResult]) -> str:
3514
+ """Format check results as a JSON string."""
3466
3515
  items = [
3467
3516
  {
3468
- "name": name,
3469
- "status": result.status,
3470
- "message": result.message,
3471
- "details": result.details,
3517
+ "name": r.name,
3518
+ "status": r.result.status,
3519
+ "message": r.result.message,
3520
+ "details": r.result.details if r.result.details is not None else [],
3472
3521
  }
3473
- for name, result in results
3522
+ for r in results
3474
3523
  ]
3475
- print(json.dumps(items, separators=(",", ":")))
3524
+ return json.dumps(items, separators=(",", ":"))
3476
3525
 
3477
3526
 
3478
3527
  # ---------------------------------------------------------------------------