strictcli 0.10.0__tar.gz → 0.12.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 (130) hide show
  1. {strictcli-0.10.0/.rlsbl/bases → strictcli-0.12.0}/.github/workflows/ci.yml +1 -0
  2. {strictcli-0.10.0 → strictcli-0.12.0}/.github/workflows/publish.yml +2 -0
  3. {strictcli-0.10.0 → strictcli-0.12.0/.rlsbl/bases}/.github/workflows/ci.yml +1 -0
  4. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.github/workflows/publish.yml +3 -1
  5. strictcli-0.12.0/.rlsbl/changes/.validated +1 -0
  6. strictcli-0.12.0/.rlsbl/changes/0.11.0.jsonl +7 -0
  7. strictcli-0.12.0/.rlsbl/changes/0.11.0.md +5 -0
  8. strictcli-0.12.0/.rlsbl/changes/0.12.0.jsonl +2 -0
  9. strictcli-0.12.0/.rlsbl/changes/0.12.0.md +5 -0
  10. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/config.json +2 -1
  11. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/hashes.json +2 -2
  12. strictcli-0.12.0/.rlsbl/hooks/pre-checks.sh +10 -0
  13. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/hooks/pre-release.sh +5 -0
  14. strictcli-0.12.0/.rlsbl/releases/v0.12.0.toml +4 -0
  15. strictcli-0.12.0/.rlsbl/releases/v0.9.0.toml +3 -0
  16. strictcli-0.12.0/.rlsbl/version +1 -0
  17. {strictcli-0.10.0 → strictcli-0.12.0}/CHANGELOG.md +12 -0
  18. {strictcli-0.10.0 → strictcli-0.12.0}/PKG-INFO +1 -1
  19. {strictcli-0.10.0 → strictcli-0.12.0}/package-lock.json +2 -2
  20. {strictcli-0.10.0 → strictcli-0.12.0}/package.json +1 -1
  21. {strictcli-0.10.0 → strictcli-0.12.0}/pyproject.toml +1 -1
  22. {strictcli-0.10.0 → strictcli-0.12.0}/strictcli/__init__.py +28 -14
  23. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_check_command.py +17 -9
  24. strictcli-0.12.0/tests/test_check_discovery.py +295 -0
  25. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_check_runner.py +7 -5
  26. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_check_schema.py +14 -6
  27. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_toml_loading.py +94 -5
  28. {strictcli-0.10.0 → strictcli-0.12.0}/uv.lock +1 -1
  29. strictcli-0.10.0/.rlsbl/changes/.validated +0 -1
  30. strictcli-0.10.0/.rlsbl/hooks/pre-checks.sh +0 -5
  31. strictcli-0.10.0/.rlsbl/version +0 -1
  32. strictcli-0.10.0/tests/test_check_discovery.py +0 -177
  33. {strictcli-0.10.0 → strictcli-0.12.0}/.claude/settings.json +0 -0
  34. {strictcli-0.10.0 → strictcli-0.12.0}/.gitignore +0 -0
  35. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.claude/settings.json +0 -0
  36. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.gitignore +0 -0
  37. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  38. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  39. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  40. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  41. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  42. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  43. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  44. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/CLAUDE.md +0 -0
  45. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/bases/LICENSE +0 -0
  46. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
  47. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.10.0.md +0 -0
  48. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  49. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.0.md +0 -0
  50. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  51. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.4.1.md +0 -0
  52. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  53. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.5.0.md +0 -0
  54. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  55. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.0.md +0 -0
  56. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  57. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.6.1.md +0 -0
  58. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  59. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.0.md +0 -0
  60. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  61. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.7.1.md +0 -0
  62. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  63. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.0.md +0 -0
  64. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  65. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.1.md +0 -0
  66. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  67. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.2.md +0 -0
  68. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  69. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.3.md +0 -0
  70. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  71. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.4.md +0 -0
  72. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  73. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.5.md +0 -0
  74. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  75. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.6.md +0 -0
  76. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  77. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.8.7.md +0 -0
  78. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
  79. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.0.md +0 -0
  80. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
  81. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/0.9.1.md +0 -0
  82. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  83. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/hooks/post-release.sh +0 -0
  84. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/lint/go.toml +0 -0
  85. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/lint/npm.toml +0 -0
  86. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/lint/python.toml +0 -0
  87. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/unreleased.toml +0 -0
  88. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.10.0.toml +0 -0
  89. /strictcli-0.10.0/.rlsbl/releases/v0.9.0.toml → /strictcli-0.12.0/.rlsbl/releases/v0.11.0.toml +0 -0
  90. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  91. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  92. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  93. {strictcli-0.10.0 → strictcli-0.12.0}/.rlsbl/releases/v0.9.1.toml +0 -0
  94. {strictcli-0.10.0 → strictcli-0.12.0}/.strictcli/schema.json +0 -0
  95. {strictcli-0.10.0 → strictcli-0.12.0}/CLAUDE.md +0 -0
  96. {strictcli-0.10.0 → strictcli-0.12.0}/LICENSE +0 -0
  97. {strictcli-0.10.0 → strictcli-0.12.0}/README.md +0 -0
  98. {strictcli-0.10.0 → strictcli-0.12.0}/index.js +0 -0
  99. {strictcli-0.10.0 → strictcli-0.12.0}/postinstall.js +0 -0
  100. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_arg_default.py +0 -0
  101. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_at_prefix.py +0 -0
  102. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_auto_version.py +0 -0
  103. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_check_types.py +0 -0
  104. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_choices.py +0 -0
  105. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_command_help_suggestion.py +0 -0
  106. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_config.py +0 -0
  107. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_config_file_path.py +0 -0
  108. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_deep_nesting.py +0 -0
  109. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_dependencies.py +0 -0
  110. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_deprecated.py +0 -0
  111. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_dump_schema.py +0 -0
  112. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_e2e.py +0 -0
  113. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_env.py +0 -0
  114. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_exit_codes.py +0 -0
  115. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_float_type.py +0 -0
  116. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_global_flags.py +0 -0
  117. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_help.py +0 -0
  118. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_int_type.py +0 -0
  119. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_mutex.py +0 -0
  120. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_nesting.py +0 -0
  121. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_parser.py +0 -0
  122. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_passthrough.py +0 -0
  123. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_registration.py +0 -0
  124. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_repeatable.py +0 -0
  125. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_tagdsl.py +0 -0
  126. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_tags.py +0 -0
  127. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_validate.py +0 -0
  128. {strictcli-0.10.0 → strictcli-0.12.0}/tests/test_variadic.py +0 -0
  129. {strictcli-0.10.0 → strictcli-0.12.0}/todo/.defer/deferred.md +0 -0
  130. {strictcli-0.10.0 → strictcli-0.12.0}/todo/.done/original-idea.md +0 -0
@@ -5,6 +5,7 @@ on:
5
5
  branches: [main]
6
6
  pull_request:
7
7
  branches: [main]
8
+ workflow_dispatch:
8
9
 
9
10
  jobs:
10
11
  test:
@@ -3,6 +3,8 @@ name: Publish
3
3
  on:
4
4
  release:
5
5
  types: [published]
6
+ workflow_dispatch:
7
+
6
8
 
7
9
  permissions:
8
10
  contents: read
@@ -5,6 +5,7 @@ on:
5
5
  branches: [main]
6
6
  pull_request:
7
7
  branches: [main]
8
+ workflow_dispatch:
8
9
 
9
10
  jobs:
10
11
  test:
@@ -3,6 +3,8 @@ name: Publish
3
3
  on:
4
4
  release:
5
5
  types: [published]
6
+ workflow_dispatch:
7
+
6
8
 
7
9
  permissions:
8
10
  contents: read
@@ -24,7 +26,7 @@ jobs:
24
26
  - uses: actions/setup-node@v6
25
27
  with:
26
28
  node-version: 24
27
- registry-url: {{registryUrl}}
29
+ registry-url: https://registry.npmjs.org
28
30
  - run: npm publish --provenance --access public
29
31
  env:
30
32
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -0,0 +1 @@
1
+ c161283293520cc94f39d9cfec589f6f4cf5a352
@@ -0,0 +1,7 @@
1
+ {"commits":["feae73fae6f1e08f1355bc7c1b9beae842397517"],"user_facing":false}
2
+ {"commits":["087bd6cb358bf6ebf99e2635e93a2fdc59ac5517"],"user_facing":false}
3
+ {"commits":["f7e07e1d5ce69895df1a95da9c51ab686300e0b3"],"user_facing":false}
4
+ {"commits":["f96d157204a50c577f78b99667be81472e23bfe7"],"user_facing":false}
5
+ {"commits":["c7ee8c1401de38d9e086c38284388497c54cca0a"],"user_facing":true,"description":"**New feature.** `checks_path` parameter on `App` for explicit checks.toml location, replacing CWD-based discovery.","type":"feature"}
6
+ {"commits":["dd7a103769db5bccfaabb2636fce0ef5082e2159","df35fbde862721ef8b01a483abf6b33e5387afa8"],"user_facing":false}
7
+ {"commits":["f94f50bad6401756ca75ee82b31fbfc7e85edc83"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.11.0
2
+
3
+ ### Features
4
+
5
+ - **New feature.** `checks_path` parameter on `App` for explicit checks.toml location, replacing CWD-based discovery.
@@ -0,0 +1,2 @@
1
+ {"commits":["495cf93ada5d5870e1fa83594b7c856f2f493f19","d604e97d59fa0e252bfd179a2e4d0f0f62757677"],"user_facing":true,"description":"**Breaking.** CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via `checks_path=`. checks.toml now requires a top-level `app` field matching the app name.","type":"breaking"}
2
+ {"commits":["fb4899a3e17880d31ac56b42aca24d359528c8af"],"user_facing":false}
@@ -0,0 +1,5 @@
1
+ ## 0.12.0
2
+
3
+ ### Breaking
4
+
5
+ - **Breaking.** CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via `checks_path=`. checks.toml now requires a top-level `app` field matching the app name.
@@ -3,5 +3,6 @@
3
3
  "pypi",
4
4
  "npm"
5
5
  ],
6
- "private": false
6
+ "private": false,
7
+ "push_timeout": 300
7
8
  }
@@ -1,6 +1,6 @@
1
1
  {
2
- ".github/workflows/ci.yml": "d1da13d13914be16b2ecafe67afc62d611dc8dcc4ebc9db2befd84c2d6296ab2",
3
- ".github/workflows/publish.yml": "5f4ac1bf0bdd4c70041e3d2502efaae28d29eca7575e8ff12fdaad0d3af5b17a",
2
+ ".github/workflows/ci.yml": "b4e075171b131d25f9cce55a3e3a30732e95a2a794af00f354ee3200626d6a04",
3
+ ".github/workflows/publish.yml": "8befe0164321eb68e9465c634aa15d6ccb30d46fa669f7d30bf3ed064ad258ba",
4
4
  "CHANGELOG.md": "7a6ec46141007c18090a0ff325e2dd68cd51588c4e0b5af7b580365e5eaaca85",
5
5
  ".gitignore": "c89d76c6c43e7f0897b3472a15b7c0103e0be5b3a04c2b44e585fa48bdcda2bb",
6
6
  "LICENSE": "e958892abc1dd5d991fca6d528e695d335f25773d373d2e0be5f4a9507d8f8d5",
@@ -0,0 +1,10 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ # This hook runs BEFORE built-in pre-release checks (tests, lint).
4
+ # Use it for setup tasks: starting services, setting env vars, etc.
5
+ # Built-in checks run after this hook. Custom validation goes in pre-release.sh.
6
+
7
+ # Ensure strictcli and conformance tool are installed (editable)
8
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
9
+ pip install -e "$REPO_ROOT/python" --break-system-packages --quiet 2>/dev/null || true
10
+ pip install -e "$REPO_ROOT/conformance" --break-system-packages --quiet 2>/dev/null || true
@@ -6,3 +6,8 @@ set -euo pipefail
6
6
  # - Check for uncommitted documentation
7
7
  # - Verify external service connectivity
8
8
  # - Run integration tests not covered by the test suite
9
+
10
+ # Run conformance checks before release
11
+ REPO_ROOT="$(git rev-parse --show-toplevel)"
12
+ cd "$REPO_ROOT/conformance"
13
+ conformance check --tag pre-release
@@ -0,0 +1,4 @@
1
+ # Version bump type: patch, minor, or major
2
+ bump = "minor"
3
+ include = ["pypi", "npm"]
4
+ exclude = []
@@ -0,0 +1,3 @@
1
+ bump = "minor"
2
+ include = ["pypi", "npm"]
3
+ exclude = []
@@ -0,0 +1 @@
1
+ 0.45.0
@@ -2,6 +2,18 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 0.12.0
6
+
7
+ ### Breaking
8
+
9
+ - **Breaking.** CWD auto-discovery of checks.toml removed; checks must be explicitly enabled via `checks_path=`. checks.toml now requires a top-level `app` field matching the app name.
10
+
11
+ ## 0.11.0
12
+
13
+ ### Features
14
+
15
+ - **New feature.** `checks_path` parameter on `App` for explicit checks.toml location, replacing CWD-based discovery.
16
+
5
17
  ## 0.10.0
6
18
 
7
19
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.10.0
3
+ Version: 0.12.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.10.0",
3
+ "version": "0.12.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "strictcli",
9
- "version": "0.10.0",
9
+ "version": "0.12.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.10.0",
3
+ "version": "0.12.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.10.0"
7
+ version = "0.12.0"
8
8
  description = "A strict, zero-dependency CLI framework for Python"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -2,7 +2,7 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __version__ = "0.10.0"
5
+ __version__ = "0.12.0"
6
6
 
7
7
  __all__ = [
8
8
  "App", "Flag", "Arg", "Tag", "MutexGroup", "CoRequired", "Requires",
@@ -130,8 +130,8 @@ _CHECK_REQUIRED_FIELDS = {"tags", "severity", "fast", "pure", "needs_network", "
130
130
  _CHECK_VALID_SEVERITIES = {"error", "warn"}
131
131
 
132
132
 
133
- def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
134
- """Parse and validate a checks.toml file, returning check definitions.
133
+ def _load_checks_toml(path: str | Path) -> tuple[str, dict[str, _CheckDef]]:
134
+ """Parse and validate a checks.toml file, returning (app_name, check_defs).
135
135
 
136
136
  Raises ValueError on any schema violation, file error, or invalid TOML.
137
137
  """
@@ -145,13 +145,20 @@ def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
145
145
  except (tomllib.TOMLDecodeError, UnicodeDecodeError) as exc:
146
146
  raise ValueError(f"checks.toml: {exc}") from exc
147
147
 
148
- # Only [checks] is allowed at the top level
148
+ # Only "app" and [checks] are allowed at the top level
149
149
  for key in data:
150
- if key != "checks":
150
+ if key not in ("app", "checks"):
151
151
  raise ValueError(f'checks.toml: unknown top-level key "{key}"')
152
152
 
153
+ # Validate required "app" field
154
+ if "app" not in data:
155
+ raise ValueError('checks.toml: missing required top-level key "app"')
156
+ if not isinstance(data["app"], str) or not data["app"]:
157
+ raise ValueError('checks.toml: "app" must be a non-empty string')
158
+ app_name = data["app"]
159
+
153
160
  if "checks" not in data:
154
- return {}
161
+ return (app_name, {})
155
162
 
156
163
  checks_section = data["checks"]
157
164
  if not isinstance(checks_section, dict):
@@ -243,7 +250,7 @@ def _load_checks_toml(path: str | Path) -> dict[str, _CheckDef]:
243
250
  f'unknown check "{dep}"'
244
251
  )
245
252
 
246
- return result
253
+ return (app_name, result)
247
254
 
248
255
 
249
256
  class _HelpRequested(Exception):
@@ -667,6 +674,7 @@ class App:
667
674
  config: bool = False
668
675
  config_path: str | None = None
669
676
  config_format: str = "json"
677
+ checks_path: str | Path | None = None
670
678
  flags: list[Flag] = field(default_factory=list)
671
679
  _commands: dict[str, Command] = field(default_factory=dict)
672
680
  _groups: dict[str, Group] = field(default_factory=dict)
@@ -704,13 +712,19 @@ class App:
704
712
  self._register_config_group()
705
713
  # Discover checks TOML
706
714
  self._check_context_factory: Callable | None = None
707
- checks_toml_path = Path.cwd() / ".strictcli" / "checks.toml"
708
- if checks_toml_path.is_file():
709
- self._check_defs: dict[str, _CheckDef] = _load_checks_toml(checks_toml_path)
715
+ if self.checks_path is not None:
716
+ checks_toml_path = Path(self.checks_path).resolve()
717
+ if not checks_toml_path.is_file():
718
+ raise ValueError(f"checks_path does not exist: {self.checks_path}")
719
+ app_name, self._check_defs = _load_checks_toml(checks_toml_path)
720
+ if app_name != self.name:
721
+ raise ValueError(
722
+ f'checks.toml: app "{app_name}" does not match app name "{self.name}"'
723
+ )
710
724
  self._checks_enabled = True
711
725
  self._register_check_command()
712
726
  else:
713
- self._check_defs = {}
727
+ self._check_defs: dict[str, _CheckDef] = {}
714
728
  self._checks_enabled = False
715
729
 
716
730
  @property
@@ -724,12 +738,12 @@ class App:
724
738
  if not self._checks_enabled:
725
739
  raise ValueError(
726
740
  f'cannot register check "{name}": '
727
- f"no .strictcli/checks.toml found"
741
+ f"checks not enabled"
728
742
  )
729
743
  if name not in self._check_defs:
730
744
  raise ValueError(
731
745
  f'cannot register check "{name}": '
732
- f"not declared in .strictcli/checks.toml"
746
+ f"not declared in checks.toml"
733
747
  )
734
748
  if self._check_defs[name].impl is not None:
735
749
  raise ValueError(f'check "{name}": duplicate registration')
@@ -750,7 +764,7 @@ class App:
750
764
  )
751
765
  if missing:
752
766
  return (
753
- "checks declared in .strictcli/checks.toml but not registered: "
767
+ "checks declared in checks.toml but not registered: "
754
768
  + ", ".join(missing)
755
769
  )
756
770
  return None
@@ -11,6 +11,8 @@ from strictcli import CheckResult
11
11
 
12
12
 
13
13
  TWO_CHECKS_TOML = """\
14
+ app = "testapp"
15
+
14
16
  [checks.version-check]
15
17
  tags = ["release"]
16
18
  severity = "error"
@@ -29,6 +31,8 @@ depends_on = []
29
31
  """
30
32
 
31
33
  THREE_CHECKS_WITH_DEP_TOML = """\
34
+ app = "testapp"
35
+
32
36
  [checks.base-check]
33
37
  tags = ["infra"]
34
38
  severity = "error"
@@ -66,11 +70,13 @@ def _setup_checks_app(tmp_path, monkeypatch, toml_content, register_impls=True,
66
70
 
67
71
  pass_results: dict mapping check name to CheckResult. If None, all return pass.
68
72
  """
69
- (tmp_path / ".strictcli").mkdir(exist_ok=True)
70
- (tmp_path / ".strictcli" / "checks.toml").write_text(toml_content)
71
- monkeypatch.chdir(tmp_path)
73
+ toml_file = tmp_path / "checks.toml"
74
+ toml_file.write_text(toml_content)
72
75
 
73
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
76
+ app = strictcli.App(
77
+ name="testapp", version="1.0.0", help="test app",
78
+ checks_path=str(toml_file),
79
+ )
74
80
 
75
81
  if register_impls:
76
82
  if pass_results is None:
@@ -317,13 +323,15 @@ class TestCheckIgnoreWarnings:
317
323
 
318
324
 
319
325
  class TestCheckNoContextFactory:
320
- def test_no_context_factory_error(self, tmp_path, monkeypatch):
326
+ def test_no_context_factory_error(self, tmp_path):
321
327
  """Running checks without set_check_context produces error."""
322
- (tmp_path / ".strictcli").mkdir(exist_ok=True)
323
- (tmp_path / ".strictcli" / "checks.toml").write_text(TWO_CHECKS_TOML)
324
- monkeypatch.chdir(tmp_path)
328
+ toml_file = tmp_path / "checks.toml"
329
+ toml_file.write_text(TWO_CHECKS_TOML)
325
330
 
326
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
331
+ app = strictcli.App(
332
+ name="testapp", version="1.0.0", help="test app",
333
+ checks_path=str(toml_file),
334
+ )
327
335
  for name in app._check_defs:
328
336
  app._check_defs[name].impl = (
329
337
  lambda ctx, n=name: CheckResult(status="pass", message=f"{n} OK")
@@ -0,0 +1,295 @@
1
+ """Tests for check discovery and double-entry registration (Phase 3)."""
2
+
3
+ import pytest
4
+
5
+ import strictcli
6
+
7
+
8
+ VALID_TOML = """\
9
+ app = "testapp"
10
+
11
+ [checks.lint-code]
12
+ tags = ["code", "fast"]
13
+ severity = "error"
14
+ fast = true
15
+ pure = true
16
+ needs_network = false
17
+ depends_on = []
18
+
19
+ [checks.check-deps]
20
+ tags = ["deps"]
21
+ severity = "warn"
22
+ fast = false
23
+ pure = false
24
+ needs_network = true
25
+ depends_on = ["lint-code"]
26
+ """
27
+
28
+ INVALID_TOML = """\
29
+ app = "testapp"
30
+
31
+ [checks.BadName]
32
+ tags = ["a"]
33
+ severity = "error"
34
+ fast = true
35
+ pure = true
36
+ needs_network = false
37
+ depends_on = []
38
+ """
39
+
40
+
41
+ class TestDiscovery:
42
+ def test_no_checks_path_ignores_cwd(self, tmp_path, monkeypatch):
43
+ """Without checks_path, CWD checks.toml is NOT discovered."""
44
+ (tmp_path / ".strictcli").mkdir()
45
+ (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
46
+ monkeypatch.chdir(tmp_path)
47
+
48
+ app = strictcli.App(name="testapp", version="1.0.0", help="test app")
49
+ assert app._checks_enabled is False
50
+ assert app._check_defs == {}
51
+
52
+ def test_no_toml_sets_checks_disabled(self, tmp_path, monkeypatch):
53
+ monkeypatch.chdir(tmp_path)
54
+
55
+ app = strictcli.App(name="testapp", version="1.0.0", help="test app")
56
+ assert app._checks_enabled is False
57
+ assert app._check_defs == {}
58
+
59
+ def test_invalid_toml_raises_valueerror(self, tmp_path):
60
+ toml_file = tmp_path / "checks.toml"
61
+ toml_file.write_text(INVALID_TOML)
62
+
63
+ with pytest.raises(ValueError, match='invalid check name "BadName"'):
64
+ strictcli.App(
65
+ name="testapp", version="1.0.0", help="test app",
66
+ checks_path=str(toml_file),
67
+ )
68
+
69
+
70
+ class TestChecksPath:
71
+ def test_explicit_path_discovers_checks(self, tmp_path, monkeypatch):
72
+ """checks_path points to a valid TOML in a non-CWD directory."""
73
+ # Put checks.toml somewhere that is NOT CWD/.strictcli/
74
+ custom_dir = tmp_path / "custom"
75
+ custom_dir.mkdir()
76
+ toml_file = custom_dir / "checks.toml"
77
+ toml_file.write_text(VALID_TOML)
78
+
79
+ # chdir to a directory with NO .strictcli/
80
+ empty_dir = tmp_path / "empty"
81
+ empty_dir.mkdir()
82
+ monkeypatch.chdir(empty_dir)
83
+
84
+ app = strictcli.App(
85
+ name="testapp", version="1.0.0", help="test app",
86
+ checks_path=str(toml_file),
87
+ )
88
+ assert app._checks_enabled is True
89
+ assert "lint-code" in app._check_defs
90
+ assert "check-deps" in app._check_defs
91
+
92
+ def test_explicit_path_as_pathlib(self, tmp_path, monkeypatch):
93
+ """checks_path accepts a Path object."""
94
+ custom_dir = tmp_path / "custom"
95
+ custom_dir.mkdir()
96
+ toml_file = custom_dir / "checks.toml"
97
+ toml_file.write_text(VALID_TOML)
98
+
99
+ empty_dir = tmp_path / "empty"
100
+ empty_dir.mkdir()
101
+ monkeypatch.chdir(empty_dir)
102
+
103
+ from pathlib import Path
104
+
105
+ app = strictcli.App(
106
+ name="testapp", version="1.0.0", help="test app",
107
+ checks_path=Path(str(toml_file)),
108
+ )
109
+ assert app._checks_enabled is True
110
+ assert "lint-code" in app._check_defs
111
+
112
+ def test_nonexistent_path_raises(self, tmp_path, monkeypatch):
113
+ """checks_path pointing to a missing file raises ValueError."""
114
+ monkeypatch.chdir(tmp_path)
115
+ bad_path = tmp_path / "nope" / "checks.toml"
116
+
117
+ with pytest.raises(ValueError, match="checks_path does not exist"):
118
+ strictcli.App(
119
+ name="testapp", version="1.0.0", help="test app",
120
+ checks_path=str(bad_path),
121
+ )
122
+
123
+ def test_none_means_disabled(self, tmp_path, monkeypatch):
124
+ """checks_path=None (default) means checks are disabled."""
125
+ (tmp_path / ".strictcli").mkdir()
126
+ (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
127
+ monkeypatch.chdir(tmp_path)
128
+
129
+ app = strictcli.App(
130
+ name="testapp", version="1.0.0", help="test app",
131
+ checks_path=None,
132
+ )
133
+ assert app._checks_enabled is False
134
+ assert app._check_defs == {}
135
+
136
+
137
+ class TestCheckDecorator:
138
+ def test_registers_implementation(self, tmp_path):
139
+ toml_file = tmp_path / "checks.toml"
140
+ toml_file.write_text(VALID_TOML)
141
+
142
+ app = strictcli.App(
143
+ name="testapp", version="1.0.0", help="test app",
144
+ checks_path=str(toml_file),
145
+ )
146
+
147
+ @app.check("lint-code")
148
+ def lint_impl(ctx):
149
+ return strictcli.CheckResult(status="pass", message="OK")
150
+
151
+ assert app._check_defs["lint-code"].impl is lint_impl
152
+
153
+ def test_undeclared_name_raises(self, tmp_path):
154
+ toml_file = tmp_path / "checks.toml"
155
+ toml_file.write_text(VALID_TOML)
156
+
157
+ app = strictcli.App(
158
+ name="testapp", version="1.0.0", help="test app",
159
+ checks_path=str(toml_file),
160
+ )
161
+
162
+ with pytest.raises(ValueError, match='cannot register check "nonexistent"'):
163
+ @app.check("nonexistent")
164
+ def bad_impl(ctx):
165
+ pass
166
+
167
+ def test_duplicate_registration_raises(self, tmp_path):
168
+ toml_file = tmp_path / "checks.toml"
169
+ toml_file.write_text(VALID_TOML)
170
+
171
+ app = strictcli.App(
172
+ name="testapp", version="1.0.0", help="test app",
173
+ checks_path=str(toml_file),
174
+ )
175
+
176
+ @app.check("lint-code")
177
+ def first(ctx):
178
+ pass
179
+
180
+ with pytest.raises(ValueError, match='check "lint-code": duplicate registration'):
181
+ @app.check("lint-code")
182
+ def second(ctx):
183
+ pass
184
+
185
+ def test_no_toml_raises(self, tmp_path, monkeypatch):
186
+ monkeypatch.chdir(tmp_path)
187
+
188
+ app = strictcli.App(name="testapp", version="1.0.0", help="test app")
189
+
190
+ with pytest.raises(ValueError, match="checks not enabled"):
191
+ @app.check("anything")
192
+ def bad(ctx):
193
+ pass
194
+
195
+ def test_register_check_not_enabled_message(self, tmp_path):
196
+ """Verify exact error message for registering check when not enabled."""
197
+ app = strictcli.App(name="testapp", version="1.0.0", help="test app")
198
+
199
+ with pytest.raises(
200
+ ValueError,
201
+ match=r'cannot register check "my-check": checks not enabled',
202
+ ):
203
+ @app.check("my-check")
204
+ def impl(ctx):
205
+ pass
206
+
207
+
208
+ class TestDoubleEntryValidation:
209
+ def test_missing_impl_error_in_test(self, tmp_path):
210
+ toml_file = tmp_path / "checks.toml"
211
+ toml_file.write_text(VALID_TOML)
212
+
213
+ app = strictcli.App(
214
+ name="testapp", version="1.0.0", help="test app",
215
+ checks_path=str(toml_file),
216
+ )
217
+
218
+ # Register a dummy command so the app has something to run
219
+ @app.command("hello", help="say hello")
220
+ def hello(**kw):
221
+ print("hello")
222
+
223
+ # Only register one of the two checks
224
+ @app.check("lint-code")
225
+ def lint_impl(ctx):
226
+ return strictcli.CheckResult(status="pass", message="OK")
227
+
228
+ # check-deps is not registered -- should fail validation
229
+ result = app.test(["hello"])
230
+ assert result.exit_code == 1
231
+ assert "checks declared in checks.toml but not registered" in result.stderr
232
+ assert "check-deps" in result.stderr
233
+
234
+ def test_all_registered_passes_validation(self, tmp_path):
235
+ toml_file = tmp_path / "checks.toml"
236
+ toml_file.write_text(VALID_TOML)
237
+
238
+ app = strictcli.App(
239
+ name="testapp", version="1.0.0", help="test app",
240
+ checks_path=str(toml_file),
241
+ )
242
+
243
+ @app.command("hello", help="say hello")
244
+ def hello(**kw):
245
+ print("hello")
246
+
247
+ @app.check("lint-code")
248
+ def lint_impl(ctx):
249
+ return strictcli.CheckResult(status="pass", message="OK")
250
+
251
+ @app.check("check-deps")
252
+ def deps_impl(ctx):
253
+ return strictcli.CheckResult(status="pass", message="OK")
254
+
255
+ result = app.test(["hello"])
256
+ assert result.exit_code == 0
257
+ assert "hello" in result.stdout
258
+
259
+ def test_no_checks_skips_validation(self, tmp_path, monkeypatch):
260
+ monkeypatch.chdir(tmp_path)
261
+
262
+ app = strictcli.App(name="testapp", version="1.0.0", help="test app")
263
+
264
+ @app.command("hello", help="say hello")
265
+ def hello(**kw):
266
+ print("hello")
267
+
268
+ result = app.test(["hello"])
269
+ assert result.exit_code == 0
270
+ assert "hello" in result.stdout
271
+
272
+ def test_app_mismatch_raises(self, tmp_path):
273
+ """checks_path with app='wrong' but App name='testapp' raises ValueError."""
274
+ toml = """\
275
+ app = "wrong"
276
+
277
+ [checks.my-check]
278
+ tags = ["a"]
279
+ severity = "error"
280
+ fast = true
281
+ pure = true
282
+ needs_network = false
283
+ depends_on = []
284
+ """
285
+ toml_file = tmp_path / "checks.toml"
286
+ toml_file.write_text(toml)
287
+
288
+ with pytest.raises(
289
+ ValueError,
290
+ match=r'checks.toml: app "wrong" does not match app name "testapp"',
291
+ ):
292
+ strictcli.App(
293
+ name="testapp", version="1.0.0", help="test app",
294
+ checks_path=str(toml_file),
295
+ )