strictcli 0.11.0__tar.gz → 0.13.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 (129) hide show
  1. strictcli-0.13.0/.rlsbl/changes/.validated +1 -0
  2. strictcli-0.13.0/.rlsbl/changes/0.12.0.jsonl +2 -0
  3. strictcli-0.13.0/.rlsbl/changes/0.12.0.md +5 -0
  4. strictcli-0.13.0/.rlsbl/changes/0.13.0.jsonl +3 -0
  5. strictcli-0.13.0/.rlsbl/releases/v0.12.0.toml +4 -0
  6. strictcli-0.13.0/.rlsbl/releases/v0.13.0.toml +4 -0
  7. {strictcli-0.11.0 → strictcli-0.13.0}/CHANGELOG.md +12 -0
  8. {strictcli-0.11.0 → strictcli-0.13.0}/PKG-INFO +1 -1
  9. {strictcli-0.11.0 → strictcli-0.13.0}/package-lock.json +2 -2
  10. {strictcli-0.11.0 → strictcli-0.13.0}/package.json +1 -1
  11. {strictcli-0.11.0 → strictcli-0.13.0}/pyproject.toml +1 -1
  12. {strictcli-0.11.0 → strictcli-0.13.0}/strictcli/__init__.py +56 -18
  13. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_command.py +17 -9
  14. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_discovery.py +93 -42
  15. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_runner.py +7 -5
  16. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_schema.py +21 -6
  17. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_dump_schema.py +66 -0
  18. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_toml_loading.py +94 -5
  19. {strictcli-0.11.0 → strictcli-0.13.0}/uv.lock +1 -1
  20. strictcli-0.11.0/.rlsbl/changes/.validated +0 -1
  21. {strictcli-0.11.0 → strictcli-0.13.0}/.claude/settings.json +0 -0
  22. {strictcli-0.11.0 → strictcli-0.13.0}/.github/workflows/ci.yml +0 -0
  23. {strictcli-0.11.0 → strictcli-0.13.0}/.github/workflows/publish.yml +0 -0
  24. {strictcli-0.11.0 → strictcli-0.13.0}/.gitignore +0 -0
  25. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.claude/settings.json +0 -0
  26. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.github/workflows/ci.yml +0 -0
  27. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.github/workflows/publish.yml +0 -0
  28. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.gitignore +0 -0
  29. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/hooks/post-release.sh +0 -0
  30. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/hooks/pre-checks.sh +0 -0
  31. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/hooks/pre-release.sh +0 -0
  32. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/lint/go.toml +0 -0
  33. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/lint/npm.toml +0 -0
  34. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/.rlsbl/lint/python.toml +0 -0
  35. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/CHANGELOG.md +0 -0
  36. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/CLAUDE.md +0 -0
  37. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/bases/LICENSE +0 -0
  38. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.10.0.jsonl +0 -0
  39. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.10.0.md +0 -0
  40. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.11.0.jsonl +0 -0
  41. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.11.0.md +0 -0
  42. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.4.0.jsonl +0 -0
  43. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.4.0.md +0 -0
  44. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.4.1.jsonl +0 -0
  45. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.4.1.md +0 -0
  46. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.5.0.jsonl +0 -0
  47. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.5.0.md +0 -0
  48. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.6.0.jsonl +0 -0
  49. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.6.0.md +0 -0
  50. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.6.1.jsonl +0 -0
  51. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.6.1.md +0 -0
  52. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.7.0.jsonl +0 -0
  53. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.7.0.md +0 -0
  54. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.7.1.jsonl +0 -0
  55. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.7.1.md +0 -0
  56. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.0.jsonl +0 -0
  57. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.0.md +0 -0
  58. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.1.jsonl +0 -0
  59. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.1.md +0 -0
  60. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.2.jsonl +0 -0
  61. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.2.md +0 -0
  62. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.3.jsonl +0 -0
  63. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.3.md +0 -0
  64. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.4.jsonl +0 -0
  65. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.4.md +0 -0
  66. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.5.jsonl +0 -0
  67. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.5.md +0 -0
  68. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.6.jsonl +0 -0
  69. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.6.md +0 -0
  70. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.7.jsonl +0 -0
  71. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.8.7.md +0 -0
  72. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.9.0.jsonl +0 -0
  73. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.9.0.md +0 -0
  74. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.9.1.jsonl +0 -0
  75. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/0.9.1.md +0 -0
  76. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/changes/unreleased.jsonl +0 -0
  77. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/config.json +0 -0
  78. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/hashes.json +0 -0
  79. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/hooks/post-release.sh +0 -0
  80. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/hooks/pre-checks.sh +0 -0
  81. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/hooks/pre-release.sh +0 -0
  82. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/lint/go.toml +0 -0
  83. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/lint/npm.toml +0 -0
  84. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/lint/python.toml +0 -0
  85. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/unreleased.toml +0 -0
  86. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.10.0.toml +0 -0
  87. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.11.0.toml +0 -0
  88. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.8.5.toml +0 -0
  89. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.8.6.toml +0 -0
  90. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.8.7.toml +0 -0
  91. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.9.0.toml +0 -0
  92. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/releases/v0.9.1.toml +0 -0
  93. {strictcli-0.11.0 → strictcli-0.13.0}/.rlsbl/version +0 -0
  94. {strictcli-0.11.0 → strictcli-0.13.0}/.strictcli/schema.json +0 -0
  95. {strictcli-0.11.0 → strictcli-0.13.0}/CLAUDE.md +0 -0
  96. {strictcli-0.11.0 → strictcli-0.13.0}/LICENSE +0 -0
  97. {strictcli-0.11.0 → strictcli-0.13.0}/README.md +0 -0
  98. {strictcli-0.11.0 → strictcli-0.13.0}/index.js +0 -0
  99. {strictcli-0.11.0 → strictcli-0.13.0}/postinstall.js +0 -0
  100. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_arg_default.py +0 -0
  101. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_at_prefix.py +0 -0
  102. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_auto_version.py +0 -0
  103. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_check_types.py +0 -0
  104. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_choices.py +0 -0
  105. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_command_help_suggestion.py +0 -0
  106. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_config.py +0 -0
  107. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_config_file_path.py +0 -0
  108. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_deep_nesting.py +0 -0
  109. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_dependencies.py +0 -0
  110. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_deprecated.py +0 -0
  111. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_e2e.py +0 -0
  112. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_env.py +0 -0
  113. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_exit_codes.py +0 -0
  114. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_float_type.py +0 -0
  115. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_global_flags.py +0 -0
  116. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_help.py +0 -0
  117. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_int_type.py +0 -0
  118. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_mutex.py +0 -0
  119. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_nesting.py +0 -0
  120. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_parser.py +0 -0
  121. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_passthrough.py +0 -0
  122. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_registration.py +0 -0
  123. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_repeatable.py +0 -0
  124. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_tagdsl.py +0 -0
  125. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_tags.py +0 -0
  126. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_validate.py +0 -0
  127. {strictcli-0.11.0 → strictcli-0.13.0}/tests/test_variadic.py +0 -0
  128. {strictcli-0.11.0 → strictcli-0.13.0}/todo/.defer/deferred.md +0 -0
  129. {strictcli-0.11.0 → strictcli-0.13.0}/todo/.done/original-idea.md +0 -0
@@ -0,0 +1 @@
1
+ 052122abb24b7318d56b1c0d657fbaf740d6f5a5
@@ -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.
@@ -0,0 +1,3 @@
1
+ {"commits":["12ad7c022938b07cf1f44b20bdbe9de0ae6a84be"],"user_facing":true,"description":"**New feature.** Schema dump (--dump-schema) now includes a project_id field read from pyproject.toml, providing provenance for schema validation.","type":"feature"}
2
+ {"commits":["a50cf7899c938974b12e8e74201bdf58c0205a55"],"user_facing":false}
3
+ {"commits":["214721745a82a47189a13b2443fb9ddc59826347"],"user_facing":false}
@@ -0,0 +1,4 @@
1
+ # Version bump type: patch, minor, or major
2
+ bump = "minor"
3
+ include = ["pypi", "npm"]
4
+ exclude = []
@@ -0,0 +1,4 @@
1
+ # Version bump type: patch, minor, or major
2
+ bump = "minor"
3
+ include = ["pypi", "npm"]
4
+ exclude = []
@@ -2,6 +2,18 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## 0.13.0
6
+
7
+ ### Features
8
+
9
+ - **New feature.** Schema dump (--dump-schema) now includes a project_id field read from pyproject.toml, providing provenance for schema validation.
10
+
11
+ ## 0.12.0
12
+
13
+ ### Breaking
14
+
15
+ - **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.
16
+
5
17
  ## 0.11.0
6
18
 
7
19
  ### Features
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: strictcli
3
- Version: 0.11.0
3
+ Version: 0.13.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.11.0",
3
+ "version": "0.13.0",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "strictcli",
9
- "version": "0.11.0",
9
+ "version": "0.13.0",
10
10
  "hasInstallScript": true,
11
11
  "license": "MIT"
12
12
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strictcli",
3
- "version": "0.11.0",
3
+ "version": "0.13.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.11.0"
7
+ version = "0.13.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.11.0"
5
+ __version__ = "0.13.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):
@@ -709,14 +716,15 @@ class App:
709
716
  checks_toml_path = Path(self.checks_path).resolve()
710
717
  if not checks_toml_path.is_file():
711
718
  raise ValueError(f"checks_path does not exist: {self.checks_path}")
712
- else:
713
- checks_toml_path = Path.cwd() / ".strictcli" / "checks.toml"
714
- if checks_toml_path.is_file():
715
- self._check_defs: dict[str, _CheckDef] = _load_checks_toml(checks_toml_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
+ )
716
724
  self._checks_enabled = True
717
725
  self._register_check_command()
718
726
  else:
719
- self._check_defs = {}
727
+ self._check_defs: dict[str, _CheckDef] = {}
720
728
  self._checks_enabled = False
721
729
 
722
730
  @property
@@ -730,12 +738,12 @@ class App:
730
738
  if not self._checks_enabled:
731
739
  raise ValueError(
732
740
  f'cannot register check "{name}": '
733
- f"no .strictcli/checks.toml found"
741
+ f"checks not enabled"
734
742
  )
735
743
  if name not in self._check_defs:
736
744
  raise ValueError(
737
745
  f'cannot register check "{name}": '
738
- f"not declared in .strictcli/checks.toml"
746
+ f"not declared in checks.toml"
739
747
  )
740
748
  if self._check_defs[name].impl is not None:
741
749
  raise ValueError(f'check "{name}": duplicate registration')
@@ -756,7 +764,7 @@ class App:
756
764
  )
757
765
  if missing:
758
766
  return (
759
- "checks declared in .strictcli/checks.toml but not registered: "
767
+ "checks declared in checks.toml but not registered: "
760
768
  + ", ".join(missing)
761
769
  )
762
770
  return None
@@ -1452,7 +1460,11 @@ class App:
1452
1460
  print(_format_version(self))
1453
1461
  sys.exit(0)
1454
1462
  except _DumpSchemaRequested:
1455
- path = _write_schema(self)
1463
+ try:
1464
+ path = _write_schema(self)
1465
+ except RuntimeError as e:
1466
+ print(f"error: {e}", file=sys.stderr)
1467
+ sys.exit(1)
1456
1468
  print(path)
1457
1469
  sys.exit(0)
1458
1470
  except _ParseError as e:
@@ -1495,8 +1507,13 @@ class App:
1495
1507
  except _VersionRequested:
1496
1508
  stdout_buf.write(_format_version(self) + "\n")
1497
1509
  except _DumpSchemaRequested:
1498
- path = _write_schema(self)
1499
- stdout_buf.write(path + "\n")
1510
+ try:
1511
+ path = _write_schema(self)
1512
+ except RuntimeError as e:
1513
+ stderr_buf.write(f"error: {e}\n")
1514
+ exit_code = 1
1515
+ else:
1516
+ stdout_buf.write(path + "\n")
1500
1517
  except _ParseError as e:
1501
1518
  stderr_buf.write(f"error: {e}\n")
1502
1519
  prefix = e.command_prefix or self.name
@@ -3079,14 +3096,35 @@ def _build_schema_defaults() -> dict:
3079
3096
  }
3080
3097
 
3081
3098
 
3099
+ def _read_project_id() -> str:
3100
+ """Read project name from pyproject.toml in the current working directory."""
3101
+ pyproject_path = Path(os.getcwd()) / "pyproject.toml"
3102
+ if not pyproject_path.exists():
3103
+ raise RuntimeError(
3104
+ "Cannot determine project_id: pyproject.toml not found "
3105
+ "or missing [project].name"
3106
+ )
3107
+ with open(pyproject_path, "rb") as f:
3108
+ data = tomllib.load(f)
3109
+ project_name = data.get("project", {}).get("name")
3110
+ if not project_name:
3111
+ raise RuntimeError(
3112
+ "Cannot determine project_id: pyproject.toml not found "
3113
+ "or missing [project].name"
3114
+ )
3115
+ return project_name
3116
+
3117
+
3082
3118
  def _dump_schema(app: App) -> dict:
3083
3119
  """Produce a JSON-serializable dict representing the app's command tree.
3084
3120
 
3085
3121
  Fields whose values match the schema defaults are omitted.
3086
3122
  The top-level ``defaults`` key documents what each missing field means.
3087
3123
  """
3124
+ project_id = _read_project_id()
3088
3125
  schema: dict = {
3089
3126
  "defaults": _build_schema_defaults(),
3127
+ "project_id": project_id,
3090
3128
  "name": app.name,
3091
3129
  "version": app.version,
3092
3130
  "help": app.help,
@@ -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")
@@ -1,13 +1,13 @@
1
1
  """Tests for check discovery and double-entry registration (Phase 3)."""
2
2
 
3
- import os
4
-
5
3
  import pytest
6
4
 
7
5
  import strictcli
8
6
 
9
7
 
10
8
  VALID_TOML = """\
9
+ app = "testapp"
10
+
11
11
  [checks.lint-code]
12
12
  tags = ["code", "fast"]
13
13
  severity = "error"
@@ -26,6 +26,8 @@ depends_on = ["lint-code"]
26
26
  """
27
27
 
28
28
  INVALID_TOML = """\
29
+ app = "testapp"
30
+
29
31
  [checks.BadName]
30
32
  tags = ["a"]
31
33
  severity = "error"
@@ -37,15 +39,15 @@ depends_on = []
37
39
 
38
40
 
39
41
  class TestDiscovery:
40
- def test_discovers_checks_toml_in_cwd(self, tmp_path, monkeypatch):
42
+ def test_no_checks_path_ignores_cwd(self, tmp_path, monkeypatch):
43
+ """Without checks_path, CWD checks.toml is NOT discovered."""
41
44
  (tmp_path / ".strictcli").mkdir()
42
45
  (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
43
46
  monkeypatch.chdir(tmp_path)
44
47
 
45
48
  app = strictcli.App(name="testapp", version="1.0.0", help="test app")
46
- assert app._checks_enabled is True
47
- assert "lint-code" in app._check_defs
48
- assert "check-deps" in app._check_defs
49
+ assert app._checks_enabled is False
50
+ assert app._check_defs == {}
49
51
 
50
52
  def test_no_toml_sets_checks_disabled(self, tmp_path, monkeypatch):
51
53
  monkeypatch.chdir(tmp_path)
@@ -54,13 +56,15 @@ class TestDiscovery:
54
56
  assert app._checks_enabled is False
55
57
  assert app._check_defs == {}
56
58
 
57
- def test_invalid_toml_raises_valueerror(self, tmp_path, monkeypatch):
58
- (tmp_path / ".strictcli").mkdir()
59
- (tmp_path / ".strictcli" / "checks.toml").write_text(INVALID_TOML)
60
- monkeypatch.chdir(tmp_path)
59
+ def test_invalid_toml_raises_valueerror(self, tmp_path):
60
+ toml_file = tmp_path / "checks.toml"
61
+ toml_file.write_text(INVALID_TOML)
61
62
 
62
63
  with pytest.raises(ValueError, match='invalid check name "BadName"'):
63
- strictcli.App(name="testapp", version="1.0.0", help="test app")
64
+ strictcli.App(
65
+ name="testapp", version="1.0.0", help="test app",
66
+ checks_path=str(toml_file),
67
+ )
64
68
 
65
69
 
66
70
  class TestChecksPath:
@@ -116,8 +120,8 @@ class TestChecksPath:
116
120
  checks_path=str(bad_path),
117
121
  )
118
122
 
119
- def test_none_uses_cwd_discovery(self, tmp_path, monkeypatch):
120
- """checks_path=None (default) falls back to CWD-based discovery."""
123
+ def test_none_means_disabled(self, tmp_path, monkeypatch):
124
+ """checks_path=None (default) means checks are disabled."""
121
125
  (tmp_path / ".strictcli").mkdir()
122
126
  (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
123
127
  monkeypatch.chdir(tmp_path)
@@ -126,17 +130,19 @@ class TestChecksPath:
126
130
  name="testapp", version="1.0.0", help="test app",
127
131
  checks_path=None,
128
132
  )
129
- assert app._checks_enabled is True
130
- assert "lint-code" in app._check_defs
133
+ assert app._checks_enabled is False
134
+ assert app._check_defs == {}
131
135
 
132
136
 
133
137
  class TestCheckDecorator:
134
- def test_registers_implementation(self, tmp_path, monkeypatch):
135
- (tmp_path / ".strictcli").mkdir()
136
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
137
- monkeypatch.chdir(tmp_path)
138
+ def test_registers_implementation(self, tmp_path):
139
+ toml_file = tmp_path / "checks.toml"
140
+ toml_file.write_text(VALID_TOML)
138
141
 
139
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
142
+ app = strictcli.App(
143
+ name="testapp", version="1.0.0", help="test app",
144
+ checks_path=str(toml_file),
145
+ )
140
146
 
141
147
  @app.check("lint-code")
142
148
  def lint_impl(ctx):
@@ -144,24 +150,28 @@ class TestCheckDecorator:
144
150
 
145
151
  assert app._check_defs["lint-code"].impl is lint_impl
146
152
 
147
- def test_undeclared_name_raises(self, tmp_path, monkeypatch):
148
- (tmp_path / ".strictcli").mkdir()
149
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
150
- monkeypatch.chdir(tmp_path)
153
+ def test_undeclared_name_raises(self, tmp_path):
154
+ toml_file = tmp_path / "checks.toml"
155
+ toml_file.write_text(VALID_TOML)
151
156
 
152
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
157
+ app = strictcli.App(
158
+ name="testapp", version="1.0.0", help="test app",
159
+ checks_path=str(toml_file),
160
+ )
153
161
 
154
162
  with pytest.raises(ValueError, match='cannot register check "nonexistent"'):
155
163
  @app.check("nonexistent")
156
164
  def bad_impl(ctx):
157
165
  pass
158
166
 
159
- def test_duplicate_registration_raises(self, tmp_path, monkeypatch):
160
- (tmp_path / ".strictcli").mkdir()
161
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
162
- monkeypatch.chdir(tmp_path)
167
+ def test_duplicate_registration_raises(self, tmp_path):
168
+ toml_file = tmp_path / "checks.toml"
169
+ toml_file.write_text(VALID_TOML)
163
170
 
164
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
171
+ app = strictcli.App(
172
+ name="testapp", version="1.0.0", help="test app",
173
+ checks_path=str(toml_file),
174
+ )
165
175
 
166
176
  @app.check("lint-code")
167
177
  def first(ctx):
@@ -177,19 +187,33 @@ class TestCheckDecorator:
177
187
 
178
188
  app = strictcli.App(name="testapp", version="1.0.0", help="test app")
179
189
 
180
- with pytest.raises(ValueError, match="no .strictcli/checks.toml found"):
190
+ with pytest.raises(ValueError, match="checks not enabled"):
181
191
  @app.check("anything")
182
192
  def bad(ctx):
183
193
  pass
184
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
+
185
207
 
186
208
  class TestDoubleEntryValidation:
187
- def test_missing_impl_error_in_test(self, tmp_path, monkeypatch):
188
- (tmp_path / ".strictcli").mkdir()
189
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
190
- monkeypatch.chdir(tmp_path)
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)
191
212
 
192
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
213
+ app = strictcli.App(
214
+ name="testapp", version="1.0.0", help="test app",
215
+ checks_path=str(toml_file),
216
+ )
193
217
 
194
218
  # Register a dummy command so the app has something to run
195
219
  @app.command("hello", help="say hello")
@@ -204,15 +228,17 @@ class TestDoubleEntryValidation:
204
228
  # check-deps is not registered -- should fail validation
205
229
  result = app.test(["hello"])
206
230
  assert result.exit_code == 1
207
- assert "checks declared in .strictcli/checks.toml but not registered" in result.stderr
231
+ assert "checks declared in checks.toml but not registered" in result.stderr
208
232
  assert "check-deps" in result.stderr
209
233
 
210
- def test_all_registered_passes_validation(self, tmp_path, monkeypatch):
211
- (tmp_path / ".strictcli").mkdir()
212
- (tmp_path / ".strictcli" / "checks.toml").write_text(VALID_TOML)
213
- monkeypatch.chdir(tmp_path)
234
+ def test_all_registered_passes_validation(self, tmp_path):
235
+ toml_file = tmp_path / "checks.toml"
236
+ toml_file.write_text(VALID_TOML)
214
237
 
215
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
238
+ app = strictcli.App(
239
+ name="testapp", version="1.0.0", help="test app",
240
+ checks_path=str(toml_file),
241
+ )
216
242
 
217
243
  @app.command("hello", help="say hello")
218
244
  def hello(**kw):
@@ -242,3 +268,28 @@ class TestDoubleEntryValidation:
242
268
  result = app.test(["hello"])
243
269
  assert result.exit_code == 0
244
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
+ )
@@ -113,7 +113,7 @@ class TestRunChecks:
113
113
  def _make_app_with_checks(self, check_defs, tmp_path, monkeypatch):
114
114
  """Build an App with pre-populated check definitions."""
115
115
  # Write a minimal TOML so the app discovers checks
116
- toml_lines = []
116
+ toml_lines = ['app = "testapp"', ""]
117
117
  for name, cdef in check_defs.items():
118
118
  tags_str = ", ".join(f'"{t}"' for t in cdef.tags)
119
119
  deps_str = ", ".join(f'"{d}"' for d in cdef.depends_on)
@@ -126,11 +126,13 @@ class TestRunChecks:
126
126
  toml_lines.append(f"depends_on = [{deps_str}]")
127
127
  toml_lines.append("")
128
128
 
129
- (tmp_path / ".strictcli").mkdir(exist_ok=True)
130
- (tmp_path / ".strictcli" / "checks.toml").write_text("\n".join(toml_lines))
131
- monkeypatch.chdir(tmp_path)
129
+ toml_file = tmp_path / "checks.toml"
130
+ toml_file.write_text("\n".join(toml_lines))
132
131
 
133
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
132
+ app = strictcli.App(
133
+ name="testapp", version="1.0.0", help="test app",
134
+ checks_path=str(toml_file),
135
+ )
134
136
  # Inject the impl functions from our check_defs
135
137
  for name, cdef in check_defs.items():
136
138
  if cdef.impl is not None:
@@ -2,10 +2,19 @@
2
2
 
3
3
  import json
4
4
 
5
+ import pytest
5
6
  import strictcli
6
7
 
7
8
 
9
+ @pytest.fixture(autouse=True)
10
+ def _pyproject_in_tmp(tmp_path):
11
+ """Ensure every test that uses tmp_path has a pyproject.toml for project_id."""
12
+ (tmp_path / "pyproject.toml").write_text('[project]\nname = "testproject"\n')
13
+
14
+
8
15
  CHECKS_TOML = """\
16
+ app = "testapp"
17
+
9
18
  [checks.lint-code]
10
19
  tags = ["code", "fast"]
11
20
  severity = "error"
@@ -28,11 +37,14 @@ class TestSchemaWithChecks:
28
37
  """--dump-schema includes checks key when checks are enabled."""
29
38
 
30
39
  def test_checks_key_present(self, tmp_path, monkeypatch):
31
- (tmp_path / ".strictcli").mkdir()
32
- (tmp_path / ".strictcli" / "checks.toml").write_text(CHECKS_TOML)
40
+ toml_file = tmp_path / "checks.toml"
41
+ toml_file.write_text(CHECKS_TOML)
33
42
  monkeypatch.chdir(tmp_path)
34
43
 
35
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
44
+ app = strictcli.App(
45
+ name="testapp", version="1.0.0", help="test app",
46
+ checks_path=str(toml_file),
47
+ )
36
48
 
37
49
  @app.check("lint-code")
38
50
  def lint_impl(ctx):
@@ -54,11 +66,14 @@ class TestSchemaWithChecks:
54
66
  assert len(data["checks"]) == 2
55
67
 
56
68
  def test_check_metadata_correct(self, tmp_path, monkeypatch):
57
- (tmp_path / ".strictcli").mkdir()
58
- (tmp_path / ".strictcli" / "checks.toml").write_text(CHECKS_TOML)
69
+ toml_file = tmp_path / "checks.toml"
70
+ toml_file.write_text(CHECKS_TOML)
59
71
  monkeypatch.chdir(tmp_path)
60
72
 
61
- app = strictcli.App(name="testapp", version="1.0.0", help="test app")
73
+ app = strictcli.App(
74
+ name="testapp", version="1.0.0", help="test app",
75
+ checks_path=str(toml_file),
76
+ )
62
77
 
63
78
  @app.check("lint-code")
64
79
  def lint_impl(ctx):