py-wlcli 0.8.0__py3-none-any.whl

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 (320) hide show
  1. py_wlcli-0.8.0.dist-info/METADATA +139 -0
  2. py_wlcli-0.8.0.dist-info/RECORD +320 -0
  3. py_wlcli-0.8.0.dist-info/WHEEL +4 -0
  4. py_wlcli-0.8.0.dist-info/entry_points.txt +2 -0
  5. py_wlcli-0.8.0.dist-info/licenses/LICENSE +21 -0
  6. wl_cli/__init__.py +27 -0
  7. wl_cli/__main__.py +130 -0
  8. wl_cli/capabilities.py +79 -0
  9. wl_cli/cc/__init__.py +5 -0
  10. wl_cli/cc/checks.py +1019 -0
  11. wl_cli/cc/cli.py +370 -0
  12. wl_cli/cc/mutate.py +1350 -0
  13. wl_cli/cli_click.py +156 -0
  14. wl_cli/commands/__init__.py +84 -0
  15. wl_cli/commands/base.py +80 -0
  16. wl_cli/commands/buildcommands/__init__.py +1 -0
  17. wl_cli/commands/buildcommands/build.py +71 -0
  18. wl_cli/commands/buildcommands/build_utils.py +47 -0
  19. wl_cli/commands/buildcommands/builders/__init__.py +18 -0
  20. wl_cli/commands/buildcommands/builders/cleanup.py +35 -0
  21. wl_cli/commands/buildcommands/builders/coordinator.py +122 -0
  22. wl_cli/commands/buildcommands/builders/environment.py +81 -0
  23. wl_cli/commands/buildcommands/builders/rust_builder.py +39 -0
  24. wl_cli/commands/buildcommands/builders/stubs.py +58 -0
  25. wl_cli/commands/buildcommands/dist.py +326 -0
  26. wl_cli/commands/buildcommands/test.py +89 -0
  27. wl_cli/commands/clean/__init__.py +1 -0
  28. wl_cli/commands/clean/build.py +151 -0
  29. wl_cli/commands/clean/clean.py +294 -0
  30. wl_cli/commands/clean/clean_utils.py +77 -0
  31. wl_cli/commands/clean/core.py +236 -0
  32. wl_cli/commands/clean/dead_code/__init__.py +0 -0
  33. wl_cli/commands/clean/dead_code/dead_code_command.py +70 -0
  34. wl_cli/commands/clean/dead_code/detector.py +161 -0
  35. wl_cli/commands/clean/dead_code/fixer.py +176 -0
  36. wl_cli/commands/clean/dead_code/reporter.py +295 -0
  37. wl_cli/commands/clean/dead_code/utils.py +0 -0
  38. wl_cli/commands/clean/lfs.py +215 -0
  39. wl_cli/commands/clean/python.py +81 -0
  40. wl_cli/commands/clean/rust.py +91 -0
  41. wl_cli/commands/clean/utils/__init__.py +34 -0
  42. wl_cli/commands/clean/venv.py +91 -0
  43. wl_cli/commands/cleanup.py +73 -0
  44. wl_cli/commands/click_base.py +79 -0
  45. wl_cli/commands/config/__init__.py +1 -0
  46. wl_cli/commands/config/config.py +291 -0
  47. wl_cli/commands/debug/__init__.py +6 -0
  48. wl_cli/commands/debug/debug.py +309 -0
  49. wl_cli/commands/debug/debug_cache.py +86 -0
  50. wl_cli/commands/debug/debug_subagent.py +156 -0
  51. wl_cli/commands/debug/output_formatter.py +165 -0
  52. wl_cli/commands/debug/source_hasher.py +92 -0
  53. wl_cli/commands/format/__init__.py +13 -0
  54. wl_cli/commands/format/format_cache_adapter.py +125 -0
  55. wl_cli/commands/format/format_command.py +197 -0
  56. wl_cli/commands/format/format_coordinator.py +191 -0
  57. wl_cli/commands/format/format_executor.py +470 -0
  58. wl_cli/commands/format/format_outcome.py +44 -0
  59. wl_cli/commands/format/format_path_handler.py +1016 -0
  60. wl_cli/commands/format/format_progress.py +62 -0
  61. wl_cli/commands/format/format_reporter.py +31 -0
  62. wl_cli/commands/format/python_formatter.py +506 -0
  63. wl_cli/commands/format/rust_formatter.py +106 -0
  64. wl_cli/commands/gate/__init__.py +1 -0
  65. wl_cli/commands/gate/gate_command.py +862 -0
  66. wl_cli/commands/gate/preflight.py +445 -0
  67. wl_cli/commands/git/__init__.py +1 -0
  68. wl_cli/commands/git/config.py +169 -0
  69. wl_cli/commands/git/git_facts.py +60 -0
  70. wl_cli/commands/git/git_flow.py +86 -0
  71. wl_cli/commands/git/hook.py +112 -0
  72. wl_cli/commands/git/policy.py +216 -0
  73. wl_cli/commands/git/push_update.py +82 -0
  74. wl_cli/commands/initenv/__init__.py +1 -0
  75. wl_cli/commands/initenv/common/__init__.py +23 -0
  76. wl_cli/commands/initenv/common/check_git_bash.py +78 -0
  77. wl_cli/commands/initenv/common/error_handler.py +68 -0
  78. wl_cli/commands/initenv/common/exceptions.py +52 -0
  79. wl_cli/commands/initenv/components/__init__.py +4 -0
  80. wl_cli/commands/initenv/components/cargo_sync.py +240 -0
  81. wl_cli/commands/initenv/components/config_manager.py +106 -0
  82. wl_cli/commands/initenv/components/directory_utils.py +71 -0
  83. wl_cli/commands/initenv/components/git/__init__.py +9 -0
  84. wl_cli/commands/initenv/components/git/branch_creator.py +143 -0
  85. wl_cli/commands/initenv/components/git/git_flow_interface.py +24 -0
  86. wl_cli/commands/initenv/components/git/git_flow_manager.py +159 -0
  87. wl_cli/commands/initenv/components/git/git_lfs_manager.py +272 -0
  88. wl_cli/commands/initenv/components/git/git_utility.py +254 -0
  89. wl_cli/commands/initenv/components/git/gitignore_manager.py +53 -0
  90. wl_cli/commands/initenv/components/git_initializer.py +212 -0
  91. wl_cli/commands/initenv/components/i18n_manager.py +104 -0
  92. wl_cli/commands/initenv/components/log_manager.py +241 -0
  93. wl_cli/commands/initenv/components/project_structure/__init__.py +23 -0
  94. wl_cli/commands/initenv/components/project_structure/config_generator.py +360 -0
  95. wl_cli/commands/initenv/components/project_structure/directory_creator.py +184 -0
  96. wl_cli/commands/initenv/components/project_structure/hook_source.py +110 -0
  97. wl_cli/commands/initenv/components/project_structure/hooks_manager.py +995 -0
  98. wl_cli/commands/initenv/components/project_structure/readme_handler.py +56 -0
  99. wl_cli/commands/initenv/components/project_structure/rust_files_handler.py +30 -0
  100. wl_cli/commands/initenv/components/project_structure/setup_handler.py +103 -0
  101. wl_cli/commands/initenv/components/project_structure/sop_generator.py +73 -0
  102. wl_cli/commands/initenv/components/project_structure/template_copier.py +104 -0
  103. wl_cli/commands/initenv/components/project_structure/template_manager.py +195 -0
  104. wl_cli/commands/initenv/components/pyproject_generator.py +194 -0
  105. wl_cli/commands/initenv/components/rust_initializer.py +145 -0
  106. wl_cli/commands/initenv/components/symlink_cleaner.py +99 -0
  107. wl_cli/commands/initenv/components/symlink_manager.py +52 -0
  108. wl_cli/commands/initenv/components/venv_manager.py +118 -0
  109. wl_cli/commands/initenv/initenv.py +169 -0
  110. wl_cli/commands/initenv/services/__init__.py +11 -0
  111. wl_cli/commands/initenv/services/environment_initializer.py +288 -0
  112. wl_cli/commands/initenv/services/git_lfs_initializer.py +26 -0
  113. wl_cli/commands/initenv/services/project_structure_initializer.py +52 -0
  114. wl_cli/commands/lfs/README.md +221 -0
  115. wl_cli/commands/lfs/__init__.py +5 -0
  116. wl_cli/commands/lfs/cli.py +106 -0
  117. wl_cli/commands/lfs/config.py +164 -0
  118. wl_cli/commands/lfs/hook.py +61 -0
  119. wl_cli/commands/lfs/initializer.py +531 -0
  120. wl_cli/commands/lfs/manager.py +95 -0
  121. wl_cli/commands/lfs/policy.py +165 -0
  122. wl_cli/commands/lfs/watcher.py +294 -0
  123. wl_cli/commands/lint/__init__.py +1 -0
  124. wl_cli/commands/lint/lint_command.py +297 -0
  125. wl_cli/commands/lint/lint_executor.py +409 -0
  126. wl_cli/commands/lint/lint_reporter.py +579 -0
  127. wl_cli/commands/lint/lint_run.py +258 -0
  128. wl_cli/commands/lint/mypy_config.py +90 -0
  129. wl_cli/commands/registry.py +103 -0
  130. wl_cli/commands/self/__init__.py +7 -0
  131. wl_cli/commands/self/common/__init__.py +8 -0
  132. wl_cli/commands/self/common/capabilities.py +30 -0
  133. wl_cli/commands/self/common/config.py +27 -0
  134. wl_cli/commands/self/common/version_cache.py +85 -0
  135. wl_cli/commands/self/handlers/__init__.py +19 -0
  136. wl_cli/commands/self/handlers/base_handler.py +28 -0
  137. wl_cli/commands/self/handlers/install_handler.py +85 -0
  138. wl_cli/commands/self/handlers/status_handler.py +66 -0
  139. wl_cli/commands/self/handlers/uninstall_handler.py +58 -0
  140. wl_cli/commands/self/handlers/update_handler.py +102 -0
  141. wl_cli/commands/self/handlers/version_handler.py +16 -0
  142. wl_cli/commands/self/self_command.py +237 -0
  143. wl_cli/commands/self/services/__init__.py +15 -0
  144. wl_cli/commands/self/services/installation_manager.py +187 -0
  145. wl_cli/commands/self/services/local_installer.py +165 -0
  146. wl_cli/commands/self/services/pypi_updater.py +278 -0
  147. wl_cli/commands/self/services/windows_update.py +174 -0
  148. wl_cli/commands/testcommands/__init__.py +8 -0
  149. wl_cli/commands/testcommands/base_dependency_analyzer.py +198 -0
  150. wl_cli/commands/testcommands/batch_processor.py +278 -0
  151. wl_cli/commands/testcommands/cache_manager.py +1156 -0
  152. wl_cli/commands/testcommands/coverage_analyzer.py +955 -0
  153. wl_cli/commands/testcommands/coverage_cache_manager.py +183 -0
  154. wl_cli/commands/testcommands/progress_manager.py +79 -0
  155. wl_cli/commands/testcommands/python_env_detector.py +207 -0
  156. wl_cli/commands/testcommands/report_generator.py +101 -0
  157. wl_cli/commands/testcommands/single_test_executor.py +250 -0
  158. wl_cli/commands/testcommands/speed_checker.py +118 -0
  159. wl_cli/commands/testcommands/test_command_builder.py +165 -0
  160. wl_cli/commands/testcommands/test_commands.py +786 -0
  161. wl_cli/commands/testcommands/test_reporter.py +181 -0
  162. wl_cli/commands/testcommands/test_result_handler.py +96 -0
  163. wl_cli/commands/testcommands/test_runner.py +1343 -0
  164. wl_cli/commands/testcommands/test_utils.py +197 -0
  165. wl_cli/commands/testcommands/utils/__init__.py +17 -0
  166. wl_cli/commands/testcommands/utils/coverage_parser.py +98 -0
  167. wl_cli/commands/testcommands/utils/env_info.py +36 -0
  168. wl_cli/commands/testcommands/utils/junit_parser.py +75 -0
  169. wl_cli/commands/testcommands/utils/markdown_utils.py +20 -0
  170. wl_cli/commands/testcommands/utils/project_root.py +61 -0
  171. wl_cli/commands/update/__init__.py +9 -0
  172. wl_cli/commands/update/services/__init__.py +21 -0
  173. wl_cli/commands/update/services/backup_manager.py +207 -0
  174. wl_cli/commands/update/services/component_manager.py +83 -0
  175. wl_cli/commands/update/services/conflict_resolver.py +189 -0
  176. wl_cli/commands/update/services/vendors_updater.py +85 -0
  177. wl_cli/commands/update/services/version_tracker.py +127 -0
  178. wl_cli/commands/update/update_command.py +270 -0
  179. wl_cli/commands/update/utils/__init__.py +3 -0
  180. wl_cli/commands/version/__init__.py +9 -0
  181. wl_cli/commands/version/version_command.py +155 -0
  182. wl_cli/commands/workflow.py +477 -0
  183. wl_cli/env/__init__.py +5 -0
  184. wl_cli/env/cli.py +167 -0
  185. wl_cli/env_status.py +157 -0
  186. wl_cli/envelope.py +91 -0
  187. wl_cli/exceptions.py +85 -0
  188. wl_cli/lib/__init__.py +3 -0
  189. wl_cli/lib/rust_utils.py +35 -0
  190. wl_cli/mcp/__init__.py +6 -0
  191. wl_cli/mcp/capabilities.py +19 -0
  192. wl_cli/mcp/cli.py +22 -0
  193. wl_cli/mcp/gitlab.py +553 -0
  194. wl_cli/mcp/k8s.py +344 -0
  195. wl_cli/mcp/protocol.py +159 -0
  196. wl_cli/platform.py +139 -0
  197. wl_cli/quality/__init__.py +18 -0
  198. wl_cli/utils/__init__.py +7 -0
  199. wl_cli/utils/cache.py +491 -0
  200. wl_cli/utils/cache_ledger.py +355 -0
  201. wl_cli/utils/cache_store.py +546 -0
  202. wl_cli/utils/config/__init__.py +27 -0
  203. wl_cli/utils/config/config_loader.py +163 -0
  204. wl_cli/utils/config/core.py +862 -0
  205. wl_cli/utils/config/defaults.py +142 -0
  206. wl_cli/utils/config/env_exporter.py +86 -0
  207. wl_cli/utils/config/env_parser.py +108 -0
  208. wl_cli/utils/config/validation.py +280 -0
  209. wl_cli/utils/error_handler.py +165 -0
  210. wl_cli/utils/file_copier.py +56 -0
  211. wl_cli/utils/file_operations.py +506 -0
  212. wl_cli/utils/language_utils.py +122 -0
  213. wl_cli/utils/lazy_import.py +240 -0
  214. wl_cli/utils/log_levels.py +35 -0
  215. wl_cli/utils/log_rotators.py +74 -0
  216. wl_cli/utils/logging.py +458 -0
  217. wl_cli/utils/output_context.py +142 -0
  218. wl_cli/utils/performance/__init__.py +1 -0
  219. wl_cli/utils/performance/decorators.py +111 -0
  220. wl_cli/utils/performance/exporter.py +38 -0
  221. wl_cli/utils/performance/models.py +16 -0
  222. wl_cli/utils/performance/monitor.py +161 -0
  223. wl_cli/utils/performance_monitor.py +32 -0
  224. wl_cli/utils/platform_adapter.py +131 -0
  225. wl_cli/utils/pre_commit.py +266 -0
  226. wl_cli/utils/pre_commit_config.py +72 -0
  227. wl_cli/utils/root.py +521 -0
  228. wl_cli/utils/root_fallback.py +256 -0
  229. wl_cli/utils/run_evidence.py +236 -0
  230. wl_cli/utils/structured_logger.py +18 -0
  231. wl_cli/utils/structured_logging/__init__.py +19 -0
  232. wl_cli/utils/structured_logging/config.py +337 -0
  233. wl_cli/utils/structured_logging/core.py +177 -0
  234. wl_cli/utils/structured_logging/env.py +56 -0
  235. wl_cli/utils/structured_logging/filtering.py +75 -0
  236. wl_cli/utils/structured_logging/filters.py +55 -0
  237. wl_cli/utils/structured_logging/formatters.py +251 -0
  238. wl_cli/utils/structured_logging/handlers.py +308 -0
  239. wl_cli/utils/structured_logging/logger.py +354 -0
  240. wl_cli/utils/subprocess.py +71 -0
  241. wl_cli/utils/subprocess_utils.py +260 -0
  242. wl_cli/utils/test_utils/__init__.py +5 -0
  243. wl_cli/utils/test_utils/test_filter.py +36 -0
  244. wl_cli/utils/uv_run.py +79 -0
  245. wl_cli/utils/uv_tool.py +117 -0
  246. wl_cli/utils/uv_tree.py +71 -0
  247. wl_cli/utils/uv_workspace.py +174 -0
  248. wl_cli/utils/version/__init__.py +15 -0
  249. wl_cli/utils/version/pypi_version_checker.py +95 -0
  250. wl_cli/utils/version/version_comparator.py +133 -0
  251. wl_cli/utils/version/version_detectors.py +312 -0
  252. wl_cli/utils/version/version_service.py +131 -0
  253. wl_cli/utils/version/version_updater.py +198 -0
  254. wl_cli/utils/wl_dir_updater.py +1440 -0
  255. wl_cli/utils/workspace_detector/__init__.py +28 -0
  256. wl_cli/utils/workspace_detector/core.py +161 -0
  257. wl_cli/utils/workspace_detector/detection_strategies.py +107 -0
  258. wl_cli/utils/workspace_detector/exceptions.py +5 -0
  259. wl_cli/utils/workspace_detector/types.py +17 -0
  260. wl_cli/utils/workspace_detector/venv_resolver.py +84 -0
  261. wl_cli/vendors/config/.codespellrc +2 -0
  262. wl_cli/vendors/config/dependencies.toml +10 -0
  263. wl_cli/vendors/config/mypy.ini +62 -0
  264. wl_cli/vendors/config/pyproject_defaults.toml +28 -0
  265. wl_cli/vendors/config/template_config.toml +24 -0
  266. wl_cli/vendors/config/tool_configs.toml +76 -0
  267. wl_cli/vendors/git/.gitignore +122 -0
  268. wl_cli/vendors/git/.pre-commit-config.yaml +37 -0
  269. wl_cli/vendors/hooks/applypatch-msg.sample +15 -0
  270. wl_cli/vendors/hooks/commit-msg.sample +24 -0
  271. wl_cli/vendors/hooks/fsmonitor-watchman.sample +174 -0
  272. wl_cli/vendors/hooks/post-update.sample +8 -0
  273. wl_cli/vendors/hooks/pre-applypatch.sample +14 -0
  274. wl_cli/vendors/hooks/pre-commit +45 -0
  275. wl_cli/vendors/hooks/pre-commit.legacy +31 -0
  276. wl_cli/vendors/hooks/pre-commit.sample +49 -0
  277. wl_cli/vendors/hooks/pre-merge-commit.sample +13 -0
  278. wl_cli/vendors/hooks/pre-push.sample +60 -0
  279. wl_cli/vendors/hooks/pre-rebase.sample +169 -0
  280. wl_cli/vendors/hooks/pre-receive.sample +24 -0
  281. wl_cli/vendors/hooks/prepare-commit-msg.sample +42 -0
  282. wl_cli/vendors/hooks/push-to-checkout.sample +78 -0
  283. wl_cli/vendors/hooks/sendemail-validate.sample +77 -0
  284. wl_cli/vendors/hooks/unix/applypatch-msg.sample +15 -0
  285. wl_cli/vendors/hooks/unix/check_lockfile.py +58 -0
  286. wl_cli/vendors/hooks/unix/check_lockfile.sh +36 -0
  287. wl_cli/vendors/hooks/unix/commit-msg.sample +24 -0
  288. wl_cli/vendors/hooks/unix/fsmonitor-watchman.sample +174 -0
  289. wl_cli/vendors/hooks/unix/post-update.sample +8 -0
  290. wl_cli/vendors/hooks/unix/pre-applypatch.sample +14 -0
  291. wl_cli/vendors/hooks/unix/pre-commit +190 -0
  292. wl_cli/vendors/hooks/unix/pre-commit.legacy +31 -0
  293. wl_cli/vendors/hooks/unix/pre-commit.py +81 -0
  294. wl_cli/vendors/hooks/unix/pre-commit.sample +49 -0
  295. wl_cli/vendors/hooks/unix/pre-merge-commit.sample +13 -0
  296. wl_cli/vendors/hooks/unix/pre-push +417 -0
  297. wl_cli/vendors/hooks/unix/pre-push.sample +53 -0
  298. wl_cli/vendors/hooks/unix/pre-rebase.sample +169 -0
  299. wl_cli/vendors/hooks/unix/pre-receive.sample +24 -0
  300. wl_cli/vendors/hooks/unix/prepare-commit-msg.sample +42 -0
  301. wl_cli/vendors/hooks/update.sample +128 -0
  302. wl_cli/vendors/hooks/win/work.bat +63 -0
  303. wl_cli/vendors/project/.gitignore +90 -0
  304. wl_cli/vendors/project/.python-version +1 -0
  305. wl_cli/vendors/project/LICENSE +21 -0
  306. wl_cli/vendors/project/README.md +0 -0
  307. wl_cli/vendors/project/pyproject.toml +27 -0
  308. wl_cli/vendors/project/pyproject_no_workspace.toml +16 -0
  309. wl_cli/vendors/project/pyproject_workspace.toml +14 -0
  310. wl_cli/vendors/readme/README.md +120 -0
  311. wl_cli/vendors/rust/Cargo.toml +14 -0
  312. wl_cli/vendors/rust/lib/__init__.py +3 -0
  313. wl_cli/vendors/rust/lib/rust_utils.py +31 -0
  314. wl_cli/vendors/rust/src/lib.rs +13 -0
  315. wl_cli/vendors/rust/src/main.rs +3 -0
  316. wl_cli/vendors/rust/tests/test_rust_fallback.py +40 -0
  317. wl_cli/vendors/sop_doc/git-dev-sop.md +220 -0
  318. wl_cli/vendors/sop_doc/industrial-standard-sop.md +145 -0
  319. wl_cli/vendors/sop_doc/python-sop.md +342 -0
  320. wl_cli/vendors/sop_doc/rust-readme.md +219 -0
@@ -0,0 +1,256 @@
1
+ """The one place ADR 0006's caller-side CWD fallback policy is implemented.
2
+
3
+ ``utils/root.py`` answers "which root does this starting point belong to" and
4
+ reports "no root found" as ``None`` -- it performs no fallback of its own. *What
5
+ to do* with that ``None`` is the caller's policy, and ADR 0006
6
+ (``docs/adr/0006-root-resolution-layering.md``) commits to implementing it in
7
+ exactly one place at the CLI top level. This module is that place.
8
+
9
+ The policy is one rule: a ``wl`` run from a directory that belongs to no root is
10
+ not an error, so the starting directory itself is used as the root, labelled
11
+ with the kind the caller asked for. Before #126 that rule was written out at
12
+ every call site -- sixteen markers across nine files, converging on nine real
13
+ landing points, several of them byte-identical copies of the same three lines --
14
+ and they now all route through :func:`resolve_root_or_start`.
15
+
16
+ Two properties are load-bearing and deliberate:
17
+
18
+ * **It takes the kind.** Both the ``PROJECT`` and ``DEPLOY`` roots fall back the
19
+ same way, so the rule is parameterised by kind rather than copied per kind.
20
+ * **It never touches *start*.** ``resolve`` answers for the *resolved* spelling
21
+ of its input, but every old resolver fell back to the spelling the caller
22
+ actually passed. So this function uses *start* verbatim -- as the ``resolve``
23
+ input and as the fallback answer's ``path`` -- and leaves canonicalisation to
24
+ the caller. The callers that resolved their start through a ``_start_directory``
25
+ helper still do (keeping their fallback byte-identical to the answer it
26
+ replaces), and ``ConfigPathResolver.get_default_config_path`` keeps passing its
27
+ *un-resolved* ``Path.cwd()`` exactly as before.
28
+
29
+ This module is deliberately **not** part of ``utils/root.py``: that module's
30
+ whole contract is that it performs no fallback, and folding this in would widen
31
+ its deliberate three-name interface (``Root`` / ``RootKind`` / ``resolve``) with
32
+ a fourth name that answers a different question.
33
+
34
+ #125 added :func:`current_root` -- the *other* caller-side policy over the root
35
+ module: which directory the process itself is anchored at. ``resolve`` requires
36
+ an explicit starting point and never reads an implicit CWD (ADR 0006), so
37
+ something has to supply the process CWD as that start, and this is the single
38
+ place that does. It exists because a bare ``Path(".wl")`` -- resolved against
39
+ the process CWD -- used to be scattered across ~20 sites, so a ``wl`` run from a
40
+ subdirectory read the subdirectory's ``.wl`` instead of the root's.
41
+
42
+ #160 added :func:`_start_directory` (the one CWD read both policies below share)
43
+ and :func:`current_context`, which resolves the three roots one invocation is
44
+ anchored on -- project, deploy and the legacy ``.wl`` to migrate -- *together*,
45
+ once, at a public entry. Callers that need only one root go through
46
+ :func:`current_root` instead of carrying a context they do not use.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ from dataclasses import dataclass
52
+ from pathlib import Path
53
+
54
+ from .root import Root, RootKind, resolve
55
+
56
+ __all__ = [
57
+ "resolve_root_or_start",
58
+ "current_root",
59
+ "current_worktree",
60
+ "RootContext",
61
+ "current_context",
62
+ ]
63
+
64
+
65
+ @dataclass(frozen=True)
66
+ class RootContext:
67
+ """The roots one ``wl`` invocation is anchored on, resolved once (#160).
68
+
69
+ Before #160 the self-sync chain re-derived its roots at every layer, each
70
+ layer spelling the derivation out as ``Root | None = None`` with an
71
+ ``else _deploy_root()`` default. A caller that forgot to thread the value
72
+ down did not fail -- it silently fell back to the process CWD, which is the
73
+ bug class #140 removed. Resolving the three values once at the public entry
74
+ and handing this frozen value down makes a missing value a type error.
75
+
76
+ * ``project`` -- the ``PROJECT`` root (nearest ``.wl`` / ``pyproject.toml``);
77
+ * ``deploy`` -- the ``DEPLOY`` root (git work-tree top, else project root);
78
+ * ``legacy_wl_dir`` -- the pre-#95/#96 ``.wl`` still to be migrated, or
79
+ ``None`` when there is none (see :func:`current_context`).
80
+
81
+ Every field is already a real root: the fallback policy (:func:`current_root`
82
+ / :func:`resolve_root_or_start`) has been applied, so no consumer has to
83
+ re-answer "what if there is none".
84
+ """
85
+
86
+ project: Root
87
+ deploy: Root
88
+ legacy_wl_dir: Path | None
89
+
90
+
91
+ def resolve_root_or_start(start: Path, kind: RootKind) -> Root:
92
+ """The ``kind``-root for *start*, falling back to *start* when there is none.
93
+
94
+ ``resolve(start, kind)`` reports "no root found from here" as ``None`` (ADR
95
+ 0006: a result, not an error). Commands whose contract is to return *some*
96
+ root -- ``wl lint``, ``wl gate``, ``wl clean``, the ``.wl`` self-sync, the
97
+ test subsystem -- substitute the starting directory instead, and this is the
98
+ single function that does so.
99
+
100
+ *start* is used exactly as given, both as the ``resolve`` input and as the
101
+ fallback answer's ``path``: no canonicalisation happens here, so a caller
102
+ that needs a resolved spelling resolves it before calling (see the module
103
+ docstring for why that preserves behaviour).
104
+
105
+ The returned value is always a :class:`~wl_cli.utils.root.Root`; a caller
106
+ that previously handed back a bare :class:`~pathlib.Path` (``lint`` /
107
+ ``clean`` / ``TemplateManager``) or a ``str`` (``project_root_for``) keeps
108
+ that outward type by taking ``.path`` or ``str(...)`` of the result, so no
109
+ caller's signature changes.
110
+ """
111
+ root = resolve(start, kind)
112
+ if root is not None:
113
+ return root
114
+ return Root(start, kind)
115
+
116
+
117
+ def current_root(kind: RootKind) -> Root:
118
+ """The ``kind``-root the process's *current directory* belongs to (#125).
119
+
120
+ The root module requires an explicit starting point and never reads an
121
+ implicit CWD (ADR 0006). Callers that hold a start of their own pass it to
122
+ :func:`resolve_root_or_start`; callers that *are* the process -- a
123
+ :class:`~click.Command` with no start in hand -- need the CWD turned into a
124
+ start, and this is the single place that does so. Before #125 each such
125
+ caller spelled this out as a bare ``Path(".wl")``, which resolves against
126
+ the process CWD: a ``wl`` run from a subdirectory then read the
127
+ subdirectory's ``.wl`` instead of the root's. Routing every one of them
128
+ through here is what makes the anchor the *root* the CWD belongs to.
129
+
130
+ The read is :func:`_start_directory` -- a deleted working directory falls
131
+ back to a relative ``.`` (#95), and the result is canonicalised so the anchor
132
+ matches the resolved root -- and the "no root found" policy is
133
+ :func:`resolve_root_or_start`'s, not a private copy. The returned value is
134
+ always a :class:`~wl_cli.utils.root.Root`: callers wanting the root itself
135
+ take ``.path``, those wanting its control directory take ``.wl_dir()``.
136
+ """
137
+ return resolve_root_or_start(_start_directory(), kind)
138
+
139
+
140
+ def current_worktree() -> Root | None:
141
+ """The ``WORKTREE`` root the process's current directory belongs to, or ``None``.
142
+
143
+ The hooks leg is the one consumer (#160): it must tell "inside a git work
144
+ tree" from "not inside one", because outside one there is no
145
+ ``core.hooksPath`` to read and nothing git would run. :func:`current_root`
146
+ cannot serve that question -- its whole point is to *never* answer ``None``,
147
+ so applying its fallback here would turn "not a work tree" into "the CWD",
148
+ and ``hooks_path_is_stale`` would then report a stale path forever.
149
+
150
+ So this is the same start read and the same ``resolve`` call the deleted
151
+ per-module copies spelled out, exposed once: a public seam rather than a
152
+ cross-module import of :func:`_start_directory`, which no caller outside
153
+ this module should reach for.
154
+ """
155
+ return resolve(_start_directory(), RootKind.WORKTREE)
156
+
157
+
158
+ def _start_directory() -> Path:
159
+ """The directory ``wl`` was invoked from, spelled the way the callers expect.
160
+
161
+ The only CWD read **on the root-resolution path** (#160). It is not the only
162
+ CWD read in the tree: :func:`_legacy_wl_dir` below deliberately keeps its own
163
+ raw read (see its docstring), and unrelated subsystems read the CWD for their
164
+ own purposes. What matters here is that ``resolve`` requires an explicit
165
+ starting point and never reads an implicit CWD (ADR 0006), so every root
166
+ policy in this module needs this one value handed to it rather than each
167
+ caller reading it for itself -- the four byte-identical ``_start_directory``
168
+ copies #160 deleted.
169
+
170
+ Spelled *resolved*: the callers being migrated resolved their start before
171
+ passing it to the root module, which keeps their fallbacks byte-identical to
172
+ the answers the old resolvers gave. A deleted working directory falls back
173
+ to a relative ``.`` (the #95 shape); an unresolvable one keeps its verbatim
174
+ spelling.
175
+ """
176
+ try:
177
+ start = Path.cwd()
178
+ except OSError:
179
+ # A deleted working directory -- the #95 shape. The old resolvers
180
+ # bottomed out at a relative ``.``; keep that as the last resort.
181
+ return Path(".")
182
+ try:
183
+ return start.resolve()
184
+ except Exception:
185
+ return start
186
+
187
+
188
+ def current_context() -> RootContext:
189
+ """The three roots the process's current directory is anchored on (#160).
190
+
191
+ The public-entry half of :class:`RootContext`: a caller that needs more than
192
+ one root (the ``.wl`` self-sync) resolves this **once** and threads the
193
+ frozen value down, instead of each layer re-deriving a root from the process
194
+ CWD. Single-root callers use :func:`current_root` and never build one.
195
+
196
+ ``project`` and ``deploy`` go through :func:`resolve_root_or_start`, i.e. the
197
+ same CLI-top-level fallback policy as :func:`current_root`; the values are
198
+ therefore exactly what those callers resolved before #160.
199
+
200
+ ``legacy_wl_dir`` keeps the historical spelling on purpose: the pre-#95/#96
201
+ walk has always been handed the **un-resolved** ``Path.cwd()`` (the F2
202
+ residual in ``wl_dir_updater``). Unifying that spelling with the resolved
203
+ start above is a separate ticket; ``resolve`` canonicalises internally
204
+ either way, so the only observable difference is the deleted-CWD state,
205
+ where the raw read raises where the resolved read would fall back to a
206
+ relative ``.`` (:func:`_legacy_wl_dir` catches it and answers ``None``).
207
+ There is no invocation directory to walk from then, so the context carries
208
+ ``None`` -- which is also what the old chain concluded: both ``.wl`` anchors
209
+ live under a CWD that is gone, so its anchor check returned before the
210
+ legacy walk was ever reached.
211
+ """
212
+ start = _start_directory()
213
+ return RootContext(
214
+ project=resolve_root_or_start(start, RootKind.PROJECT),
215
+ deploy=resolve_root_or_start(start, RootKind.DEPLOY),
216
+ legacy_wl_dir=_legacy_wl_dir(),
217
+ )
218
+
219
+
220
+ def _legacy_wl_dir() -> Path | None:
221
+ """The pre-#95/#96 ``.wl`` at the *invocation* directory, or ``None``.
222
+
223
+ Read with the raw, un-resolved ``Path.cwd()`` -- the spelling
224
+ ``wl_dir_updater.check_and_update_wl_dir`` has always used (the F2 residual:
225
+ it must be resolved *before* the deploy ``.wl`` exists, or the walk resolves
226
+ to the deploy root itself and the migration is silently skipped).
227
+
228
+ A deleted working directory raises ``OSError`` on that raw read, and the
229
+ exception **is caught here and turned into ``None``**: this state has no
230
+ invocation directory to walk from, so there is nothing a legacy walk could
231
+ find. Catching it is load-bearing for the component-level callers that
232
+ drive :func:`current_context` or :meth:`ProjectStructureSetup.setup`
233
+ *directly* (component use, tests, embedding), where an escaping ``OSError``
234
+ would abort the call. No shipped ``wl`` CLI entry reaches this leg in a
235
+ deleted-CWD shell: ``wl init`` reads ``Path.cwd()`` earlier
236
+ (``EnvironmentInitializer.__init__`` /
237
+ ``ProjectStructureInitializer.initialize``), and the per-command hot path
238
+ returns at its anchor early-return (``check_and_update_wl_dir``) before this
239
+ function is reached. The #95 fallback in :func:`_start_directory` must not
240
+ be used for this leg (it would walk a relative ``.`` instead of the
241
+ invocation directory); ``None`` is the answer.
242
+ ``tests/test_root_context_160.py::test_deleted_cwd`` pins this leg, and
243
+ driving ``ProjectStructureSetup.setup`` through it is pinned by
244
+ ``tests/test_project_structure_modules.py::
245
+ test_setup_in_a_deleted_cwd_does_not_raise_oserror``.
246
+
247
+ The bounds of the walk (volume-root ceiling, system-scope blacklist, the
248
+ ``<home>/.wl`` refusal) are the root module's ``LEGACY`` kind (#118); this
249
+ reads no legacy directory of its own.
250
+ """
251
+ try:
252
+ invocation_dir = Path.cwd()
253
+ except OSError:
254
+ return None
255
+ legacy_root = resolve(invocation_dir, RootKind.LEGACY)
256
+ return legacy_root.wl_dir() if legacy_root is not None else None
@@ -0,0 +1,236 @@
1
+ """Structured evidence for one ``wl test`` run -- the cross-process seam (#212).
2
+
3
+ ``wl gate`` used to decide "did this run exercise the tree?" by regex-matching
4
+ the human-readable ``Cache usage: N/M files`` line in ``wl test``'s stdout
5
+ (``_CACHE_USAGE_RE`` / ``cached_result_count``). That made a *presentation*
6
+ string a decision input, with two concrete failures (ADR-0010):
7
+
8
+ * The line is suppressed whenever ``concise`` renders the summary, so the fact
9
+ simply disappears and the gate reads "0 hits".
10
+ * The all-cached run short-circuits before pytest, so it writes no junit report
11
+ -- and a cached run is exactly the state the gate must judge red.
12
+
13
+ This module carries the fact across the process boundary as a versioned value
14
+ instead: which files ran, how many results came from the cache, whether
15
+ ``--no-cache`` was requested, whether the run was cacheable at all, its exit
16
+ code, and when it started/finished. It carries **facts only**; the verdict
17
+ ("a cache hit is red") belongs to :mod:`wl_cli.commands.gate.gate_command`.
18
+
19
+ Reading is deliberately fail-closed: a missing, unreadable, corrupt or
20
+ unknown-version file is ``None`` -- never a half-parsed value. The gate turns
21
+ ``None`` into red and names the path.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import math
28
+ import os
29
+ import tempfile
30
+ from dataclasses import dataclass
31
+ from typing import TYPE_CHECKING, Any
32
+
33
+ if TYPE_CHECKING:
34
+ from pathlib import Path
35
+
36
+ #: Bumped whenever the payload gains, loses, or renames a fact. A reader that
37
+ #: does not know the version returns ``None`` rather than guessing -- a stale
38
+ #: schema can silently mean something else (ADR-0010: "绝不半信").
39
+ SCHEMA_VERSION = 1
40
+
41
+ #: Every fact a reader must find. ``cache_blocked_reason`` is nullable but not
42
+ #: optional: an absent key and an explicit ``null`` mean different things to a
43
+ #: producer, so the reader requires the key and rejects the missing one.
44
+ _REQUIRED_FIELDS: tuple[str, ...] = (
45
+ "schema_version",
46
+ "executed_files",
47
+ "cache_hits",
48
+ "cache_total",
49
+ "no_cache_requested",
50
+ "cacheable",
51
+ "cache_blocked_reason",
52
+ "exit_code",
53
+ "started_at",
54
+ "finished_at",
55
+ )
56
+
57
+
58
+ @dataclass(frozen=True, kw_only=True)
59
+ class TestRunEvidence:
60
+ """The facts one ``wl test`` invocation produced.
61
+
62
+ ``executed_files`` are spellings relative to the project root, in the order
63
+ the run observed them. They are a *record*, not the verdict: the gate only
64
+ refuses runs whose ``cache_hits`` is non-zero.
65
+
66
+ ``no_cache_requested`` states what the caller asked for, so
67
+ "``--no-cache`` was requested but the runner still hit the cache" becomes a
68
+ fact instead of something the gate has to guess.
69
+
70
+ ``cacheable`` / ``cache_blocked_reason`` describe whether the run was
71
+ eligible to write cache at all (an unsafe passthrough switch or
72
+ ``PYTEST_ADDOPTS`` closes it). They are independent of
73
+ ``no_cache_requested``: a run may be cacheable and still asked to bypass
74
+ the cache.
75
+ """
76
+
77
+ #: Keep pytest from collecting this value class as a test case: its name
78
+ #: starts with ``Test`` and it has an ``__init__``. Same guard the CLI's
79
+ #: ``TestCommand`` uses (``commands/testcommands/test_commands.py``).
80
+ __test__ = False
81
+
82
+ executed_files: tuple[str, ...]
83
+ cache_hits: int
84
+ cache_total: int
85
+ no_cache_requested: bool
86
+ cacheable: bool
87
+ cache_blocked_reason: str | None
88
+ exit_code: int
89
+ started_at: float
90
+ finished_at: float
91
+ schema_version: int = SCHEMA_VERSION
92
+
93
+ def to_dict(self) -> dict[str, Any]:
94
+ """The JSON payload, with an explicit shape (and field order)."""
95
+ return {
96
+ "schema_version": self.schema_version,
97
+ "executed_files": list(self.executed_files),
98
+ "cache_hits": self.cache_hits,
99
+ "cache_total": self.cache_total,
100
+ "no_cache_requested": self.no_cache_requested,
101
+ "cacheable": self.cacheable,
102
+ "cache_blocked_reason": self.cache_blocked_reason,
103
+ "exit_code": self.exit_code,
104
+ "started_at": self.started_at,
105
+ "finished_at": self.finished_at,
106
+ }
107
+
108
+ @classmethod
109
+ def from_dict(cls, raw: Any) -> TestRunEvidence | None:
110
+ """Rebuild from a decoded payload, or ``None`` when anything is off.
111
+
112
+ Every required field must be present *and* of the right type, and the
113
+ schema version must be exactly :data:`SCHEMA_VERSION`. A payload that
114
+ merely looks close is rejected whole -- a half-trusted record is worse
115
+ than no record, because the gate would act on a guess.
116
+ """
117
+ if not isinstance(raw, dict):
118
+ return None
119
+ if any(field not in raw for field in _REQUIRED_FIELDS):
120
+ return None
121
+ version = raw.get("schema_version")
122
+ # ``bool`` is a subclass of ``int``, so ``isinstance(True, int)`` is
123
+ # true; the explicit bool exclusion keeps a flag from masquerading as
124
+ # a version.
125
+ if not isinstance(version, int) or isinstance(version, bool):
126
+ return None
127
+ if version != SCHEMA_VERSION:
128
+ return None
129
+
130
+ executed_files = raw.get("executed_files")
131
+ if not isinstance(executed_files, list) or not all(
132
+ isinstance(entry, str) for entry in executed_files
133
+ ):
134
+ return None
135
+
136
+ cache_hits = raw.get("cache_hits")
137
+ cache_total = raw.get("cache_total")
138
+ exit_code = raw.get("exit_code")
139
+ if (
140
+ not isinstance(cache_hits, int)
141
+ or isinstance(cache_hits, bool)
142
+ or not isinstance(cache_total, int)
143
+ or isinstance(cache_total, bool)
144
+ or not isinstance(exit_code, int)
145
+ or isinstance(exit_code, bool)
146
+ ):
147
+ return None
148
+
149
+ no_cache_requested = raw.get("no_cache_requested")
150
+ cacheable = raw.get("cacheable")
151
+ if not isinstance(no_cache_requested, bool) or not isinstance(cacheable, bool):
152
+ return None
153
+
154
+ cache_blocked_reason = raw.get("cache_blocked_reason")
155
+ if cache_blocked_reason is not None and not isinstance(
156
+ cache_blocked_reason, str
157
+ ):
158
+ return None
159
+
160
+ started_at = raw.get("started_at")
161
+ finished_at = raw.get("finished_at")
162
+ if (
163
+ not isinstance(started_at, (int, float))
164
+ or isinstance(started_at, bool)
165
+ or not isinstance(finished_at, (int, float))
166
+ or isinstance(finished_at, bool)
167
+ ):
168
+ return None
169
+ # ``json`` accepts bare ``NaN`` / ``Infinity`` literals by default, and
170
+ # both are ``float`` -- so the type check above lets them through.
171
+ # Neither is an epoch second, so a non-finite timestamp is a corrupt
172
+ # record, judged like any other corrupt payload: whole record rejected
173
+ # (review N1).
174
+ if not math.isfinite(started_at) or not math.isfinite(finished_at):
175
+ return None
176
+
177
+ return cls(
178
+ executed_files=tuple(executed_files),
179
+ cache_hits=cache_hits,
180
+ cache_total=cache_total,
181
+ no_cache_requested=no_cache_requested,
182
+ cacheable=cacheable,
183
+ cache_blocked_reason=cache_blocked_reason,
184
+ exit_code=exit_code,
185
+ started_at=float(started_at),
186
+ finished_at=float(finished_at),
187
+ )
188
+
189
+ def write(self, path: Path) -> None:
190
+ """Write this record to *path* atomically.
191
+
192
+ The payload lands in a sibling temporary file and is moved into place
193
+ with :func:`os.replace`, so a concurrent reader observes either the old
194
+ file or the complete new one -- never a truncated JSON document. Missing
195
+ parent directories are created (the gate hands us a path under the
196
+ system temp directory, which always exists).
197
+ """
198
+ parent = path.parent
199
+ parent.mkdir(parents=True, exist_ok=True)
200
+ payload = json.dumps(self.to_dict(), ensure_ascii=False) + "\n"
201
+
202
+ descriptor, temporary = tempfile.mkstemp(
203
+ prefix=f".{path.name}.", suffix=".tmp", dir=str(parent)
204
+ )
205
+ try:
206
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
207
+ handle.write(payload)
208
+ handle.flush()
209
+ os.fsync(handle.fileno())
210
+ os.replace(temporary, path)
211
+ except BaseException:
212
+ try:
213
+ os.unlink(temporary)
214
+ except OSError:
215
+ pass
216
+ raise
217
+
218
+ @classmethod
219
+ def read(cls, path: Path | None) -> TestRunEvidence | None:
220
+ """Read the evidence at *path*, or ``None`` when it cannot be trusted.
221
+
222
+ Missing, unreadable, non-JSON, wrong-shape, wrong-type and
223
+ unknown-version payloads all collapse to ``None``: the gate's
224
+ fail-closed red. There is no partial result and no fallback.
225
+ """
226
+ if path is None:
227
+ return None
228
+ try:
229
+ text = path.read_text(encoding="utf-8")
230
+ except (OSError, UnicodeDecodeError):
231
+ return None
232
+ try:
233
+ raw = json.loads(text)
234
+ except (ValueError, RecursionError):
235
+ return None
236
+ return cls.from_dict(raw)
@@ -0,0 +1,18 @@
1
+ """
2
+ Structured logging implementation for WL Commands.
3
+
4
+ This file maintains backward compatibility while delegating to the modularized
5
+ structured_logging package.
6
+ """
7
+
8
+ # Import the modularized StructuredLogger from the structured_logging package
9
+ # Import datetime for backward compatibility with tests
10
+ import datetime as _datetime
11
+
12
+ from .structured_logging.core import StructuredLogger
13
+
14
+ # Re-export for backward compatibility
15
+ __all__ = ["StructuredLogger"]
16
+
17
+ # For backward compatibility, expose datetime
18
+ datetime = _datetime
@@ -0,0 +1,19 @@
1
+ """Structured logging module for WL Commands."""
2
+
3
+ # Export main components from submodules
4
+ from .config import LoggingConfig
5
+ from .core import StructuredLogger, StructuredLoggerCore
6
+ from .filtering import LogLevelChecker
7
+ from .filters import LogFilter
8
+ from .formatters import LogFormatter
9
+ from .handlers import LogHandler
10
+
11
+ __all__ = [
12
+ "StructuredLogger",
13
+ "StructuredLoggerCore",
14
+ "LoggingConfig",
15
+ "LogLevelChecker",
16
+ "LogFormatter",
17
+ "LogHandler",
18
+ "LogFilter",
19
+ ]